Files
booth/booth/templates/base.html
T
vh bf351a26d1 feat(u7): filename groups — the last v1 unit, and a table that did not reproduce
Completes U7 with its fourth component: a jump-to-group rail derived from
filename prefixes, replacing the subfolder sections ROADMAP named. The scope
departure was ratified by the operator 2026-09-22; this commit deletes
test_no_group_rail_is_shipped_yet, the guard that held it back, in the same
change that builds what it guarded against.

All seven v1 capabilities are now landed. The 1.0 cut is a decision, not a
dependency, and it is the operator's — no version bump here, because a commit
is not a release.

THE RULE CHANGED AT IMPLEMENTATION, ON MEASURED GROUNDS. The contract specified
`strip ONE trailing run of digits`; run against the live set that yields 24
groups for sindra-bakeoff's 40 images and 27 for sindra's 30 — a rail with a row
per tile — because it keys on the END of the stem, where the instance number
lives. The contract's own table claimed 5 and 1 for those two booths and neither
reproduces; the numbers are reachable only by two OTHER heuristics, so the table
that justified the design was assembled from more than one rule. Its own worked
example contradicts it in plain sight.

The shipped rule keys on the first separator-delimited segment, where the family
lives, destemming only when the stem has no separator at all — so `ac01` -> `ac`
while `v30-seed8302` and `v35-seed8302` stay apart. Re-measured across all 17
live booths; the table is in the contract.

INV-3 GAINED ITS SECOND DEGENERACY. The contract guarded one group for
everything (sc-iso-spread: DSC0001-DSC0006). The live set's actual failure is
the opposite — pewpew-ui-brief yields 23 groups for 34 items, dfa-concepts 13
for 20 — and the contract as written would have shipped a rail that is a second
copy of the grid. The rail now renders only when grouping is informative: two or
more groups, and the middle group holding more than one item. That predicate
gets all 17 booths right.

Grouping is a VIEW. The grid stays sorted(rel) and the zoom ring stays that
order filtered to images; the group fixture interleaves across subdirectories
precisely so a (group, rel) re-sort goes red. Groups are derived from the
RENDERED list, not the full gallery, so no anchor points at a filtered-out tile.

booth/items.py       _group_of + Item.group, derived in the resolver (INV-1)
booth/app.py         _groups() builds the rail rows; build_gallery carries it
booth/templates/     the rail-groups nav and its CSS
tests/               +16 tests; 639 green

Every new falsifier was proved by running its defeating change (12/12). Three
were vacuous first time out: one fixture's positional order happened to be
alphabetical, one assertion miscounted elements, and the harness itself
certified a broken test twice — no green baseline, and byte-identical mutations
silently defeated by the pyc cache's one-second mtime granularity.
2026-09-22 21:33:54 -07:00

555 lines
37 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}The Booth{% endblock %}</title>
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' rx='7' fill='%23171a23'/%3E%3Ccircle cx='16' cy='16' r='6' fill='none' stroke='%2342dcd1' stroke-width='2.5'/%3E%3Ccircle cx='16' cy='16' r='2.2' fill='%2342dcd1'/%3E%3C/svg%3E">
<style>
/* ============================================================
The Booth — Corviduo "Australis" theme (aurora accents, dark-first).
Token values adopted from ratatoskr-web (colors_and_type.css canonical
palette). Self-contained, no webfont CDN — system fallback stacks.
============================================================ */
:root{
/* ---- Australis raw palette ---- */
--aus-black:#222531; --aus-white:#a9bcc3; --aus-bright-white:#cce7ec; --aus-bright-black:#373b46;
--aus-dark-30:#414751; --aus-dark-40:#565f69; --aus-dark-50:#6e7882; --aus-dark-60:#86929d;
--aus-bright-70:#9daeb6; --aus-bright-80:#b3cbcf;
--aus-blue:#6388d8; --aus-bright-blue:#a4c4ff;
--aus-cyan:#00b1a8; --aus-bright-cyan:#42dcd1;
--aus-green:#16b866; --aus-bright-green:#51e08a;
--aus-red:#ff491a; --aus-bright-red:#ff854f;
--aus-yellow:#e1c631; --aus-bright-yellow:#ffe14e;
--aus-magenta:#9d78ff; --aus-bright-magenta:#d8adff;
/* ---- semantic surface / foreground ---- */
--border-subtle:var(--aus-dark-30); --border-default:var(--aus-dark-40); --border-strong:var(--aus-dark-50);
--fg-0:var(--aus-bright-white); --fg-1:var(--aus-white); --fg-2:var(--aus-bright-70);
--fg-3:var(--aus-dark-60); --fg-muted:var(--aus-dark-50); --fg-on-accent:var(--aus-black);
/* ---- ratatoskr console surfaces ---- */
--rk-deep:#14161d; --rk-canvas:#171a23; --rk-well:#1b1e28; --rk-panel:#1d2029; --rk-ghost:#2c3040;
/* ---- type ---- */
--font-display:"Space Grotesk","Inter",ui-sans-serif,system-ui,sans-serif;
--font-sans:"Inter",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
--font-mono:"Berkeley Mono","JetBrains Mono","IBM Plex Mono",ui-monospace,"SF Mono","Cascadia Code",Menlo,Consolas,monospace;
/* ---- radius / shadow / motion ---- */
--radius-sm:4px; --radius-md:6px; --radius-lg:10px; --radius-pill:9999px;
--shadow-2:0 2px 6px rgba(10,12,18,.45),0 1px 2px rgba(10,12,18,.4);
--shadow-3:0 8px 24px rgba(10,12,18,.5),0 2px 6px rgba(10,12,18,.4);
--glow-cyan:0 0 0 3px rgba(66,220,209,.26);
--ease-out:cubic-bezier(.2,.8,.2,1);
--ease-aurora:cubic-bezier(.65,0,.35,1);
}
@media (prefers-color-scheme: light){
:root{
--rk-deep:#e5edef; --rk-canvas:#eef4f6; --rk-well:#ffffff; --rk-panel:#f6fafb; --rk-ghost:#cad8dd;
--border-subtle:#d3dfe3; --border-default:#bccad0; --border-strong:#9fb0b7;
--fg-0:#10151c; --fg-1:#26313a; --fg-2:#48555f; --fg-3:#64727c; --fg-muted:#8a99a1; --fg-on-accent:#ffffff;
--aus-bright-cyan:#0a938b; --aus-cyan:#0a938b; --aus-bright-green:#12925a; --aus-green:#12925a;
--aus-bright-blue:#3f66bd; --aus-blue:#4a6fc0; --aus-bright-magenta:#7a52d8; --aus-magenta:#7a52d8;
--aus-bright-red:#d63a15; --aus-red:#d63a15; --aus-bright-yellow:#a9820a; --aus-yellow:#b98f0c;
--aus-dark-40:#b0bec4;
--shadow-3:0 8px 24px rgba(30,50,60,.16),0 2px 6px rgba(30,50,60,.1);
--glow-cyan:0 0 0 3px rgba(10,147,139,.2);
}
}
*{box-sizing:border-box}
html,body{margin:0}
body{background:var(--rk-canvas);color:var(--fg-1);font-family:var(--font-sans);
line-height:1.5;-webkit-font-smoothing:antialiased;min-height:100vh;display:flex;flex-direction:column;
transition:background-color .32s var(--ease-aurora),color .32s var(--ease-aurora)}
a{color:var(--aus-bright-cyan);text-decoration:none}
a:hover{color:var(--aus-cyan);text-decoration:underline}
code{font-family:var(--font-mono);font-size:.84em;background:var(--rk-well);color:var(--fg-2);
padding:.12em .42em;border-radius:var(--radius-sm);border:1px solid var(--border-subtle)}
.topbar{display:flex;align-items:center;gap:1.1rem;flex-wrap:wrap;
padding:1rem 1.5rem;border-bottom:1px solid var(--border-subtle);
background:linear-gradient(180deg,var(--rk-panel),transparent)}
.brand{display:inline-flex;align-items:center;gap:.6rem;color:var(--fg-0)}
.brand:hover{text-decoration:none}
.brand .dot{width:.62rem;height:.62rem;border-radius:50%;background:var(--aus-bright-cyan);
box-shadow:0 0 0 4px rgba(66,220,209,.15),0 0 12px rgba(66,220,209,.5);
animation:pulse 2.8s var(--ease-aurora) infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
.brand .name{font-family:var(--font-display);font-size:1.22rem;font-weight:600;letter-spacing:-.01em}
.tagline{color:var(--aus-cyan);font-size:.66rem;font-family:var(--font-mono);
letter-spacing:.2em;text-transform:uppercase}
main{flex:1;width:100%;max-width:1240px;margin:0 auto;padding:1.7rem 1.5rem 3rem}
/* ---- upload / pickup ---- */
.uploader{display:flex;gap:.9rem;align-items:stretch;margin-bottom:1.7rem;flex-wrap:wrap}
.drop{flex:1 1 300px;position:relative;display:flex;flex-direction:column;align-items:center;
justify-content:center;gap:.15rem;text-align:center;cursor:pointer;padding:1.1rem 1rem;
border:1.5px dashed var(--border-default);border-radius:var(--radius-lg);background:var(--rk-well);
transition:border-color .16s var(--ease-out),background .16s var(--ease-out),box-shadow .16s var(--ease-out)}
.drop:hover{border-color:var(--aus-cyan)}
.drop.over{border-color:var(--aus-bright-cyan);background:rgba(66,220,209,.06);box-shadow:var(--glow-cyan)}
.drop.has{border-style:solid;border-color:var(--aus-cyan)}
.drop input[type=file]{position:absolute;inset:0;width:100%;height:100%;opacity:0;cursor:pointer}
.drop-icon{font-size:1.2rem;color:var(--aus-bright-cyan)}
.drop-main{font-family:var(--font-display);font-weight:600;color:var(--fg-0);font-size:.98rem}
.drop-sub{font-family:var(--font-mono);font-size:.7rem;letter-spacing:.03em;color:var(--fg-3)}
.up-go{flex:0 0 auto;align-self:stretch;cursor:pointer;font-family:var(--font-mono);font-size:.74rem;
letter-spacing:.1em;text-transform:uppercase;padding:0 1.15rem;border-radius:var(--radius-lg);
background:transparent;border:1px solid var(--aus-cyan);color:var(--aus-bright-cyan);
transition:.14s var(--ease-out)}
.up-go:hover{background:var(--aus-bright-cyan);color:var(--fg-on-accent);border-color:var(--aus-bright-cyan)}
.badge{display:inline-block;font-family:var(--font-mono);font-size:.6rem;letter-spacing:.1em;
text-transform:uppercase;font-weight:600;color:var(--fg-on-accent);background:var(--aus-bright-cyan);
padding:.08rem .42rem;border-radius:var(--radius-pill);vertical-align:middle}
.card .thumb{position:relative}
.thumb .badge{position:absolute;top:.5rem;left:.5rem;box-shadow:var(--shadow-2)}
/* Kept lane. The accent is a 2px TOP edge in aurora blue — the one accent
border Australis sanctions (never a coloured left border), and it marks the
card as featured without changing its fill, so kept and ephemeral still
read as the same family of object. */
.lane-head{margin:1.9rem 0 .8rem;font-family:var(--font-mono);font-size:.68rem;font-weight:600;
letter-spacing:.14em;text-transform:uppercase;color:var(--aus-bright-cyan);
display:flex;align-items:center;gap:.7rem}
.lane-head::after{content:"";flex:1;height:1px;background:var(--border-subtle)}
.lane-note{font-weight:400;letter-spacing:.06em;color:var(--fg-3);text-transform:none}
.lane-note code{font-size:.95em;color:var(--fg-2)}
.kept-grid{margin-bottom:.4rem}
.card-kept{border-top:2px solid var(--aus-blue)}
.card-kept:hover{border-color:var(--aus-blue);border-top-color:var(--aus-bright-blue)}
.badge-kept{background:var(--aus-blue);color:var(--fg-on-accent)}
/* Release sits where the ephemeral card's × sits, but reads as a word rather
than a destructive glyph — it is not the delete, it is what unlocks it. */
/* One positioned row holds BOTH kept-card controls. They used to pin
themselves to the same corner independently and the later sibling won. */
.kept-actions{position:absolute;top:.4rem;right:.4rem;display:flex;gap:.3rem;
align-items:center;opacity:0;transition:opacity .12s}
.card-kept:hover .kept-actions,.kept-actions:focus-within{opacity:1}
.kept-actions form{position:static;opacity:1;margin:0}
.release{opacity:0;transition:opacity .12s}
.card-kept:hover .release,.release:focus-within{opacity:1}
.release button{font:inherit;font-size:.72rem;line-height:1;padding:.22rem .45rem;
border-radius:.3rem;cursor:pointer;border:1px solid var(--aus-blue);
background:var(--rk-panel);color:var(--aus-blue)}
.release button:hover{background:var(--aus-blue);color:var(--fg-on-accent)}
/* ---- the standing link board ------------------------------------------
Rows, not a markdown blob. Dense enough that thirty entries stay
scannable, with provenance de-emphasised so the description leads and the
× only surfaces on hover — destructive controls should not compete for
attention with the thing you came to read. */
.board{border:1px solid var(--border-subtle);border-radius:.5rem;overflow:hidden;
background:var(--rk-panel);margin:.6rem 0 1rem}
.board-head{display:flex;align-items:center;gap:.6rem;padding:.45rem .75rem;
border-bottom:1px solid var(--border-subtle);background:var(--rk-well)}
.board-selall{display:inline-flex;align-items:center;cursor:pointer;flex:0 0 auto}
.board-title{font-weight:600;font-size:.85rem}
.board-note{font-size:.72rem;color:var(--fg-3)}
.board-spacer{flex:1 1 auto}
/* Bulk delete: quiet until something is selected. Red-outline danger word,
matching the .wipe-lg language — fills on hover, greys out when disabled. */
.board-del-sel{flex:0 0 auto;cursor:pointer;font-family:var(--font-mono);font-size:.7rem;
letter-spacing:.06em;padding:.28rem .6rem;border-radius:var(--radius-sm);
background:transparent;border:1px solid var(--aus-red);color:var(--aus-bright-red);
transition:.14s var(--ease-out)}
.board-del-sel:hover:not(:disabled){background:var(--aus-red);border-color:var(--aus-red);color:#fff}
.board-del-sel:disabled{opacity:.4;cursor:default;border-color:var(--border-default);color:var(--fg-3)}
.board-row{display:flex;align-items:center;gap:.6rem;padding:.45rem .75rem;
border-bottom:1px solid var(--border-subtle);transition:background .1s}
.board-row:last-child{border-bottom:0}
.board-row:hover{background:var(--rk-well)}
/* Pinned rows: a subtle cyan wash + a filled star. NOT a coloured left border
(Australis forbids it); the tint is enough to group them, and the ★ carries
the state at the row level. */
.board-row.is-pinned{background:rgba(66,220,209,.05)}
.board-row.is-pinned:hover{background:rgba(66,220,209,.09)}
.board-check{flex:0 0 auto;width:1rem;height:1rem;cursor:pointer;accent-color:var(--aus-bright-cyan)}
.board-selall input{width:1rem;height:1rem;cursor:pointer;accent-color:var(--aus-bright-cyan)}
/* ★ favorite toggle. Always visible (unlike ×) because it carries state you
need to see; dim-hollow when off, warm-gold and filled when pinned. */
.board-pin{flex:0 0 auto;border:0;background:none;cursor:pointer;line-height:1;
font-size:1.02rem;padding:.05rem .2rem;border-radius:.25rem;color:var(--fg-3);
transition:color .12s,transform .12s var(--ease-out)}
.board-pin:hover{color:var(--aus-bright-yellow);transform:scale(1.15)}
.board-pin.on{color:var(--aus-bright-yellow)}
.board-main{flex:1 1 auto;min-width:0}
.board-link{font-size:.9rem;text-decoration:none;font-weight:500}
.board-link:hover{text-decoration:underline}
.board-url{font-size:.7rem;color:var(--fg-3);opacity:.8;overflow:hidden;text-overflow:ellipsis;
white-space:nowrap;font-family:var(--font-mono)}
.board-meta{flex:0 0 auto;display:flex;flex-direction:column;align-items:flex-end;
gap:.05rem;font-size:.68rem;color:var(--fg-3);white-space:nowrap}
.board-who{font-weight:600}
.board-copy,.board-rm-btn{opacity:0;transition:opacity .12s;flex:0 0 auto}
.board-row:hover .board-copy,.board-row:hover .board-rm-btn,
.board-copy:focus,.board-rm-btn:focus{opacity:1}
.board-rm-btn{font:inherit;font-size:1rem;line-height:1;padding:.1rem .35rem;
border:0;background:none;cursor:pointer;color:var(--fg-2);border-radius:.25rem}
.board-rm-btn:hover{background:var(--aus-red);color:#fff}
@media (max-width:600px){
/* No hover on touch — controls must be permanently visible or unreachable. */
.board-copy,.board-rm-btn{opacity:1}
.board-meta{display:none}
}
.pickup-note{margin:-.5rem 0 1.5rem;padding:.6rem .85rem;border:1px solid var(--border-subtle);
border-left:3px solid var(--aus-bright-cyan);border-radius:var(--radius-md);background:var(--rk-well);
font-family:var(--font-mono);font-size:.78rem;color:var(--fg-2)}
.copy-btn{cursor:pointer;font-family:var(--font-mono);font-size:.68rem;letter-spacing:.04em;
padding:.12rem .5rem;margin:0 .25rem;border:1px solid var(--border-default);border-radius:var(--radius-sm);
background:transparent;color:var(--aus-bright-cyan);vertical-align:middle;transition:.12s var(--ease-out)}
.copy-btn:hover{border-color:var(--aus-cyan);background:rgba(66,220,209,.08)}
.copy-btn.copied{border-color:var(--aus-green);color:var(--aus-bright-green)}
.dl-link{color:var(--aus-bright-cyan);text-decoration:none;margin-right:.4rem;font-size:.95em}
.dl-link:hover{color:var(--aus-cyan)}
.cap-text{color:var(--fg-2)}
/* ---- image viewer (fixed full-viewport overlay) ---- */
.viewer{position:fixed;inset:0;z-index:50;background:var(--rk-canvas);display:flex;flex-direction:column}
.vbar{display:flex;align-items:center;gap:.7rem;padding:.5rem .8rem;
border-bottom:1px solid var(--border-subtle);background:var(--rk-panel)}
.vname{font-family:var(--font-mono);font-size:.8rem;color:var(--fg-2);
white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:50vw}
.vspacer{flex:1}
.vbtn{display:inline-flex;align-items:center;justify-content:center;min-width:2rem;height:2rem;
padding:0 .55rem;border:1px solid var(--border-default);border-radius:var(--radius-md);
color:var(--fg-1);background:transparent;font-family:var(--font-mono);font-size:.9rem;
transition:.14s var(--ease-out)}
.vbtn:hover{border-color:var(--aus-cyan);color:var(--aus-bright-cyan);text-decoration:none}
.vx:hover{border-color:var(--aus-red);color:#fff;background:var(--aus-red)}
.vtoggle{border:1px solid var(--border-default);border-radius:var(--radius-md);overflow:hidden}
.vseg{cursor:pointer;border:0;background:transparent;color:var(--fg-3);font-family:var(--font-mono);
font-size:.72rem;letter-spacing:.06em;padding:.4rem .75rem;transition:.14s var(--ease-out)}
.vseg+.vseg{border-left:1px solid var(--border-default)}
.vseg:hover{color:var(--fg-1)}
.vseg.on{background:var(--aus-bright-cyan);color:var(--fg-on-accent)}
.vstage{flex:1;min-height:0;background:var(--rk-deep)}
.vstage.fit{display:flex;align-items:center;justify-content:center;overflow:hidden;padding:1rem}
.vstage.fit img{max-width:100%;max-height:100%;width:auto;height:auto;box-shadow:var(--shadow-3)}
.vstage.one{overflow:auto;text-align:center}
.vstage.one img{max-width:none;max-height:none;margin:auto}
.foot{border-top:1px solid var(--border-subtle);color:var(--fg-muted);
font-size:.72rem;font-family:var(--font-mono);letter-spacing:.04em;padding:1rem 1.5rem;text-align:center}
.empty{border:1px dashed var(--border-default);border-radius:var(--radius-lg);
padding:3rem 1.5rem;text-align:center;color:var(--fg-3);background:var(--rk-well);font-family:var(--font-mono);font-size:.92rem}
/* index grid */
.grid{display:grid;gap:1.1rem;grid-template-columns:repeat(auto-fill,minmax(248px,1fr))}
.card{position:relative;background:var(--rk-panel);border:1px solid var(--border-subtle);
border-radius:var(--radius-lg);overflow:hidden;box-shadow:var(--shadow-2);
transition:transform .16s var(--ease-out),border-color .16s var(--ease-out),box-shadow .16s var(--ease-out)}
.card:hover{transform:translateY(-3px);border-color:var(--aus-cyan);box-shadow:var(--shadow-3),var(--glow-cyan)}
.card .thumb{display:block;aspect-ratio:16/10;background:var(--rk-deep);overflow:hidden}
.card .thumb img{width:100%;height:100%;object-fit:cover;display:block}
.card .ph{width:100%;height:100%;display:flex;align-items:center;justify-content:center;
color:var(--fg-3);font-family:var(--font-mono);font-size:.82rem;letter-spacing:.16em;text-transform:uppercase}
.card .meta{padding:.7rem .85rem .85rem}
.card .name{display:block;font-family:var(--font-display);font-weight:600;color:var(--fg-0);word-break:break-word}
.card .name:hover{text-decoration:none;color:var(--aus-bright-cyan)}
.card .sub{color:var(--fg-3);font-size:.72rem;font-family:var(--font-mono);letter-spacing:.03em;margin-top:.3rem}
/* THE ANNOUNCEMENT — who posted this booth and why (U5). Same size and
rhythm as .sub above it, because it is the same class of information: a
second line of card metadata, not a heading. The handle carries the only
colour, so a scan down the index reads as a column of posters. */
.prov{margin-top:.28rem;font-size:.72rem;font-family:var(--font-mono);
letter-spacing:.03em;color:var(--fg-3);line-height:1.45;
overflow-wrap:anywhere}
.prov-who{color:var(--fg-2)}
.prov-why{color:var(--fg-3)}
/* Quiet on purpose. 26 booths arrived before this convention existed and
rsync keeps making more, so the marker has to be visible-if-you-look and
never a badge shouting 26 times. `unreadable` gets the warning tint
because, unlike `unannounced`, it is something somebody has to fix. */
.prov-none{color:var(--fg-muted);font-style:italic}
.prov-broken{color:var(--aus-bright-yellow,#e8c547);font-style:italic;cursor:help}
.wipe{position:absolute;top:.5rem;right:.5rem;margin:0}
/* ★ keep, mirroring .wipe on the other shoulder of the card. Same
hover-to-reveal language as .release in the kept lane. */
.keepit{position:absolute;top:.5rem;left:.5rem;margin:0;opacity:0;transition:opacity .12s}
/* × on a KEPT card. Same shoulder as the ephemeral ×, deliberately tinted so
it does not read as the same weight of action. */
/* positioned by .kept-actions, not by itself */
.wipe-kept button{font:inherit;line-height:1;cursor:pointer;border:1px solid var(--line);
border-radius:.3rem;padding:.02rem .3rem;background:var(--bg);color:var(--muted)}
.wipe-kept button:hover{background:var(--aus-red,#ff6b6b);color:var(--fg-on-accent,#fff);
border-color:var(--aus-red,#ff6b6b)}
/* keep / release from inside a booth, beside "Wipe now" */
.keep-lg{display:inline-block;margin:0 .4rem 0 0}
.keep-lg button{font:inherit;font-size:.8rem;line-height:1;padding:.32rem .6rem;
cursor:pointer;border:1px solid var(--line);border-radius:.3rem;
background:var(--bg);color:var(--fg)}
.keep-lg button:hover{background:var(--aus-blue);color:var(--fg-on-accent)}
.card:hover .keepit,.keepit:focus-within{opacity:1}
.keepit button{font:inherit;font-size:.9rem;line-height:1;padding:.1rem .34rem;
cursor:pointer;border:1px solid var(--line);border-radius:.3rem;
background:var(--bg);color:var(--fg)}
.keepit button:hover{background:var(--aus-blue);color:var(--fg-on-accent)}
/* ⚠ Blur is COSMETIC. The file is still served at its own URL and still in
the zip. This hides an item from a glance, nothing more. */
.item.blurred img,.item.blurred video,
.item.blurred .doc-body,.item.blurred .textview{filter:blur(22px);transition:filter .15s}
.item.blurred.revealed img,.item.blurred.revealed video,
.item.blurred.revealed .doc-body,.item.blurred.revealed .textview{filter:none}
.item.blurred{position:relative}
.item.blurred .reveal{position:absolute;top:.5rem;left:.5rem;z-index:2;
font:inherit;font-size:.72rem;line-height:1;padding:.24rem .5rem;cursor:pointer;
border:1px solid var(--line);border-radius:.3rem;background:var(--bg);color:var(--fg)}
.item.blurred .reveal:hover{background:var(--aus-blue);color:var(--fg-on-accent)}
/* Legible on purpose. v1 was a bare muted glyph with no border and no label,
and the operator's reaction to it was "no UI option to blur/unblur?" — a
control nobody can find is a control that is not there. */
.blurtoggle{display:inline-block;margin:0}
.blurtoggle button{font:inherit;font-size:.7rem;line-height:1;padding:.2rem .4rem;
cursor:pointer;border:1px solid var(--line);border-radius:.3rem;
background:var(--bg);color:var(--fg);white-space:nowrap}
.blurtoggle button:hover{background:var(--aus-blue);color:var(--fg-on-accent)}
/* In the doc bar it sits beside ⤢ ⬇ ✕ and should not out-shout them. */
.doc-bar .blurtoggle{margin-left:.35rem}
.doc-bar .blurtoggle button{font-size:.66rem;padding:.14rem .34rem}
/* Cover thumbs on the index inherit the blur so the front page cannot undo it. */
.blurred-thumb{filter:blur(16px)}
/* opaque dark control-scrim + always-light glyph — legible over any thumbnail
AND in both themes (glyph must NOT follow --fg-*, which flips dark on light). */
.wipe button{cursor:pointer;border:1px solid rgba(255,255,255,.16);background:rgba(16,18,25,.86);
color:#e9eef0;width:1.9rem;height:1.9rem;border-radius:var(--radius-md);font-size:1.1rem;line-height:1;
backdrop-filter:blur(6px);transition:.14s var(--ease-out)}
.wipe button:hover{border-color:var(--aus-red);color:#fff;background:var(--aus-red)}
/* Asks — a session's multiple-choice question awaiting the operator.
Amber = "needs you" while open (the one colour the page does not otherwise
use for state), green check once answered; the accent is a TOP edge, per
Australis, never a coloured left border. */
.badge-mark{background:var(--aus-bright-yellow);color:var(--fg-on-accent)}
/* U4: the lifetime line's HELD states. Marked rather than styled into
invisibility — the whole safety argument for an unbounded hold is that
a booth which stopped counting down says so where the countdown was. */
.held{color:var(--aus-bright-yellow)}
.held-broken{color:var(--fg-3);text-decoration:underline dotted}
.thumb .badge+.badge-mark{top:2.2rem}
.marks{display:flex;flex-direction:column;gap:.9rem;margin:.2rem 0 1.4rem}
.mark{border:1px solid var(--border-subtle);border-top:2px solid var(--aus-bright-yellow);
border-radius:.5rem;background:var(--rk-panel);overflow:hidden}
.mark.is-answered{border-top-color:var(--aus-bright-green)}
/* Partial: answered SOME questions. Not a failure and not done — blanks are a
legal outcome (operator ruling 2026-09-09), so it gets its own state rather
than being forced into one of the other two. */
.mark.is-partial{border-top-color:var(--aus-bright-blue)}
.mark.is-partial .mark-state{color:var(--aus-bright-blue)}
.mark-part{font-family:var(--font-mono);font-size:.68rem;color:var(--aus-bright-blue);font-weight:700}
.mark-answer-choice.is-skipped{opacity:.55;font-style:italic}
.mark-answer-choice.is-skipped::before{content:"— ";color:var(--fg-3)}
.mark.is-broken{border-top-color:var(--aus-bright-red)}
.mark-head{display:flex;align-items:center;gap:.6rem;padding:.4rem .8rem;
border-bottom:1px solid var(--border-subtle);background:var(--rk-well);
font-family:var(--font-mono);font-size:.68rem;color:var(--fg-3)}
.mark-state{font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:var(--aus-bright-yellow)}
.mark.is-answered .mark-state{color:var(--aus-bright-green)}
.mark.is-broken .mark-state{color:var(--aus-bright-red)}
.mark-when{white-space:nowrap}
/* ---- marks: the per-item controls and the note slots ------------------- */
.mark-state-note{color:var(--aus-bright-cyan,#42dcd1)}
.mark-state-flag{color:var(--aus-bright-green)}
.mark-text,.vnote pre,.item-note pre{margin:.3rem 0;padding:.5rem .7rem;white-space:pre-wrap;
background:var(--bg-2,rgba(128,140,160,.10));border-radius:6px;font-size:.82rem}
.mark-flaglist{margin:.3rem 0 .2rem;padding-left:1.1rem;font-size:.84rem}
.mark-add,.vaddnote,.item-addnote form{display:flex;gap:.5rem;align-items:flex-start;margin:.6rem 0}
.mark-add textarea,.vaddnote textarea,.item-addnote textarea{flex:1;min-width:0}
.mark-x{background:none;border:0;color:var(--fg-3);cursor:pointer;font-size:1rem;line-height:1;padding:0 .3rem}
.mark-x:hover{color:var(--aus-bright-red)}
.mark-undo{margin-left:.4rem}
.flagtoggle button{background:none;border:1px solid var(--border-subtle);border-radius:6px;
padding:.12rem .45rem;font-size:.72rem;color:var(--fg-2);cursor:pointer}
.flagtoggle button:hover{border-color:var(--aus-bright-green);color:var(--aus-bright-green)}
.item.is-flagged{outline:2px solid var(--aus-bright-green);outline-offset:2px}
.item-note{display:flex;align-items:flex-start;gap:.3rem}
.item-addnote summary{cursor:pointer;font-size:.72rem;color:var(--fg-3);padding:.15rem 0}
.vmarks{margin:.6rem auto 0;max-width:min(92vw,900px);display:flex;flex-direction:column;gap:.4rem}
.vflag{display:flex}
.vbtn.is-flagged{color:var(--aus-bright-green);border-color:var(--aus-bright-green)}
.vnote{display:flex;align-items:flex-start;gap:.3rem}
.mark-title{margin:.8rem .9rem -.35rem;font-family:var(--font-mono);font-size:.7rem;
letter-spacing:.08em;text-transform:uppercase;color:var(--fg-3)}
.mark-prompt{margin:.85rem .9rem .5rem;font-size:1.02rem;font-weight:600;color:var(--fg-0);white-space:pre-wrap}
.mark-error{margin:.8rem .9rem;color:var(--aus-bright-red);font-size:.85rem}
.mark-answer{margin:.2rem .9rem .6rem;padding:.55rem .75rem;border:1px solid var(--border-subtle);
border-radius:var(--radius-md);background:rgba(81,224,138,.06)}
.mark-answer-choice{font-weight:600;color:var(--fg-0)}
.mark-answer-choice::before{content:"✓ ";color:var(--aus-bright-green)}
.mark-answer-notes{margin:.4rem 0 0;white-space:pre-wrap;font-family:var(--font-sans);font-size:.86rem;
color:var(--fg-1)}
.mark-answer-file{display:block;margin-top:.35rem;font-family:var(--font-mono);font-size:.68rem;color:var(--fg-3)}
.mark-answer-file a{color:var(--fg-2)}
.mark-formwrap{margin:0 .9rem .8rem}
.mark-change{cursor:pointer;font-family:var(--font-mono);font-size:.7rem;letter-spacing:.06em;
text-transform:uppercase;color:var(--fg-3);list-style:none;user-select:none}
.mark-change::-webkit-details-marker{display:none}
.mark-formwrap[open]>.mark-change{margin-bottom:.4rem}
.mark-formwrap:not([open])>.mark-change{color:var(--aus-bright-cyan)}
.mark-q{border:0;margin:0 0 .7rem;padding:0;min-width:0}
.mark-q:last-of-type{margin-bottom:0}
.mark-q-prompt{padding:0;margin:0 0 .35rem;font-size:.9rem;font-weight:600;color:var(--fg-0)}
.mark-qnotes{margin-top:.35rem;font-size:.82rem}
.mark-answer-q{padding:.3rem 0;border-bottom:1px dashed var(--border-subtle)}
.mark-answer-q:last-of-type{border-bottom:0}
.mark-answer-qprompt{display:block;font-size:.76rem;color:var(--fg-3)}
.mark-options{display:flex;flex-direction:column;gap:.35rem}
.mark-opt{display:flex;align-items:flex-start;gap:.6rem;padding:.5rem .65rem;cursor:pointer;
border:1px solid var(--border-subtle);border-radius:var(--radius-md);background:var(--rk-well);
transition:border-color .12s,background .12s}
.mark-opt:hover{border-color:var(--border-strong)}
.mark-opt:has(input:checked){border-color:var(--aus-bright-cyan);background:rgba(66,220,209,.07)}
.mark-opt input{margin:.2rem 0 0;accent-color:var(--aus-bright-cyan);flex:0 0 auto}
.mark-opt-main{display:flex;flex-direction:column;gap:.1rem;min-width:0}
.mark-opt-label{font-size:.92rem;color:var(--fg-0)}
.mark-opt-detail{font-size:.76rem;color:var(--fg-3);white-space:pre-wrap}
.mark-notes{display:block;width:100%;box-sizing:border-box;margin:.6rem 0 0;padding:.5rem .6rem;
font:inherit;font-size:.88rem;color:var(--fg-0);background:var(--rk-well);
border:1px solid var(--border-subtle);border-radius:var(--radius-md);resize:vertical}
.mark-notes:focus{outline:none;border-color:var(--aus-bright-cyan);box-shadow:var(--glow-cyan)}
.mark-actions{display:flex;justify-content:flex-end;margin-top:.6rem}
.mark-submit{cursor:pointer;font-family:var(--font-mono);font-size:.74rem;letter-spacing:.06em;
padding:.42rem .9rem;border-radius:var(--radius-sm);border:1px solid var(--aus-bright-cyan);
background:var(--aus-bright-cyan);color:var(--fg-on-accent);font-weight:700;transition:.14s var(--ease-out)}
.mark-submit:hover{background:var(--aus-cyan);border-color:var(--aus-cyan)}
/* booth page */
.boothhead{display:flex;align-items:center;gap:1rem;flex-wrap:wrap;
padding-bottom:1rem;margin-bottom:1.4rem;border-bottom:1px solid var(--border-subtle)}
.boothhead .back{font-family:var(--font-mono);font-size:.76rem;letter-spacing:.08em;color:var(--fg-3)}
.boothhead h1{margin:0;font-family:var(--font-display);font-weight:600;font-size:1.5rem;
letter-spacing:-.01em;word-break:break-word;flex:1 1 auto;color:var(--fg-0)}
.boothhead .sub{color:var(--fg-3);font-size:.74rem;font-family:var(--font-mono);letter-spacing:.06em}
/* Its own row under the title, not another chip in the flex line — a `why`
can run to WHY_MAX and would otherwise shove the zip link around. */
.boothhead .prov{flex:0 0 100%;margin-top:-.35rem}
/* The directory name beside a manifest title: quieter than the title, but
never absent — it is what the URL says and what "the third one" refers to. */
.h1-slug{font-family:var(--font-mono);font-size:.62em;font-weight:400;
letter-spacing:.06em;color:var(--fg-3);margin-left:.5rem;white-space:nowrap}
.wipe-lg{position:static}
/* red-outline danger button — legible on the dark canvas, fills on hover */
.wipe-lg button{width:auto;height:auto;padding:.42rem .85rem;border-radius:var(--radius-md);
font-size:.72rem;font-family:var(--font-mono);letter-spacing:.1em;text-transform:uppercase;
background:transparent;border-color:var(--aus-red);color:var(--aus-bright-red)}
.wipe-lg button:hover{background:var(--aus-red);border-color:var(--aus-red);color:#fff}
.gallery{display:grid;gap:1.4rem;grid-template-columns:repeat(auto-fill,minmax(320px,1fr))}
.item{margin:0;background:var(--rk-panel);border:1px solid var(--border-subtle);border-radius:var(--radius-lg);
overflow:hidden;display:flex;flex-direction:column;box-shadow:var(--shadow-2)}
.item img,.item video{width:100%;height:auto;display:block;background:var(--rk-deep)}
.item audio{width:100%;margin:1.3rem .9rem .4rem;max-width:calc(100% - 1.8rem)}
.item .dl{padding:1.4rem .9rem;font-family:var(--font-mono);font-size:.86rem;word-break:break-all}
.item figcaption{padding:.6rem .85rem .75rem;color:var(--fg-2);font-size:.78rem;
font-family:var(--font-mono);letter-spacing:.02em;border-top:1px solid var(--border-subtle);word-break:break-word}
.item-audio figcaption,.item-other figcaption{border-top:none}
/* Inline doc rendering — a .md/.txt/.log shows in place, collapsible and
closable, instead of a link to a separate page. The item spans the full
grid width so prose has a readable measure. */
.item-doc{grid-column:1 / -1}
.item-doc.is-closed{display:none}
.doc-inline{display:block}
.doc-inline > .doc-bar{list-style:none;cursor:pointer;display:flex;align-items:center;gap:.55rem;
padding:.6rem .85rem;font-family:var(--font-mono);font-size:.8rem;color:var(--fg-2);
background:var(--rk-well);border-bottom:1px solid var(--border-subtle);user-select:none}
.doc-inline > .doc-bar::-webkit-details-marker{display:none}
.doc-chevron{color:var(--fg-3);transition:transform .12s ease;font-size:.7rem}
.doc-inline[open] > .doc-bar .doc-chevron{transform:rotate(90deg)}
.doc-name{color:var(--fg-1);word-break:break-all}
.doc-spacer{flex:1}
.doc-act{color:var(--fg-3);text-decoration:none;padding:.1rem .35rem;border-radius:5px;
font-size:.9rem;line-height:1;background:none;border:0;cursor:pointer;font-family:inherit}
.doc-act:hover{color:var(--aus-bright-cyan,#42dcd1);background:var(--rk-deep)}
.doc-close:hover{color:var(--aus-red,#ff6b6b)}
.doc-body{margin:0;border:0;border-radius:0;max-height:32rem;overflow:auto;padding:1rem 1.15rem}
.doc-body.textview{background:var(--rk-panel)}
/* Shared doc typography — used by the inline body above AND the full-page
doc view (doc.html). Kept here so both surfaces render identically. */
.textview{white-space:pre-wrap;word-break:break-word;font-family:var(--font-mono);
font-size:.86rem;line-height:1.5;color:var(--fg-1);background:var(--rk-well);
border:1px solid var(--rk-line,#252a35);border-radius:10px;padding:1rem 1.15rem;overflow-x:auto}
.markdown-body{color:var(--fg-1);line-height:1.62;font-size:.98rem;overflow-wrap:break-word}
.markdown-body h1,.markdown-body h2,.markdown-body h3{line-height:1.25;margin:1.6em 0 .5em}
.markdown-body h1{font-size:1.7em}.markdown-body h2{font-size:1.35em}.markdown-body h3{font-size:1.12em}
.markdown-body h1,.markdown-body h2{border-bottom:1px solid var(--rk-line,#252a35);padding-bottom:.3em}
.markdown-body :first-child{margin-top:0}
.markdown-body p,.markdown-body ul,.markdown-body ol,.markdown-body blockquote{margin:.7em 0}
.markdown-body a{color:var(--aus-bright-cyan,#42dcd1)}
.markdown-body code{font-family:var(--font-mono);font-size:.86em;background:var(--rk-well);
padding:.12em .38em;border-radius:5px}
.markdown-body pre{background:var(--rk-well);border:1px solid var(--rk-line,#252a35);
border-radius:10px;padding:.9rem 1.05rem;overflow-x:auto}
.markdown-body pre code{background:none;padding:0}
.markdown-body blockquote{border-left:3px solid var(--aus-bright-cyan,#42dcd1);
padding-left:1em;color:var(--fg-2);margin-left:0}
.markdown-body table{border-collapse:collapse;display:block;overflow-x:auto}
.markdown-body th,.markdown-body td{border:1px solid var(--rk-line,#252a35);padding:.4em .7em}
.markdown-body img{max-width:100%}
/* U6 — the bench registry, on the standing board's page only. */
.benches{margin:1rem 0;border:1px solid var(--line,#2a2a2a);border-radius:6px;overflow:hidden}
.bench-head{display:flex;gap:.6rem;align-items:baseline;padding:.5rem .7rem;background:rgba(255,255,255,.03)}
.bench-title{font-weight:600}
.bench-note,.bench-empty{opacity:.6;font-size:.85em}
.bench-empty{padding:.6rem .7rem}
.bench-err{padding:.6rem .7rem;color:#f2b8b5;background:rgba(242,184,181,.08)}
.bench-row{display:flex;gap:.6rem;align-items:center;padding:.45rem .7rem;border-top:1px solid var(--line,#2a2a2a)}
.bench-row.is-retired{opacity:.5}
.bench-state{font-size:.7em;text-transform:uppercase;letter-spacing:.06em;padding:.1rem .4rem;border-radius:3px;background:rgba(255,255,255,.08)}
.bench-row.is-live .bench-state{background:rgba(120,200,140,.18)}
.bench-row.is-promoted .bench-state{background:rgba(130,170,240,.18)}
.bench-main{flex:1;min-width:0}
.bench-url{font-size:.78em;opacity:.55;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.bench-meta{display:flex;flex-direction:column;align-items:flex-end;font-size:.75em;opacity:.6}
.bench-acts{display:flex;gap:.3rem}
.bench-to,.bench-rm{font-size:.75em;padding:.15rem .4rem;cursor:pointer}
.bench-add{display:flex;gap:.4rem;padding:.5rem .7rem;border-top:1px solid var(--line,#2a2a2a)}
.bench-add input[type=url]{flex:2;min-width:0}
.bench-add input[type=text]{flex:1;min-width:0}
/* A board row whose booth has been swept. Marked, never auto-removed. */
.board-row.board-dead{opacity:.45}
.board-dead-tag{font-size:.9em;color:#f2b8b5;opacity:.9}
/* U7 — the rail, and the grid cursor. */
.rail{position:sticky;top:0;z-index:5;display:flex;gap:.5rem;align-items:baseline;
padding:.4rem .6rem;margin:.6rem 0;background:var(--bg,#111);
border-bottom:1px solid var(--line,#2a2a2a);flex-wrap:wrap}
.rail-total{font-weight:600}
.rail-f{font-size:.85em;padding:.1rem .45rem;border-radius:3px;text-decoration:none;
opacity:.65;border:1px solid transparent}
.rail-f:hover{opacity:1}
.rail-f.on{opacity:1;border-color:var(--line,#2a2a2a);background:rgba(255,255,255,.06)}
/* The group row. Wraps rather than scrolls: 16 groups is the live maximum
and a horizontal scroller hides half of them behind a gesture. */
.rail-groups{display:flex;gap:.35rem;flex-wrap:wrap;align-items:baseline;
padding-left:.5rem;margin-left:.25rem;border-left:1px solid var(--line,#2a2a2a)}
.rail-g{font-size:.8em;padding:.1rem .4rem;border-radius:3px;text-decoration:none;
opacity:.6;border:1px solid transparent}
.rail-g:hover{opacity:1;border-color:var(--line,#2a2a2a)}
/* The jumped-to tile, so a fragment jump says where it landed. */
figure.item:target{outline:2px dashed #7aa2f7;outline-offset:3px}
figure.item.is-cursor{outline:2px solid #7aa2f7;outline-offset:2px}
</style>
</head>
<body>
<header class="topbar">
<a class="brand" href="/"><span class="dot"></span><span class="name">The&nbsp;Booth</span></a>
<span class="tagline">ephemeral media · auto-wipes {{ ttl_hours }}h · kept boards don't</span>
</header>
<main>{% block content %}{% endblock %}</main>
<footer class="foot">
drop a folder into <code>{{ data_dir }}</code>{% if host %} · {{ host }}{% endif %}
</footer>
</body>
</html>