:root {
  color-scheme: light dark;
  --color-background: #f7f6f3;
  --color-surface: #ffffff;
  --color-text: #25221d;
  --color-muted: #6f6a60;
  --color-accent: #2e6e4e;
  --color-accent-soft: #e3efe8;
  --color-on-accent: #ffffff;
  --color-danger: #a33b3b;
  --color-danger-soft: #f7e3e3;
  --color-on-danger: #ffffff;
  --color-warning: #8a6a1f;
  --color-warning-soft: #f7efdb;
  --color-border: #ddd8cf;
  /* Podium medals. The showcase marks 1st/2nd/3rd with these rather than
     flooding the card — a metal edge reads as triumphant; a wash of colour
     reads as a highlighted table row. */
  --color-gold: #a8790c;
  --color-silver: #71787f;
  --color-bronze: #97572a;
  /* Steam's store-art shape, the standard for every game cover on the
     showcase (see .cover-frame). */
  --cover-aspect: 460 / 215;
  /* The shape every play-proof screenshot is shown in, site-wide (see
     .screenshot-frame). Judges upload whatever their capture produced — a
     whole screen, a dragged rectangle, a phone photo of a monitor — so almost
     none of them match this exactly, and none are ever cropped to it. */
  --screenshot-aspect: 16 / 9;
  /* The gap between a page's hero — the festival's banner art, or the This Guy
     Is Marbles strip — and the page title under it. One knob because both
     heroes want the same answer, and the two rules that use it sit a thousand
     lines apart. Collapses with the title's own top margin, so the number here
     is the gap you actually get. */
  --hero-gap: 2.4rem;
}

@media (prefers-color-scheme: dark) {
  :root {
    --color-background: #1b1a17;
    --color-surface: #26241f;
    --color-text: #e8e4db;
    --color-muted: #a59e90;
    --color-accent: #74c39a;
    --color-accent-soft: #243a2d;
    --color-on-accent: #14241c;
    --color-danger: #e08c8c;
    --color-danger-soft: #3c2525;
    --color-on-danger: #241414;
    --color-warning: #d9b96a;
    --color-warning-soft: #3a3222;
    --color-border: #3c382f;
    --color-gold: #e0bd63;
    --color-silver: #c0c7d0;
    --color-bronze: #cf8f5e;
  }
}

* { box-sizing: border-box; }

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  background: var(--color-background);
  color: var(--color-text);
  line-height: 1.55;
}

.site-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 0.8rem 1.5rem;
  background: var(--color-surface);
  border-bottom: 1px solid var(--color-border);
}

.site-title {
  font-weight: 700;
  text-decoration: none;
  color: var(--color-accent);
  display: inline-flex;
  align-items: center;
  gap: 0.55rem;
}
/* The TGIM mark beside the title: the favicon's symbol on a small white tile
   (deliberately white in both themes, like a browser tab) so the black
   marbles never sink into the dark header. 48px source at 24px = retina. */
.site-mark {
  width: 24px;
  height: 24px;
  padding: 2px;
  background: #ffffff;
  border-radius: 6px;
  box-sizing: content-box;
}

nav { display: flex; align-items: center; gap: 1rem; }
nav a { color: var(--color-text); text-decoration: none; }
nav a:hover { color: var(--color-accent); }
.account-link { color: var(--color-muted); }
.role-badge {
  background: var(--color-accent-soft);
  color: var(--color-accent);
  border-radius: 999px;
  padding: 0.1rem 0.6rem;
  font-size: 0.8rem;
}
/* Site-wide standing (e.g. "admin"): a solid accent fill so it reads as a
   chip — not a link — and adapts to light/dark, unlike the soft per-event badge. */
.role-badge--site {
  background: var(--color-accent);
  color: var(--color-on-accent);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
/* An organizer's badge is a shortcut to their dashboard — hint that it's live. */
.role-badge--link { text-decoration: none; }
.role-badge--link:hover { background: var(--color-accent); color: var(--color-on-accent); }

/* The registration ladder's step rail (entries/_entry_steps.html): a read-only
   map of enter → verify → pay. Done rungs get an accent check, the current
   rung a solid accent badge (echoing .role-badge--site), upcoming rungs stay
   muted. Rungs are text, never links — the page's action card does the acting. */
.entry-steps {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem 1.4rem;
  margin: 0.75rem 0 1rem;
  padding: 0;
}
.entry-step {
  display: flex;
  align-items: center;
  gap: 0.45rem;
  color: var(--color-muted);
  font-size: 0.9rem;
}
.entry-step-marker {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.4rem;
  height: 1.4rem;
  border-radius: 50%;
  border: 1px solid var(--color-border);
  font-size: 0.8rem;
  flex-shrink: 0;
}
.entry-step--done { color: var(--color-text); }
.entry-step--done .entry-step-marker {
  border-color: var(--color-accent);
  background: var(--color-accent-soft);
  color: var(--color-accent);
}
.entry-step--current { color: var(--color-text); font-weight: 600; }
.entry-step--current .entry-step-marker {
  border-color: var(--color-accent);
  background: var(--color-accent);
  color: var(--color-on-accent);
  font-weight: 600;
}

/* A message box shrunk to live inside a form field's <p>, right under its
   input (e.g. the Game URL's "no Play in browser build" warning). Must be a
   span (block-displayed) for valid nesting — and [hidden] has to keep winning
   over that display:block. */
.field-warning {
  display: block;
  margin: 0.4rem 0 0;
  font-size: 0.9rem;
}
.field-warning[hidden] { display: none; }

/* Breadcrumb trail: one persistent location path under the header. Each page
   fills the {% block breadcrumbs %}; pages that don't render an empty bar that
   :empty hides. Separators are drawn in CSS so templates just list crumbs. */
.breadcrumbs {
  max-width: 920px;
  margin: 0 auto;
  padding: 0.7rem 1.5rem 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  font-size: 0.85rem;
}
.breadcrumbs:empty { display: none; }
.breadcrumbs a { color: var(--color-muted); text-decoration: none; }
.breadcrumbs a:hover { color: var(--color-accent); }
.breadcrumbs > :not(:first-child)::before {
  content: '›';
  margin: 0 0.5rem;
  color: var(--color-border);
}
.breadcrumbs [aria-current="page"] { color: var(--color-text); font-weight: 600; }

.site-footer {
  max-width: 920px;
  margin: 3rem auto 0;
  padding: 1.2rem 1.5rem;
  border-top: 1px solid var(--color-border);
  text-align: center;
}

main { max-width: 920px; margin: 0 auto; padding: 1.5rem; }

/* Section rhythm: generous air before each new top-level section so headings
   like "Thanks for Registering!" stand apart from surrounding text. Scoped to
   main's direct children so headings inside cards keep their tight spacing. */
main > h1 { margin-bottom: 1.4rem; }
/* The festival's deck (templates/public/event_detail.html): the draw in one
   sentence, a size up from the body and a shade heavier, in the text colour,
   so it reads as the line that opens the story rather than as a heading or a
   caption. It follows the facts line under the title, with the facts line's
   full air between them so it groups with the story it opens rather than with
   the price and dates (Christian, 2026-09-17); in the phases that have no
   facts line it tucks up under the title the way the facts line does. The
   title stays the loudest thing on the page. */
.festival-deck {
  margin: 0 0 1.1rem;
  font-size: 1.3rem;
  font-weight: 500;
  line-height: 1.35;
  color: var(--color-text);
}
h1 + .festival-deck { margin-top: -0.9rem; }
.festival-deck .fineprint { margin-left: 0.4rem; }
/* The festival hero banner (1920×622 — Steam's event-header canvas, plan
   5.6): full content width above the title, aspect preserved. */
.festival-banner {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 10px;
  margin-bottom: var(--hero-gap);
}
/* The festival page's key-facts line, tucked under the title (plan 5.7): the
   two numbers every prospect needs — the entry fee (never hidden) and the
   registration deadline. Words stay muted; the numbers carry the weight. */
.event-facts {
  margin: -1rem 0 1.6rem;
  color: var(--color-muted);
}
.event-facts strong { color: var(--color-text); }
.event-facts-sep { margin: 0 0.45rem; }
/* A qualifier in a smaller voice beside the claim it qualifies — "+ tax/VAT"
   after the fee, "pending Steam approval" after the deck's promise — read in
   the same glance as the claim, so nobody hunts for a footnote. */
.fineprint { font-size: 0.85rem; font-weight: 400; color: var(--color-muted); }
main > h2 { margin-top: 2.4rem; margin-bottom: 0.9rem; }
main > h3 { margin-top: 1.9rem; }
/* A message box standing alone as a page-level section (e.g. the red "Verify
   your email!" card) follows the card rhythm, not the tight flash-banner one. */
main > .message { margin-bottom: 1.4rem; }

.inline-form { display: inline; }
.hidden { display: none; }
.link-button {
  background: none; border: none; padding: 0; cursor: pointer;
  color: var(--color-text); font: inherit;
}
.link-button:hover { color: var(--color-accent); }

.messages { max-width: 920px; margin: 1rem auto 0; padding: 0 1.5rem; }
/* The container is always in the page so scripts can raise a message into it;
   with nothing in it, it must take up no room. */
.messages:empty { display: none; }
.message { padding: 0.6rem 1rem; border-radius: 6px; margin-bottom: 0.5rem; }
/* A heading leading a message box (e.g. "Verify your email!"). */
.message h3 { margin: 0.25rem 0 0.5rem; }
/* Quiet persistent nudge for logged-in accounts that never verified — muted on
   purpose; the urgent red treatment stays on event pages where an entry is at
   stake. */
.verify-nag {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  color: var(--color-muted);
}
.verify-nag .link-button { text-decoration: underline; }
.message-success { background: var(--color-accent-soft); color: var(--color-accent); }
.message-error { background: var(--color-danger-soft); color: var(--color-danger); }
.message-warning { background: var(--color-warning-soft); color: var(--color-warning); }
/* Neutral on purpose, and the only one of these that is. The three above are
   news — something went well, something went wrong, something needs care — and
   each borrows the colour that already means that across the site. An info
   message is none of those: it tells somebody where they already stand, like
   being registered for a festival already, or a game already withdrawn. So it
   takes the quiet treatment instead, the surface with a border, which is what
   this site has always used for a line that is not an event. Without a rule of
   its own it would fall through to the bare box below, whose padding and
   rounded corners have nothing to sit on. */
.message-info {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  color: var(--color-muted);
}
/* The organizer's preview bar on a festival page (public/views.py, PREVIEWS):
   an info message with room below it. A control drawn inert on a preview
   keeps the real button's look — the point of the preview is what the page
   will look like, and the bar says the buttons do nothing — minus the hand
   cursor. */
.preview-bar { margin-bottom: 1.2rem; }
.preview-bar-back { display: block; margin-top: 0.4rem; }
[data-preview-control] { cursor: default; }
/* The edit page's preview buttons (templates/events/event_edit.html): a row at
   the top right of the form, above its first field, wrapping onto more lines
   when the width runs out. */
.form-previews { display: flex; justify-content: flex-end; flex-wrap: wrap; gap: 0.6rem; margin: 0 0 0.6rem; }

.card {
  /* Named because something else has to line up with it: the account page's
     Edit control is lifted to the card's top right corner, and that offset IS
     this padding. Two copies of the same number would drift. */
  --card-padding-y: 1rem;
  --card-padding-x: 1.25rem;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 8px;
  padding: var(--card-padding-y) var(--card-padding-x);
  margin-bottom: 1.4rem;
}

.card h3 { margin-top: 0; }

/* The festival form's sections, one per phase that requires the fields in
   it (templates/events/_event_form_fields.html): a fieldset for the
   grouping the browser and a screen reader both understand, dressed as a
   plain run of rows under a heading. */
.form-section { border: 0; padding: 0; margin: 2.2rem 0 0; min-width: 0; }
/* A step up from the field labels, so the sections read as sections. */
.form-section > legend { padding: 0; margin-bottom: 1rem; font-size: 1.35rem; font-weight: 700; }

/* Festival explainer panel beneath the register action. */
.festival-info { margin-top: 1.5rem; }
/* The pitch's closing ask (the CTA sandwich's bottom slice): the page's
   crescendo gets air on both sides — a cramped call-to-action reads as an
   afterthought. */
.register-cta { margin: 2.2rem 0 2.6rem; }
.festival-info h2 { margin-top: 1.75rem; font-size: 1.3rem; }
.festival-info h2:first-child { margin-top: 0; }
.festival-info blockquote {
  margin: 0.75rem 0;
  padding: 0.4rem 1rem;
  border-left: 3px solid var(--color-border);
  color: var(--color-muted);
  font-style: italic;
}
/* The FAQ, in sections (templates/public/_festival_faq.html). The hierarchy
   runs heading → short answer → questions: the heading and its one muted
   sentence are the strong elements and what most readers need, and the
   questions under them are a quiet list — body size and weight, no rule
   between them, a "+" at the right edge in place of the disclosure triangle,
   which becomes "−" once open. Twenty bold questions each on a ruled row of
   their own read as a wall; twenty plain lines read as a list. One hairline
   per section, where sections meet, rather than one per question. */
.faq-section + .faq-section { margin-top: 1.6rem; padding-top: 1.6rem; border-top: 1px solid var(--color-border); }
/* The heading and its sentence stop where the questions' text does: the
   marker column at the right is clear the whole way down the card. */
.faq-section h3 { margin: 0 1.75rem 0.25rem 0; font-size: 1.2rem; font-weight: 600; }
.faq-lead { margin: 0 1.75rem 0.9rem 0; color: var(--color-muted); }
.festival-info details { margin: 0; }
.festival-info summary {
  display: block;            /* over a summary's own list-item display */
  position: relative;        /* the "+" sits in the padding at the right edge */
  padding: 0.45rem 1.75rem 0.45rem 0;
  cursor: pointer;
  list-style: none;          /* the "+" below stands in for the triangle */
}
.festival-info summary::-webkit-details-marker { display: none; }
/* Absolutely placed rather than laid out beside the text as a flex item: a
   question is inline text, an <em> and all, and a flex row would space its
   pieces apart. */
.festival-info summary::after {
  content: "+";
  position: absolute;
  right: 0;
  top: 0.4rem;
  color: var(--color-muted);
  font-size: 1.2rem;
  line-height: 1;
}
.festival-info details[open] summary::after { content: "−"; }
.festival-info summary:hover { color: var(--color-accent); }
.festival-info details[open] summary { font-weight: 600; margin-bottom: 0.25rem; }
/* An open answer hangs under its question, indented, so where the answer
   ends and the next question begins is a return to the left margin rather
   than a guess. The question itself stays flush with its neighbours. On the
   right the answer stops where the question's text does — short of the
   marker column — so nothing runs on past the "−". */
.festival-info details[open] { margin-bottom: 1.25rem; }
.festival-info details > :not(summary) { margin-left: 1.25rem; margin-right: 1.75rem; }
.festival-info details > :last-child { margin-bottom: 0; }

/* The festival's pitch (templates/public/_festival_pitch.html): the deal on a
   card of its own, told apart from the festival's story above it. Its section
   labels wear an editorial kicker's dress — small, spaced, muted capitals —
   so the pitch reads as sections of the page rather than as a document. */
.pitch { margin-top: 1.6rem; padding-top: 1.25rem; padding-bottom: 1.35rem; }
.pitch-label {
  margin: 0 0 0.8rem;
  font-size: 0.8rem;
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--color-muted);
}
.offer + .pitch-label { margin-top: 2rem; }
/* The offer leads with its figures: each sentence's first word, a number, set
   large in a column of its own, right-aligned so the sentences line up beside
   it, and the top of the number on the top of the sentence's first line, the
   way a chapter's initial sits (Christian's pick, 2026-09-16). The column
   fits the widest figure; a wider one would overflow it and push its sentence
   out of line. The nudge equalises the air above the two glyphs, measured on
   the Mac system font; other systems' fonts may sit a pixel off. */
.offer { list-style: none; margin: 0; padding: 0; }
.offer > li { display: grid; grid-template-columns: 4.6rem minmax(0, 1fr); column-gap: 0.8rem; align-items: start; }
.offer > li + li { margin-top: 0.9rem; }
.offer-figure {
  font-size: 2.1rem;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.02em;
  color: var(--color-accent);
  text-align: right;
  margin-top: -2px;
}
/* The prize named inside a reward (<strong> in the template) wears the
   figure's accent, a touch larger than its sentence, so it is noticed while
   still reading as running text — no fill, no underline (Christian,
   2026-09-17: a chip or highlighter stops reading as prose). Heavier than the
   figure because small strokes are mostly blended edge: at the heaviest
   weight the letters hold enough solid green to read as the number's colour
   by eye. */
.offer strong { color: var(--color-accent); font-weight: 900; font-size: 1.08em; }
/* The offer ranks its prizes, the best first (Christian, 2026-09-17): the
   lead row is set a size up, figure and sentence together, so it reads as
   the card's headline and the rows beneath it as the supporting lines, with
   a beat more air under it to mark the step down. The figure's nudge is
   eyeballed for this size the way .offer-figure's was measured. The sentence
   grows less than the figure: it carries the prize and its condition in one
   line, and at full content width a size larger would wrap it. */
.offer > .offer-lead { margin-bottom: 1.15rem; }
.offer > .offer-lead > .offer-figure { font-size: 2.6rem; margin-top: -3px; }
.offer > .offer-lead > span { font-size: 1.1em; }
/* The steps: a circled check beside each one — "yep, done" (Christian,
   2026-09-16), in place of a step number. */
.steps { list-style: none; margin: 0; padding: 0; }
.steps > li { display: grid; grid-template-columns: 1.75rem minmax(0, 1fr); column-gap: 0.85rem; align-items: start; }
.steps > li + li { margin-top: 0.75rem; }
.steps > li::before {
  content: '\2713';
  width: 1.75rem;
  height: 1.75rem;
  border-radius: 50%;
  background: var(--color-accent-soft);
  color: var(--color-accent);
  font-size: 0.9rem;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
  margin-top: 0.05rem;
}
/* Heading on the left, an action button on the right. */
.card-header { display: flex; justify-content: space-between; align-items: center; gap: 0.6rem; }
.card-header h2 { margin: 0; }

/* The Edit control on an account field.

   In the markup it sits after the value, beside the form it opens, because it
   is that form's summary and a summary has to live inside the element it
   discloses. It cannot be written into the heading row, so it is lifted there:
   the heading is the card's first row, which makes the card's own padding the
   entire offset.

   Out of flow, so a closed editor takes up no height and the value below the
   heading sits exactly where it did when this was a button in the header. */
/* The title and the note belong to the popup. A modal has to name itself, and
   the line saying what the field is for cannot be read through a backdrop.
   Disclosed in the card instead, both are already on screen above the form —
   the card's own heading, and its own copy of the same note.

   The note names .helptext as well as itself, because that is what it IS — the
   same class the card's copy and the signup card's lines use — and .helptext
   sets display further down this file. A single class would lose to it and the
   note would show in both places at once. */
.editor-title { display: none; }
.helptext.editor-note { display: none; }
dialog .editor-title { display: block; margin-top: 0; }
dialog .helptext.editor-note { display: block; }
.editor-actions { display: flex; align-items: center; gap: 0.6rem; }
/* A WIDTH, not a maximum. A question fills the popup because it is a sentence;
   a form is a stack of short boxes, so left to size itself this shrank to about
   260px and the boxes with it. Same number the questions reach, so the site has
   one popup width rather than two. */
.editor-dialog { width: min(440px, calc(100vw - 2rem)); }

.account-field { position: relative; }
.account-edit {
  position: absolute;
  top: var(--card-padding-y);
  right: var(--card-padding-x);
  display: inline-block;   /* over a summary's own list-item display */
  list-style: none;        /* no disclosure triangle — it is wearing a button */
}
.account-edit::-webkit-details-marker { display: none; }
/* EVERY control that opens a form goes while any of them is open — not just
   the open one's, and not only the four that say Edit. Deleting an account is
   asked for by a control of its own, in the flow rather than in the corner,
   and it opens a form like the rest.
   Whichever of them is pressed, the result is a form folded away rather than
   cancelled: it keeps what was typed and hands it back next time, which is the
   one thing Cancel exists to prevent (Christian, 2026-09-03, on the open form's
   own control, then on the others: "Is this just the Cancel bug yet again?").
   So while a form is open the only ways out of it are Save and Cancel.

   That is also what the scripted page does, without anyone arranging it: the
   form is a modal there, and a modal blocks the page behind it. Reaching
   another field means finishing with this one either way.

   The shared `name` on the elements themselves does the same job a second way,
   by making the browser close one as another opens. Two independent mechanisms
   on purpose: this rule needs :has() and that one needs exclusive disclosures,
   so a browser missing either is still left with one open form at a time. */
body:has(.account-editor[open]) .form-opener { display: none; }

/* A code card standing in the page is an open form too, so the same thing is
   true of it: a change is in flight, and starting to edit another field would
   leave it behind (Christian, 2026-09-04, "those Edit buttons should also still
   be off"). Its way out is the Cancel beside Change my address.

   The no-scripting road only. With scripting the card is a popup, which blocks
   the page behind it without help — and the card is never `hidden` there, since
   the dialog's own closed-ness is what hides it, so this test would hold
   forever. */
html:not(.js-ready):has([data-code-card]:not([hidden])) .form-opener { display: none; }
/* And while a question the server asked is standing on the page. That state
   does not look open to the rule above: proving the password re-renders the
   page, so every disclosure is closed and the question stands on its own —
   which brought all five controls back underneath a question still waiting on
   an answer (Christian, 2026-09-04, who expected this to follow from the rest).
   With scripting the question is a modal and blocks the page by itself. */
html:not(.js-ready):has(.confirm-step) .form-opener { display: none; }



/* Links. Against a warm dark palette with a green accent the browser's default
   blue — and its purple once visited — reads as a page nobody finished, so
   every link takes the accent instead. Which ones you have already opened is
   not worth colouring on a site this size, and a page of half-purple,
   half-green links looks broken rather than informative.

   THERE IS DELIBERATELY NO `a:visited` RULE HERE, and its absence is
   load-bearing. The rule below already covers visited links: the browser's
   purple lives in the user-agent stylesheet, and the cascade settles ORIGIN
   before specificity, so any author rule beats it however unspecific. Adding
   `a:visited` back buys nothing and costs a great deal — at (0,1,1) it outranks
   anything styled by a single class, so a link dressed as a button takes the
   accent as its TEXT on top of the accent as its BACKGROUND, and renders as a
   solid pill with nothing written on it. Guarded by a test, because the failure
   is invisible to the author: browsers report the unvisited style to
   getComputedStyle and to devtools, so the bug shows only to somebody who has
   been to the page the link points at. */
a { color: var(--color-accent); text-underline-offset: 0.15em; }

.muted { color: var(--color-muted); }
.small { font-size: 0.85rem; }
/* Holds a run of words on one line. For a phrase that reads as a single
   offer — a control and the punctuation belonging to it — where a line break
   through the middle would leave the tail looking like a fragment. Keep what
   it wraps short: a run this long cannot break has nowhere to go but out past
   its container. */
.keep-whole { white-space: nowrap; }

button, .button {
  background: var(--color-accent);
  color: var(--color-on-accent);
  border: none;
  border-radius: 6px;
  padding: 0.5rem 1.1rem;
  font-size: 1rem;
  line-height: 1.2;  /* pin it so <a> and <button> buttons match height */
  cursor: pointer;
  text-decoration: none;
  display: inline-block;
}
/* `hidden` has to keep meaning hidden, for EVERYTHING. Any author style that
   sets display — the button rule above, a flex row, a grid — beats the
   browser's own [hidden] { display: none } no matter how specific it is, so an
   element marked hidden stays on screen. This was first hit with buttons and
   fixed for buttons; the second time was a flex row of dialog actions, which is
   how it earned a rule that covers the class rather than the case. The
   !important is the point: it has to outrank whatever set display. */
[hidden] { display: none !important; }
button:hover, .button:hover { filter: brightness(1.1); }
button:disabled, button[disabled] {
  background: var(--color-border);
  color: var(--color-muted);
  cursor: not-allowed;
}
button:disabled:hover, button[disabled]:hover { filter: none; }
/* What a busy button is waiting on, beside it rather than on it (see
   setUpBusyButton in form_validation.js). It sits empty until a submit is
   really going, and an empty one must take up no room at all — margin applies
   to an empty inline box too, which would shift the button's neighbours by a
   space that never showed anything. */
.busy-note {
  color: var(--color-accent);
  font-size: 0.85rem;
}
.busy-note:not(:empty) { margin-left: 0.6rem; }
/* The answer to pressing "send yourself a new one", beside that link rather
   than under the code boxes — those belong to a different act.
   Size comes from `small`, which is what the quiet line above it already uses
   and what the dialog sizes its own quiet text by; setting one here would only
   be a second opinion for the same rule to outrank. Colour is this line's own
   business: it is the only thing in that corner that can be an answer. */
/* Holds its room whether or not it is saying anything, exactly as the message
   row above the boxes does and for the same reason: this dialog is CENTRED, so
   a card that grows pushes everything above the new line upward — the boxes
   move under the cursor of somebody mid-type, and the link they just pressed
   slides away as its own answer arrives.
   ONE line's worth, because every sentence that can land here is now written
   to fit one — see verification.SAID, where they are held to about 30
   characters for this. It was two: the longest of them ran to two lines even
   on a full-width card, so the reserve was sized for the rarest message on the
   card and the empty half showed under every common one (Christian, 2026-09-02:
   "That is a *lot* of space"). Measured, not guessed: 22.3px a line at this
   size, so 1.55em is one of them.

   The constraint this puts on copy is real and belongs with the copy: shorten
   a sentence past one line here and the card starts jumping again. Anything
   added to this slot later has the same 30-character budget. */
.resend-status {
  color: var(--color-accent);
  margin: 0.4rem 0 0;
  min-height: 1.55em;
}
.resend-status.is-wrong { color: var(--color-danger); }
/* This slot used to be hidden without scripting, because nothing ever filled it
   there: that road answered with a flash at the top of the page, so the room it
   held would have been a gap kept for a sentence that could not arrive.

   The server fills it now, on exactly that road, so the hiding is gone. Hiding
   it only while empty was tried in between and was worse than either: it moved
   the resizing problem to the road with no script, where the line appeared and
   pushed the page down at the moment the answer landed. One slot, one reserved
   line, both roads. */
button.secondary, .button.secondary {
  background: var(--color-surface);
  color: var(--color-accent);
  border: 1px solid var(--color-accent);
}
/* Destructive buttons (e.g. Delete): red filled, or red outline when secondary.
   Filled needs a partner colour for its TEXT, exactly as the primary button
   does — white on the light theme's deep red, near-black on the dark theme's
   pale one. A background that flips with the theme cannot have its foreground
   pinned to one value and stay readable in both. */
button.danger, .button.danger {
  background: var(--color-danger);
  color: var(--color-on-danger);
  border: none;
}
button.secondary.danger, .button.secondary.danger {
  background: var(--color-surface);
  color: var(--color-danger);
  border: 1px solid var(--color-danger);
}

input[type="text"], input[type="url"], input[type="email"], input[type="password"],
input[type="number"], input[type="date"], textarea, select {
  padding: 0.5rem;
  border: 1px solid var(--color-border);
  border-radius: 6px;
  font: inherit;
  background: var(--color-surface);
}
input::placeholder, textarea::placeholder { color: var(--color-muted); opacity: 1; }

/* Show/hide (eye) toggle injected onto every password input by base.html. */
.password-wrapper { position: relative; display: block; }
.password-wrapper input { padding-right: 2.4rem; }
.password-toggle {
  position: absolute; right: 0.6rem; top: 50%; transform: translateY(-50%);
  background: none; border: none; padding: 0; cursor: pointer;
  color: var(--color-muted); display: flex; align-items: center;
}
.password-toggle:hover { color: var(--color-accent); }
/* Free-text fields fill the width; selects and numbers stay compact so their
   arrows/spinners sit near the (short) content. */
input[type="text"], input[type="url"], input[type="email"], input[type="password"],
textarea {
  width: 100%;
}
input[type="number"] { width: 8rem; max-width: 100%; }
/* Date fields hold a fixed "2026-07-11" (~85px in our 16px system font); with
   the 0.5rem padding + border that's ~103px, so 7.5rem sits snug with just room
   for the caret. The class (not [type=date]) is what survives flatpickr swapping
   the input to type=text, which would otherwise catch the full-width rule above. */
input.date-field { width: 7.5rem; max-width: 100%; }
/* Drop the OS-native dropdown control (otherwise the browser ignores our
   border/background and draws its own) and supply a matching chevron. */
select {
  width: 16rem;
  max-width: 100%;
  -webkit-appearance: none;
  appearance: none;
  padding-right: 2rem;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath d='M2 4l4 4 4-4' fill='none' stroke='%23a0a0a0' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 0.65rem center;
  background-size: 0.7rem;
}

/* Tighter than it looks, because each row now carries a reserved status line
   of its own (.field-status): the gap between one field and the next is mostly
   that line rather than margin, so the whitespace has moved to where a message
   will appear rather than being added to it. */
.form-field { margin: 0.75rem 0; }
/* Section headings inside a long form ("About you" / "Your game"): clearly more
   air above than any field-to-field gap, a hairline rule (the festival-info FAQ's
   separator pattern), and a size that clearly outranks the 1.1rem field labels
   while staying under the card's h2 title. Placed after `.card h3
   { margin-top: 0 }`, which would otherwise flatten them. */
h3.form-section {
  margin-top: 2.5rem;
  padding-top: 1.5rem;
  border-top: 1px solid var(--color-border);
  font-size: 1.4rem;
}
/* The form's first section has nothing above it but the card intro, so a
   separator there is clutter — drop the hairline and most of the air, mirroring
   the login card's clean heading-then-fields flow. (Works whichever section
   comes first: signup-less visitors start straight at "Your game".) */
form h3.form-section:first-of-type {
  border-top: none;
  margin-top: 1.5rem;
  padding-top: 0;
}
/* A submit button paired with a Cancel link, sitting on one row. */
.form-actions { display: flex; align-items: center; gap: 0.6rem; }
label { font-weight: 600; display: block; margin-bottom: 0.3rem; font-size: 1.1rem; }
/* A field's name on the left, its length bounds on the right of the same row.
   baseline so the two sit on one line however they differ in size; wrap so a
   long label pushes the note down rather than crushing it. */
.field-heading { display: flex; align-items: baseline; justify-content: space-between;
                 gap: 0.75rem; flex-wrap: wrap; }
/* No margin-left:auto — space-between alone does the right thing at both
   widths. Wide, the note sits right of the label; narrow, it wraps to a line of
   its own and starts at the left, which Christian found much the better of the
   two on a phone (2026-08-17): it gets whitespace above and below, and it reads
   BEFORE the help text rather than beside it, so it is noticed on its own
   terms. Forced right on a narrow screen just looked strange. */
.field-limit { color: var(--color-muted); font-size: 0.8rem; white-space: nowrap;
               margin-bottom: 0.3rem; }
/* Required fields (Django adds .required to the label) get a trailing star. */
label.required::after { content: ' *'; color: var(--color-danger); }
/* Field descriptions read as subordinate to the larger field title. */
.helptext { display: block; color: var(--color-muted); font-size: 0.85rem; margin-bottom: 0.3rem; }
/* Numeric scale: a slider flanked by its end labels, with the live value
   tracking the knob underneath. The min label hugs the slider on the right,
   the max label on the left. */
.scale-field { display: block; }
.scale-row { display: flex; align-items: center; gap: 0.7rem; padding-bottom: 1.4rem; }
.scale-end { font-weight: 600; }
.scale-end--min { text-align: right; }
.scale-end--max { text-align: left; }
.scale-slider { flex: 1; position: relative; }
.scale-output {
  position: absolute; top: 100%; transform: translateX(-50%);
  margin-top: 0.25rem; font-weight: 600; color: var(--color-text); white-space: nowrap;
}

/* Radio buttons. Django wraps each choice in its own <div> inside the group
   (it was a <ul> of <li> long ago, and these rules still said so until
   2026-08-11 — they had quietly stopped matching anything, which is why the
   choices were rendering in the page's heavy label weight).

   A choice here can be a whole sentence — the review-consent question's are —
   so the button sits at the top of its own column and the text wraps beside it
   rather than under it. The whole label is clickable, which for wordy choices
   is most of the target. Regular weight and body size: these are sentences to
   read, not headings. */
.radio-group { margin: 0.4rem 0 0; }
.radio-group > div + div { margin-top: 0.5rem; }
.radio-group label {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.6rem;
  align-items: start;
  font-weight: 400;
  font-size: 1rem;
  cursor: pointer;
  line-height: 1.45;
}
.radio-group input[type="radio"] { margin-top: 0.25rem; width: auto; }
/* The small print half of a two-part choice (the review-consent question's).
   Its own line under the decision, and muted: the eye takes the answer first
   and what it commits you to second. Block rather than a second grid item, so
   it stays in the text column and keeps its left edge with the decision above
   it rather than sliding under the button. */
.choice-note { display: block; color: var(--color-muted); margin-bottom: 0.45rem; }

/* Custom slider: we draw the track and thumb ourselves so the thumb is a known
   fixed width (18px) on every browser/OS. The native thumb width varies, which
   makes centering the value readout under the knob impossible to do exactly;
   owning it makes the math (THUMB in the JS) precise everywhere. */
.scale-slider input[type="range"] {
  display: block; width: 100%; margin: 0; height: 18px;
  -webkit-appearance: none; appearance: none;
  background: transparent; cursor: pointer;
}
.scale-slider input[type="range"]::-webkit-slider-runnable-track {
  height: 4px; border-radius: 2px; background: var(--color-border);
}
.scale-slider input[type="range"]::-moz-range-track {
  height: 4px; border-radius: 2px; background: var(--color-border);
}
.scale-slider input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none; appearance: none;
  width: 18px; height: 18px; border-radius: 50%; border: none;
  background: var(--color-accent); cursor: pointer;
  margin-top: -7px;  /* centre the 18px thumb on the 4px track */
}
.scale-slider input[type="range"]::-moz-range-thumb {
  width: 18px; height: 18px; border-radius: 50%; border: none;
  background: var(--color-accent); cursor: pointer;
}
.scale-slider input[type="range"]:focus-visible::-webkit-slider-thumb {
  outline: 2px solid var(--color-accent); outline-offset: 2px;
}
.scale-slider input[type="range"]:focus-visible::-moz-range-thumb {
  outline: 2px solid var(--color-accent); outline-offset: 2px;
}

/* A checkbox sitting next to its description, under a block label like the
   other fields. The checkbox keeps its natural size (not the full-width input). */
.checkbox-line { display: inline-flex; align-items: center; gap: 0.45rem; font-size: 1rem; font-weight: 400; margin-bottom: 0; cursor: pointer; }
.checkbox-line input[type="checkbox"] { width: auto; margin: 0; }
/* Form errors render as spans (templates/django/forms/errors/list/ul.html) so
   they can legally sit inside the field's row, right under the input box; the
   CSS makes them look like the classic bulleted red list.

   The over-limit counter joins them rather than having a look of its own. Being
   past a maximum IS a field error — the submit refuses the value — so the live
   message and the one that comes back from the server are the same message
   about the same box, said in the same words (MAX_LENGTH_MESSAGE). They had
   drifted in presentation anyway: identical sentences, one bulleted and
   indented, one not, swapping places as you left the box. One rule now draws
   whichever of them is talking — and it needs no selector of its own here,
   because over the limit the counter takes this very class (form_validation.js)
   rather than a lookalike. Listing both selectors would have been an assertion
   that they should match; this way they cannot help but. */
/* ONE SIZE for everything a field says about itself under its box, whichever
   way the news goes — a refusal, a confirmation, a running count. They occupy
   the same slot and take turns in it, so a difference in size read as one of
   them mattering more, when what separates them is colour and already says so.
   It is the size of the guidance above the box too, which leaves the box's own
   text as the only full-size thing in the row: the part that is the visitor's.

   Set here rather than on each, so a status added later cannot arrive at a
   size of its own. */
.errorlist, .field-ok, .char-counter { font-size: 0.85rem; }

/* THE FIELD'S STATUS LINE. Drawn for every field by django/forms/field.html,
   and the single place anything is said about one — a refusal, a confirmation,
   a running count. They take turns in it.

   It holds this height while empty, which is the point: a field that starts
   speaking must not shove the page down, and the confirming kind arrives just
   as somebody reaches for the button below. A line, plus room beneath it so a
   message never runs into the next field's label.

   A message longer than a line still grows the row. That is deliberate — the
   password box alone can fail four ways at once, and holding room for four
   lines under every box on the site would be a permanent hole for a rare
   event. `lh` is the honest unit; the em value is the same number by hand, for
   browsers without it. */
.field-status {
  display: block;
  font-size: 0.85rem;
  /* Clear of the box above it. A focused box wears a ring drawn 2px OUTSIDE
     itself, so with the status line sitting flush the two nearly touch and a
     refused field — ringed and speaking at once — reads as crowded. Just enough
     to let the ring finish; the reserved height below is untouched. */
  margin-top: 0.25rem;
  min-height: calc(1.55em + 0.45rem);
  min-height: calc(1lh + 0.45rem);
}
.errorlist {
  display: block; color: var(--color-danger);
  padding-left: 1.2rem;
}
.errorlist__item { display: list-item; list-style: disc; }
/* Live-validation extras (form_validation.js): the green "✓ available" mark and
   the character counter under minimum-length paragraph fields. Both sit in the
   status slot above, which is what holds the room — a mark is usually two words
   but it is whatever good news a field has, and the verification code's is a
   sentence, so it wraps rather than running out of a narrow card. */
.field-ok { display: block; color: var(--color-accent); margin-left: 0.5rem; }
.field-ok[hidden] { display: none; }
.char-counter { display: block; color: var(--color-muted); }
/* A maximum with room to spare says nothing at all, and `display: block` above
   would otherwise beat the browser's own rule for [hidden] and leave a gap. */
/* Keyed on the counter's identity, not its class: over the limit it drops
   .char-counter and takes .errorlist instead, and it must still be hideable. */
[data-char-counter][hidden] { display: none; }
.char-counter--met { color: var(--color-accent); }
/* Over the maximum the counter has no styling of its own at all — it takes the
   .errorlist class further down, because a value the submit will refuse should
   look like every other refusal. Typing is still never blocked. */

/* Drag-and-drop upload zones (form_validation.js piece 4): click or drop.
   The zone has the target canvas's own proportions (the script sets its
   aspect ratio) at the form's full width, so it IS the canvas at the size
   the page allows; a chosen image fills it. The hint beneath names the
   canvas, and after a drop goes green on an exact match or red with both
   sizes — guidance, not a gate. */
.dropzone {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.4rem;
  width: 100%;
  box-sizing: border-box;
  border: 2px dashed var(--color-border);
  border-radius: 10px;
  padding: 1.1rem;
  margin-top: 0.35rem;
  color: var(--color-muted);
  cursor: pointer;
  text-align: center;
}
.dropzone:hover, .dropzone:focus-visible, .dropzone--active {
  border-color: var(--color-accent);
  color: var(--color-text);
}
.dropzone--filled { padding: 0; }
/* The card canvas is a third of the banner's area. Drawn at the banner's
   scale it would be a small box; drawn at the form's full width it dwarfs
   the banner. Two thirds of the width is the compromise (Christian,
   2026-09-15): plainly the smaller canvas, still near its real 800 pixels. */
input[name="cover_image"] + .dropzone { max-width: 66%; }
/* Edge to edge, proportions kept: an image of the wrong shape shows bars. */
.dropzone-preview { width: 100%; height: 100%; min-height: 0; object-fit: contain; border-radius: 8px; }
.dropzone-hint { display: block; margin-top: 0.35rem; font-size: 0.85rem; color: var(--color-muted); }
.dropzone-hint--ok { color: var(--color-accent); }
.dropzone-hint--bad { color: var(--color-danger); }

/* Festival list cards (landing + Home): cover art beside the text when the
   festival has art — the 800×450 cover doing its "Steam lists" job at
   thumbnail size. Wraps to art-on-top when narrow; artless cards are text. */
.event-card { display: flex; flex-wrap: wrap; gap: 1rem; align-items: flex-start; container-type: inline-size; }
.event-card-art { flex: 0 0 220px; }
.event-card-art img { display: block; width: 100%; height: auto; border-radius: 8px; }
.event-card-body { flex: 1 1 16rem; min-width: 0; }
/* On a phone the body wraps under the art, and the art then spans the card
   rather than standing 220px wide over full-width text (Christian,
   2026-09-22, on his phone). Asked of the card, not the screen: the art goes
   full width exactly when the row can no longer hold the art, the gap and
   the body's minimum side by side — the same numbers as the rules above. */
@container (max-width: 30.75rem) { .event-card-art { flex-basis: 100%; } }
.event-card-body h3 { margin-top: 0; }

table { width: 100%; border-collapse: collapse; background: var(--color-surface); }
th, td { text-align: left; padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--color-border); }

/* Click-to-sort table headings. */
.sortable-table th[data-sort] { cursor: pointer; user-select: none; white-space: nowrap; }
.sortable-table th[data-sort]:hover { color: var(--color-accent); }
.sortable-table th.sorted-asc::after { content: ' ▲'; font-size: 0.7em; }
.sortable-table th.sorted-desc::after { content: ' ▼'; font-size: 0.7em; }

/* Small square cover thumbnail for the entries roster (placeholder when empty). */
.cover-thumb { width: 44px; height: 44px; object-fit: cover; border-radius: 4px; display: block; border: 1px solid var(--color-border); }
.cover-thumb--empty { background: var(--color-border); }

.phase-badge {
  display: inline-block;
  background: var(--color-accent-soft);
  color: var(--color-accent);
  border-radius: 999px;
  padding: 0.15rem 0.8rem;
  font-size: 0.9rem;
}

/* Phase control diagram: a vertical flow of one box per phase (each carrying the
   side effects its start brings), joined by down-arrows, with the current phase
   highlighted. All from the theme tokens, so it adapts to light/dark for free. */
.phase-flow { margin: 0.75rem 0 1.25rem; }
.phase-box {
  /* A narrow, centered box so each phase reads as a discrete node in the flow
     rather than a full-width banner. */
  max-width: 24rem;
  margin: 0 auto;
  text-align: center;
  border: 1px solid var(--color-border);
  border-radius: 6px;
  padding: 0.5rem 0.75rem;
  background: var(--color-surface);
}
.phase-box__label {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  font-weight: 600;
}
/* One short phrase per line, centred under the phase name (the box is already
   text-align:center) — no bullet markers; centred markers read poorly. */
.phase-box__points {
  margin-top: 0.35rem;
  color: var(--color-muted);
  font-size: 0.85rem;
  line-height: 1.4;
}
.phase-box__point { margin: 0.1rem 0; }
/* The connector between boxes — the "arrow" down the sequence. */
.phase-arrow {
  text-align: center;
  color: var(--color-muted);
  line-height: 1;
  margin: 0.25rem 0;
}
/* Done: subdued, dashed, ticked off. */
.phase-box--done { border-style: dashed; }
.phase-box--done .phase-box__label { color: var(--color-muted); font-weight: 500; }
.phase-box__check { color: var(--color-accent); }
/* Current: accent highlight — where the organizer is right now. */
.phase-box--current {
  border-color: var(--color-accent);
  background: var(--color-accent-soft);
}
.phase-box--current .phase-box__label { color: var(--color-accent); }
/* "You are here" pill on the current phase. */
.phase-box__here {
  background: var(--color-accent);
  color: var(--color-on-accent);
  border-radius: 999px;
  padding: 0.05rem 0.5rem;
  font-size: 0.7rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.04em;
}

/* Drag-to-rank list */
.rank-list { list-style: none; padding: 0; margin: 0; counter-reset: rank; }
.rank-list li {
  display: flex;
  align-items: center;
  gap: 0.75rem;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 6px;
  padding: 0.6rem 0.9rem;
  margin-bottom: 0.4rem;
  cursor: grab;
}
.rank-list li::before {
  counter-increment: rank;
  content: counter(rank);
  font-weight: 700;
  color: var(--color-accent);
  min-width: 1.5rem;
}
.rank-list li.sortable-ghost { opacity: 0.4; }
.drag-hint { color: var(--color-muted); font-size: 0.85rem; }

/* The row under the review form. Left: a standing state of the review (what
   it still needs before the game unlocks). Right: what the system last did
   (Saving… / ✓ Saved / a refusal), and a Save button until the page's script
   takes that slot over. Opposite corners because they are
   different kinds of fact and change on different clocks — as one sentence
   they read as one muddled thought. Wraps to two lines when narrow, state
   first. */
.review-status-row {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  gap: 1rem;
  flex-wrap: wrap;
}
.review-status { font-size: 0.85rem; }
.review-status.ok { color: var(--color-accent); }
.review-status.short { color: var(--color-danger); }

.winner-row { background: var(--color-accent-soft); }

/* Dashboard histograms */
.histo-section { font-size: 1rem; font-weight: 600; margin: 1.1rem 0 0.5rem; }
.histo-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 3.5rem; }
@media (max-width: 600px) { .histo-grid { grid-template-columns: 1fr; } }
.histo__title { font-weight: 600; font-size: 1.5rem; margin-bottom: 0.35rem; text-align: center; }
.histo__body { display: flex; gap: 6px; }
.histo__ylabel { writing-mode: vertical-rl; transform: rotate(180deg); align-self: center; text-align: center; font-size: 1.5rem; color: var(--color-muted); flex-shrink: 0; }
.histo__bars { flex: 1; min-width: 0; }
.histo__xlabel { text-align: center; font-size: 1.5rem; color: var(--color-muted); margin-top: 4px; }
.histo__row { display: flex; align-items: center; gap: 8px; margin-bottom: 3px; font-size: 0.85rem; color: inherit; text-decoration: none; border-radius: 4px; }
a.histo__row:hover { background: var(--color-accent-soft); }
a.histo__row:hover .histo__fill { filter: brightness(1.1); }
.histo__label { width: 64px; text-align: right; color: var(--color-muted); flex-shrink: 0; }
.histo__track { flex: 1; height: 16px; background: var(--color-background); border: 1px solid var(--color-border); border-radius: 4px; overflow: hidden; }
.histo__fill { display: block; height: 100%; background: var(--color-accent); }
.histo__count { width: 28px; text-align: right; flex-shrink: 0; font-variant-numeric: tabular-nums; }

/* Two-column ranker (Round 1 / Round 2 judging) */
.ranker { display: flex; gap: 1.5rem; align-items: flex-start; margin-bottom: 1rem; }
.ranker__column { flex: 1; min-width: 0; }
/* Centred over their columns: each heading labels the whole list beneath it,
   not just its first card — and set with enough weight, and enough air under
   it, to read as that column's title rather than as a line of body text. */
.ranker__column h2 { font-size: 1.3rem; margin-bottom: 0.8rem; text-align: center; }
.ranker__hint { color: var(--color-muted); font-size: 0.82rem; margin-top: 0; }
/* Numbered instructions, indented as a block from the body text. The numbers
   sit outside the text (the default) so a wrapped step lines up under itself
   rather than under its own digit. */
.step-list { margin: 0 0 1rem; padding-left: 2.75rem; }
.step-list li { margin-bottom: 0.25rem; }
/* Closes the ranking card: one line saying where the judge stands. The Done
   button used to sit under it here; it is a page-level action now and lives
   outside the card, so this is only ever the message. */
.ranker__finish { margin-top: 0.5rem; }
/* The two states of that slot: what's still owed, or that nothing is. They sit
   in the same place and never both apply, so they read as one line that changes
   rather than as two notices. */
/* The judging criterion. The lead-in is at body colour so the eye catches it
   among the muted instructions around it; the question itself follows on the
   same line, muted, reading as the instruction it is. */
.ranking-question { margin: 0 0 1.1rem; }   /* opens the ranking card */

.judging-todo { color: var(--color-danger); }
.judging-complete { color: var(--color-accent); font-weight: 600; }
/* Both zones stand the same height, and that height is the fuller column plus
   one card — so a column never looks full while there is still somewhere for a
   game to go. A seven-and-one split used to render as two brim-full boxes,
   which is what this fixes; a column holding every game keeps no gap, since
   nothing can arrive in it. The space this leaves in the emptier column is not
   waste — a yawning ranking column asks to be filled, a yawning unranked one
   shows the work already done. --ranker-slots carries the count (see
   judging.views._ranker_slots and the script in pool_detail.html, which
   recomputes it after each drop).

   5.4rem per slot is a card (4.9rem) plus the gap beneath it. A card whose
   title wraps runs taller, so this stays a min-height: an uneven foot is a
   far smaller sin than a card the box refuses to show. */
.ranker__list {
  min-height: calc(var(--ranker-slots, 2) * 5.4rem + 1rem);
  border: 1px dashed var(--color-border);
  border-radius: 8px;
  padding: 0.5rem;
  background: var(--color-background);
  transition: min-height 0.15s ease;
}

/* The title and the game's requirement chips — stacked, so the status row
   never competes with the name for the same 288px. */
.game-card__main { display: flex; flex-direction: column; flex: 1; min-width: 0; }
.game-card__needs {
  display: flex;
  align-items: center;
  gap: 0.3rem;
  margin-top: 0.25rem;
  flex-wrap: wrap;
}
.game-card__lock { font-size: 0.72rem; }
/* One job this game asks of its judge. Owed: an outline in muted text. Done:
   the accent, plus a ✓ inside the chip — the state never rides on colour
   alone. Deliberately smaller and quieter than the verdict badges on the
   organizer's tables: those report a ruling someone made, these are a
   to-do list the judge is working through. */
.req-chip {
  display: inline-block;
  border-radius: 999px;
  padding: 0.02rem 0.45rem;
  font-size: 0.72rem;
  font-weight: 400;
  line-height: 1.5;
  white-space: nowrap;
  color: var(--color-muted);
  border: 1px solid var(--color-border);
  /* Above the card body's stretched link, so a chip takes its own clicks —
     and so hovering one does NOT count as hovering the body. */
  position: relative;
  z-index: 2;
  text-decoration: none;
}
.req-chip--done { background: var(--color-accent-soft); color: var(--color-accent); border-color: transparent; }
/* Where a click would land. Hovering the card body lights the chip whose page
   the body opens (--next: the first job still owed); hovering a chip lights
   only that one, since the body is not hovered then. A card with nothing owed
   has no --next chip, so nothing lights and the body opens the game instead. */
.game-card__body:hover ~ .game-card__main .req-chip--next,
.game-card__link:hover ~ .game-card__needs .req-chip--next,
.game-card__link:focus-visible ~ .game-card__needs .req-chip--next,
a.req-chip:hover,
a.req-chip:focus-visible { box-shadow: 0 0 0 2px var(--color-accent); }

.game-card {
  position: relative;   /* the body link stretches to these bounds */
  display: flex;
  align-items: center;
  gap: 0.75rem;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 6px;
  padding: 0.5rem;
  margin-bottom: 0.5rem;
  text-decoration: none;
  color: var(--color-text);
}
.game-card:last-child { margin-bottom: 0; }
/* Something answers the pointer wherever it rests on a card — a card with
   every job done has no chip left to light, and would otherwise sit mute. */
.game-card:hover { border-color: var(--color-muted); }
.game-card__cover {
  display: block;
  width: 80px;
  height: 60px;
  object-fit: cover;
  border-radius: 4px;
  flex-shrink: 0;
}
.game-card__cover--placeholder { display: inline-block; background: var(--color-accent-soft); }
/* No flex:1 here any more — .game-card__main is the flex child now, and the
   title is simply the first line inside it. The title is also the card BODY's
   link: its ::after stretches over the whole card, so everything that isn't a
   chip opens the same page. Under the chips (z-index), never over them. */
/* Above the surface, so the game's name keeps the pointer and its underline:
   the title is the one part of the card that says "open me" in words, and a
   card with nothing outstanding has no chip left to light on hover. */
.game-card__link {
  position: relative;
  z-index: 2;
  align-self: flex-start;
  font-weight: 600;
  min-width: 0;
  color: inherit;
  text-decoration: none;
}
.game-card__link:hover, .game-card__link:focus-visible { text-decoration: underline; }
/* The card's surface: the same destination as the title, covering everything
   the words and chips don't. It wears the GRAB cursor, because a drag started
   anywhere on the card is a drag — the words and chips above it keep the
   pointer, so the two readings sit where each is true. Clicking here still
   navigates; a drag only becomes a drag once the pointer moves. */
.game-card__body {
  position: absolute;
  inset: 0;
  z-index: 1;
  cursor: grab;
  border-radius: inherit;
}
/* A locked card can't move, so its surface only ever means "open me". */
.game-card--locked .game-card__body,
.ranker__list--readonly .game-card__body { cursor: pointer; }
.game-card__body:active { cursor: grabbing; }
/* The handle, above the surface link so grabbing it can't navigate. Bigger
   than the form builder's: these cards are the page's main gesture, and a
   handle you have to aim at is a handle nobody uses. */
.game-card__drag {
  position: relative;
  z-index: 2;
  flex-shrink: 0;
  line-height: 1;
  padding: 0.35rem 0.25rem;
}
/* Locked cards carry no handle — they are filtered out of the sortable, so
   there is nothing to grab and the body's pointer is the whole story. */
.game-card--locked { opacity: 0.55; }
.game-card--carried { border-style: dashed; }
.game-card.sortable-ghost { opacity: 0.3; }
.game-card.sortable-chosen { box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18); }

.ranker__list--single { max-width: 420px; }
.ranker__list--readonly { border: none; padding: 0; background: transparent; }
.ranker__list--readonly .game-card { cursor: pointer; }
/* A card that opens nothing (a game withdrawn after judging closed): the name
   wears the linked titles' weight and none of their promises — no pointer, no
   underline, no border that answers a hover. */
.game-card__name { font-weight: 600; min-width: 0; }
.ranker__list--readonly .game-card--nowhere { cursor: default; }
.game-card--nowhere:hover { border-color: var(--color-border); }


/* Play-proof screenshots.
   The drop zone is a .screenshot-frame wearing drop-target clothes: the shape,
   the containment and the blurred backdrop all come from the frame, and only
   the dashed edge and the centring of the empty-state text are its own. Written
   with both class names so it outranks .screenshot-frame, which is defined
   further down the file. */
.screenshot-frame.proof-drop {
  border: 2px dashed var(--color-border);
  border-radius: 8px;
  /* No padding: a screenshot fills its frame, so the delete button in the
     corner sits on the picture rather than floating in a margin beside it. The
     empty state pads itself instead (.proof-drop p). */
  text-align: center;
  background: var(--color-background);
  max-width: 640px;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* Delete, in the corner a window's close button lives in. Sits over the art
   rather than beside it, so it is unmistakably about this picture. */
.proof-delete {
  position: absolute;
  top: 0.4rem;
  right: 0.4rem;
  width: 1.9rem;
  height: 1.9rem;
  padding: 0;
  border-radius: 6px;
  font-size: 1.2rem;
  line-height: 1;
  background: var(--color-surface);
  color: var(--color-text);
  border: 1px solid var(--color-border);
}
.proof-delete:hover { background: var(--color-danger); color: var(--color-on-danger); border-color: var(--color-danger); }
.proof-drop--over { border-color: var(--color-accent); background: var(--color-accent-soft); }
.proof-drop p { margin: 0; padding: 1rem; }
/* The upload's own status line, between the picture and the button. It holds
   its line whether or not there is anything to say: this line appears and
   disappears through a whole upload — preparing, uploading, saved — and a
   button that hops down the page each time is worse than a little empty space.
   min-height matches line-height exactly, so empty and one-line are the same
   height. */
.proof-status { margin: 0.4rem 0 0.35rem; line-height: 1.4; min-height: 1.4em; }
/* The button under that line. Its paragraph brings a 1rem top margin of its
   own, which collapses over anything smaller set above it, so the gap has to be
   cancelled here or the status line can't be tucked in close. line-height too:
   the button is an inline-block sitting on a text line, and the leading above
   it was a second gap that margins had nothing to do with.
   A class rather than a sibling selector — the confirm dialog sits between the
   two paragraphs in the markup, invisible but very much in the way. */
.proof-actions { margin-top: 0; line-height: 1; }
/* The foot of a page in a sequence, laid out like the corners of a book: the
   way back on the left, the way on to the right. A judge working through a
   game moves left-to-right across two pages and the buttons say so without
   anyone having to read them. */
.page-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 0.6rem;
  margin-top: 2rem;
}
/* A refusal reads as one — the screenshot upload's and the review autosave's
   alike. After .muted in the file, so it wins the colour. */
.proof-status--error,
.saved-indicator--error { color: var(--color-danger); }
/* A green tick, muted words. The whole line in green would put a verdict
   opposite the red one on the left of the same row — "all good" facing "not
   done yet" — about two different things. A mark acknowledging that the typing
   is safe is not a verdict on the review, so it can carry the colour while the
   sentence stays quiet. Grey alone was missed entirely, which is the wrong
   failure for the one line telling somebody their work is kept. */
.saved-indicator--done::before { content: '✓ '; color: var(--color-accent); }
.proof-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 1rem; }
/* Every screenshot the reader can click through to the full file. The frame
   inside takes the width these give it, which is the only thing that differs
   from one page to the next. */
.screenshot-zoom { display: block; cursor: zoom-in; }
/* The table minis navigate to a screenshots PAGE rather than opening the raw
   image, so they get the link hand — the zoom-in glass promises "this image,
   bigger", which is only what the full-size frames deliver. */
.screenshot-zoom--mini { width: 80px; cursor: pointer; }
/* The judge-review page's screenshot: the size the judge saw when they
   uploaded it — the play page's proof frame (.screenshot-frame.proof-drop)
   and closed-state record (.screenshot-review) are 640 too. */
.screenshot-zoom--full { max-width: 640px; }
.screenshot-review { max-width: 640px; }
/* The screenshot card's heading row (events/judge_review.html): title with
   the verdict chip on its right shoulder; a dispute's text hangs directly
   below the chip, left edges aligned — they share the chip's column. */
.screenshot-heading { display: flex; align-items: baseline; gap: 0.8rem; margin-bottom: 0.8rem; }
.screenshot-heading h3 { margin: 0; }
.screenshot-heading p { margin: 0.3rem 0 0; }
/* nowrap: a chip never breaks its own words, however narrow its column. */
.verdict-badge { display: inline-block; border-radius: 999px; padding: 0.1rem 0.6rem; font-size: 0.8rem; white-space: nowrap; }
/* The organizer's judging tables — every rendering of
   events/_judging_table.html: a game's judges (the game page, and the user
   page's results card) and a judge's games (the user page's pool card). One
   include, so the columns cannot drift apart between pages. Fixed layout,
   because the verdict column ends with a creator's dispute note truncated at
   the table's edge — and a cell can only clip what auto layout would instead
   grow to fit; the verdict column is last and takes the leftover width. */
/* The Contact card's lines (the organizer's user page) sit 0.75rem in —
   the left padding the table cells carry (th, td above) — so the name
   and email line up with the first column of every table down the page. */
.contact-card p { margin-left: 0.75rem; }
.verdict-table { table-layout: fixed; width: 100%; }
.verdict-table th.verdict-table-who { width: 26%; }
.verdict-table th.verdict-table-review { width: 12%; }
.verdict-table th.verdict-table-rank { width: 10%; }
.verdict-table th.verdict-table-shot { width: 14%; }
/* Chips, rank numbers and screenshot minis are values, not prose — they
   centre in their columns (the classes sit on th and td alike, so headers
   follow). The mini is a fixed-width block, so text-align cannot move it;
   it centres by margin instead. */
.verdict-table .verdict-table-review,
.verdict-table .verdict-table-rank,
.verdict-table .verdict-table-shot { text-align: center; }
.verdict-table td.verdict-table-shot .screenshot-zoom--mini { margin: 0 auto; }
/* Confirmed, Disputed and Unverified render at one width, so the chips
   make a clean edge down the column and every dispute note starts at the
   same x. The review chips keep their own sizes — they centre instead.
   Box-sizing is border-box, so this width is the WHOLE chip, padding
   included: sized to hold the widest labels ("Confirmed" ~82px,
   "Unverified" ~83px rendered) with a little slack for other platforms'
   font metrics. */
.verdict-table td.verdict-table-verdict .verdict-badge { min-width: 5.4rem; text-align: center; }
/* Badge and note share one line: the note sits to the badge's right and
   ellipsizes at the edge rather than wrapping under and stretching the row.
   Hover the note for the full text (title attribute). */
.verdict-table td.verdict-table-verdict { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* The creator's dispute note: one more door to the same screenshots page, but
   dressed as the muted prose it is rather than as a link — it only underlines
   on hover to admit it. The margin keeps it off the badge's shoulder. */
a.verdict-note { color: var(--color-muted); margin-left: 0.45rem; text-decoration: none; }
a.verdict-note:hover, a.verdict-note:focus-visible { text-decoration: underline; }
/* A verdict chip that is also the way into a screenshots page. The chip is the
   label, so no underline; the ring on hover/focus is the cue that this status
   can be clicked. */
a.verdict-link { text-decoration: none; }
a.verdict-link:hover .verdict-badge,
a.verdict-link:focus-visible .verdict-badge { box-shadow: 0 0 0 2px var(--color-accent); }
.verdict-badge--confirmed { background: #d9ecf7; color: #005b8f; }
.verdict-badge--disputed { background: #fbe6d6; color: #b04a00; }
.verdict-badge--pending { background: var(--color-background); color: var(--color-muted); }
/* Colorblind-safe status on the blue/orange axis, plus a solid-vs-dashed
   border pattern so the signal survives without color (and in grayscale).
   Confirmed is a MUTED steel blue rather than Okabe-Ito's vivid #0072b2:
   a heavy saturated blue ring is the OS idiom for a SELECTED image, and
   that is what it read as (Christian, 2026-08-10). Disputed keeps the
   vivid Okabe-Ito vermillion — a warning should shout.

   THE outline formatter, in one place: an element wears .verdict-card and
   declares its verdict as data-verdict (the model's raw value, straight off
   {{ proof.verdict }}) — no template chooses a modifier class, so no two
   surfaces can disagree about what a verdict looks like. Pending matches
   no rule and keeps the transparent space-reserving border. */
.verdict-card { border: 3px solid transparent; }
.verdict-card[data-verdict="confirmed"] { border-style: solid; border-color: #4a7a9b; }
.verdict-card[data-verdict="disputed"] { border-style: dashed; border-color: #d55e00; }
/* The outline on a bare screenshot frame (the judging tables' minis):
   outer radius = the frame's 6px + the 3px border, concentric corners. */
.screenshot-zoom.verdict-card { border-radius: 9px; }
.verdict-buttons { display: flex; gap: 0.5rem; }
.verdict-buttons > button { flex: 1; }
.dispute-panel { margin-top: 0.6rem; }
.dispute-panel textarea { width: 100%; box-sizing: border-box; resize: vertical; min-height: 5rem; margin-bottom: 0.5rem; }
.dispute-panel > button { width: 100%; }

/* Form builder */
.form-builder-nav { display: flex; justify-content: flex-start; gap: 0.6rem; margin-top: 1.2rem; }
.field-list, .field-list-builtin { list-style: none; padding: 0; margin: 0.5rem 0 0; }
.field-row--locked { background: var(--color-surface); }
.lock-icon { color: var(--color-muted); user-select: none; padding-top: 0.1rem; font-size: 0.9rem; }
.field-row {
  display: flex; align-items: flex-start; gap: 0.6rem;
  padding: 0.6rem 0.7rem; margin-bottom: 0.4rem;
  background: var(--color-background); border: 1px solid var(--color-border); border-radius: 6px;
}
.field-row .field-main { flex: 1; }
.field-row .field-actions { display: flex; align-items: center; gap: 0.8rem; white-space: nowrap; }
/* A row can hold a question about itself, and when it does the question takes a
   whole line under the field rather than squeezing in beside it. Wrapping is
   off until then, so an ordinary row is unchanged. */
.field-row:has(.confirm-step) { flex-wrap: wrap; }
.field-row .confirm-step { flex-basis: 100%; margin-top: 0.6rem; }
/* ⠿, everywhere something can be dragged. Sized up on 2026-08-11: at body
   size it read as punctuation, and a handle you have to aim at is a handle
   nobody reaches for. */
.drag-handle {
  cursor: grab;
  color: var(--color-muted);
  user-select: none;
  font-size: 1.35rem;
  line-height: 1;
  padding-top: 0.1rem;
}
.drag-handle:active { cursor: grabbing; }
.field-type-badge {
  display: inline-block; margin-left: 0.4rem; padding: 0.05rem 0.45rem; border-radius: 999px;
  font-size: 0.72rem; background: var(--color-accent-soft); color: var(--color-accent);
}
.field-type-badge--req { background: var(--color-danger-soft); color: var(--color-danger); }
.link-button.danger { color: var(--color-danger); }

/* Themed confirmation dialog (replaces the browser's native confirm box) */
.confirm-dialog {
  background: var(--color-surface);
  color: var(--color-text);
  border: 1px solid var(--color-border);
  border-radius: 8px;
  padding: 1.3rem 1.4rem;
  max-width: 440px;
  box-shadow: 0 10px 40px rgba(0, 0, 0, 0.35);
}
.confirm-dialog::backdrop { background: rgba(0, 0, 0, 0.55); }
.confirm-dialog h3 { margin: 0 0 0.6rem; }
.confirm-dialog p { margin: 0; }
/* A quieter second line (e.g. the discard dialog's "your real entry is safe"
   subtitle) gets a sliver of separation from the question above it. */
.confirm-dialog p + p { margin-top: 0.4rem; }
.confirm-dialog ul { margin: 0; padding-left: 1.25rem; }
.confirm-dialog li { margin: 0.2rem 0; }
.confirm-actions { display: flex; flex-wrap: wrap; gap: 0.6rem; margin-top: 1.1rem; }
/* The understated (secondary) choice gets the smaller font, whichever side it's on. */
.confirm-actions .secondary { font-size: 0.9rem; }

/* WHERE YOU ARE, in a vocabulary of its own.
 *
 * A popup opens with the focus on its harmless choice, so that Enter cannot
 * destroy anything merely because a dialog appeared — and the browser drew a
 * bright blue ring round it, which was both ugly and not ours.
 *
 * Coopting the button dresses for this was considered and dropped (Christian,
 * 2026-08-27). Across this site FILLED means "the next thing to do" — the
 * dashboard fills a form button until you have opened it, the review page fills
 * "Return to ranking" once the review is done — and that is a recommendation
 * the server computes from where somebody is in a process. Focus is a different
 * kind of fact: transient, and about where the cursor is rather than what to
 * press. Overloading one look with both would have put the site's strongest
 * do-this signal on "Yes, delete it" the moment a keyboard user tabbed to it.
 *
 * :focus-visible, NOT :focus, so that the ring appears for the people it is for.
 * Opening a popup with the mouse focuses its button in code, which does not
 * match :focus-visible, so a mouse user sees nothing at all. Tab, and it is
 * there.
 */
:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}
/* On a destructive control the ring follows the button rather than the accent,
   so nothing green outlines a red button. */
.danger:focus-visible { outline-color: var(--color-danger); }
/* And on a box that is currently being refused. The page puts the cursor in the
   field it could not accept and says why underneath in red — an accent ring
   round it at the same moment is the page agreeing and disagreeing with itself
   in one glance (Christian, 2026-08-27). The ring says where you are; it should
   not also imply everything is fine. */
.form-field:has(.errorlist) :focus-visible { outline-color: var(--color-danger); }

/* THE BUTTON THAT RAISED THE QUESTION, once the question is standing in the
 * card beside it. Only without scripting, and the difference is real rather
 * than cosmetic:
 *
 * With scripting the question is a modal, and the card behind a modal must look
 * exactly as it did — dimmed and out of reach, not rearranged. So the button
 * stays.
 *
 * Without it, the question is IN the card, right under the button that asked
 * for it, and both are live. "What happens if I press Withdraw again?" is a
 * fair thing to wonder and there is no good answer to it, so the spent button
 * goes (Christian, 2026-08-27). Nothing is lost: the question offers both ways
 * out.
 *
 * The server cannot tell these two apart — it renders one page for both — so
 * this is CSS's job, keyed off the class the script adds once it has run. Same
 * shape as the jury tier form's Move button above.
 *
 * :has() on the page rather than on the card, because the question does not
 * always stand in the same box as the control that raised it — a field
 * builder's question stands in one row of twenty, and the draft rescue's stands
 * outside the card holding its form. A page asks one thing at a time, so "this
 * page is waiting for an answer" is the true scope. On a browser too old for
 * :has(), the button stays and we are back to merely confusing, which is where
 * this started. */
html:not(.js-ready):has(.confirm-step) [data-asks-to-confirm] { display: none; }

/* The verification dialog wears the same look but is sized around different
   contents: a row of code boxes and an address to check against an inbox,
   rather than a question and two buttons. Narrower, with larger text, so the
   sentences reach across the card instead of trailing off — the boxes stretch
   to whatever width this sets, so a card wide enough to leave the writing
   short leaves the boxes looking stretched beside it.

   The width is the tightest that still lets the resend line and its "Sending a
   new code…" share one line; narrower and the status message wraps, leaving a
   word alone underneath.

   Naming a max-width of our own overrides the one the browser gives a dialog
   for free, gutter included, so the gutter is kept here instead — without it
   the card sits flush against both edges of a phone screen. */
/* Standing in the page, this card is inside another one — and a card within a
   card reads as a separate object rather than as the next step of the same
   errand (Christian, 2026-09-04). The two share a background, so the border and
   the side padding were the whole of the difference; without them the code
   simply continues the Email card.
   IT REACHES THE POPUP AS WELL, which is worth knowing and does no harm here:
   the script builds the dialog where the card already stood, so on this page
   "inside another card" stays true after the lift. The card comes out the same
   either way — on the road where that is not true the script has dropped the
   `card` class instead — and the dialog's own padding does the work. */
.card .code-card { border: 0; padding: 0; margin-bottom: 0; }
/* The NAME is the one thing that must not follow it there. In the page the card
   holding this one says Email a line above, so naming itself would be the same
   word twice; in a modal there is nothing above it and nothing else to say what
   it is, so it says so (Christian, 2026-09-04, reading the page: "it was
   supposed to still be printed when scripting was on").
   Two rules rather than one selector clever enough to hold both, and the second
   has to outweigh the first — which is what naming the dialog by its class buys
   here, rather than any need to know which dialog it is. */
.card .code-card > h2 { display: none; }
dialog.code-dialog .code-card > h2 { display: block; }

.code-dialog { max-width: min(390px, calc(100vw - 2rem)); }
/* The row keeps room for what it might say, whether or not it is saying
   anything. Otherwise the card grows as a message lands and shrinks when it
   goes — and since a dialog is centred, everything above the message moves
   too, so the boxes shift under the cursor at the moment somebody is typing
   into them.

   One line's worth, which is all any of these messages needs now that each is
   written to fit one on the narrowest phone (verification.SAID). Measured
   rather than guessed: the row is 51px holding just the boxes, 90px holding a
   one-line message, and 111px holding two — so this reserve fell by a line
   when the copy did.

   The bottom margin is tighter than a form row's usual, because the space
   under this row is reserved rather than incidental: at rest it is already
   holding a message's worth of room, and a message filling it does not want
   the ordinary gap underneath as well.

   KEYED ON THE CARD, NOT ON THE DIALOG. Scoped to .code-dialog this reached
   only the scripted rendering, where the script lifts the card into a popup —
   so the copy that stands in the page, which is the whole no-scripting road,
   reserved nothing and moved the page under the reader the moment the server
   had something to say (Christian, 2026-09-03). The same trap caught the
   note's spacing a few hours earlier: a rule hung on the popup is a rule half
   the visitors never get. */
.code-card .form-field { min-height: 5.625rem; margin-bottom: 0.6rem; }
.code-dialog h2 { font-size: 2rem; }
/* An email address is one long unbreakable word. On a narrow screen it has to
   be allowed to break mid-address, or it runs out past the edge of the card
   carrying it — and this card exists to be read against an inbox. */
.code-dialog p { font-size: 1.1rem; overflow-wrap: break-word; }
.code-dialog .small { font-size: 0.9rem; }
/* The address starts its own line BY INSTRUCTION, not by running out of room
   (Christian, 2026-09-03: "should be explicit, not an accident"). A long
   address wrapped onto its own line anyway, which made the two look identical
   while comparing them; a short one sat on the end of the sentence, which is
   the case this is for. Declared on a class rather than on
   [data-code-address], which is the script's handle for writing the address in
   — styling off the same attribute would tie the look to the wiring. */
.code-address { display: block; }
/* The note above the boxes is a caption ON the boxes, and its spacing now says
   so: the air goes ABOVE it, not below (Christian, 2026-09-03 — "making the
   note clearly be a comment about the code boxes, which it is"). It shipped
   the other way round, 6px above and 22px below, which grouped it with the
   address it happens to follow rather than the row it describes.

   The field row's own top margin has to be cleared as well. It collapses with
   the boxes' top margin and the larger wins, so leaving it at 12px keeps a
   paragraph's worth of air under a caption however small these numbers get.

   Scoped to .code-card, not .code-dialog, so the no-scripting rendering — this
   card inline on the page rather than lifted into a dialog — is spaced the
   same. Nothing outside this card uses either class.

   The card class earns its keep twice over on the note. Lifted into a dialog
   this card also carries the confirmation dialogs' class, whose `.confirm-dialog
   p + p` outranks a bare `.code-note` and quietly set this margin instead — so
   the spacing here needs the extra class to win, AND that rule reached only the
   scripted rendering, which is how the two came to be spaced differently in the
   first place. */
.code-card .code-note { margin: 1.25rem 0 0.35rem; }
.code-card .form-field { margin-top: 0; }
.code-card .code-boxes { margin-top: 0; }
@media (max-width: 380px) {
  /* Below this a title no longer fits on one line, and one broken across two
     reads worse than the same words a size smaller. Sized for the longest of
     them — this card is raised from more than one road, and each names its own
     errand in its own heading. */
  .code-dialog h2 { font-size: 1.6rem; }
}

/* Dashboard drill-down table */
.breakdown-table td { vertical-align: top; }
.pill {
  display: inline-block; margin: 0 0.25rem 0.25rem 0; padding: 0.05rem 0.5rem;
  border-radius: 999px; font-size: 0.8rem;
}
.pill--done { background: var(--color-accent-soft); color: var(--color-accent); }
.pill--pending { background: var(--color-danger-soft); color: var(--color-danger); }
/* .screenshot-mini and .screenshot-full lived here and both cropped with
   object-fit: cover. Every screenshot now goes through .screenshot-frame
   instead, which never crops — see _screenshot_frame.html. */
.review-answer { margin-bottom: 0.9rem; }
.review-answer p { margin: 0.1rem 0 0; white-space: pre-wrap; }
/* A game's own detail page (judging/game_detail.html). Not a card — a card is
   how a game looks inside something else, and this page IS the game.

   Steam authors header.jpg at 460x215 for every game on the store, so 460 is
   where the picture stops getting bigger and starts getting softer. That is a
   hard ceiling, and it decides the whole layout: the art cannot fill the card,
   so something has to sit beside it or a third of the row is empty.

   Narrowing the card to fit the art was tried and traded one problem for
   another — it squeezed the spec sheet below until the questions wrapped to
   three lines each. So the pitch goes beside the art instead: the art at its
   full size on the left, the two lines that sell the game on the right, and the
   spec sheet keeps the room it needs underneath.

   flex-start, so the title reads level with the top of the cover wherever
   the hero renders — centring made the title's height depend on how long
   the pitch happened to be, which read as no alignment at all. The title's
   own margin-top is already 0 (.game-hero-title). */
.game-hero {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: 1.5rem;
  margin-bottom: 0.4rem;
}
.game-hero-art { flex: 0 1 460px; }
/* The linked frame (detail_url pages): a block, so the anchor is exactly the
   frame with no inline descender gap under the art. */
.game-hero-art-link { display: block; position: relative; }
/* De-surprise the art: on hover or keyboard focus, the veil names the
   click's destination before it is committed — the same words as the
   quiet caption below the art, at the moment they are needed. Radius
   matches the frame's own rounding. */
.game-hero-art-veil {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0, 0, 0, 0.55);
  color: #ffffff;
  border-radius: 6px;
  opacity: 0;
  transition: opacity 0.12s ease;
}
.game-hero-art-link:hover .game-hero-art-veil,
.game-hero-art-link:focus-visible .game-hero-art-veil { opacity: 1; }
/* "Game details →" — centred under the art it opens, so the pair reads as
   one door. Caption-sized (.small's 0.85rem) and hugging the art: it
   labels the picture above, not whatever content follows below, so it
   sits close to the one and clear of the other. */
.game-hero-details { display: block; text-align: center; margin-top: 0.25rem; font-size: 0.85rem; }
/* A card straight after a bare hero is a masthead followed by its work
   (the judge's play and review pages) — daylight between the page's
   identity and the first step's box. Heroes inside cards (the detail
   pages) have no card sibling, so this touches nothing there. */
.game-hero + .card { margin-top: 1rem; }
/* Wraps under the art rather than shrinking to a sliver beside it. */
.game-hero-text { flex: 1 1 15rem; }
/* The title, living inside the box: store-page sized rather than
   page-heading sized, so it titles the game without shouting at the card.
   On the game's own detail page it is the page's <h1>; seen in passing it is
   a <p> linking there — so the weight is set here, not left to the tag, and
   the two render identically. Title and by-line sit tight as one lockup —
   the game and who made it — and the breathing room goes after the by-line,
   before the pitch. */
.game-hero-title { margin: 0 0 0.2rem; font-size: 1.7rem; font-weight: 700; line-height: 1.2; }
/* A title that happens to be clickable, not a link that happens to be a
   title: it keeps the body colour and picks up its underline on hover.
   Rendering it in the accent green would make the loudest thing on the box
   its linkiness. */
.game-hero-title a { color: inherit; text-decoration: none; }
.game-hero-title a:hover { text-decoration: underline; }
.game-hero-studio { margin: 0 0 1rem; }
/* The sentence that decides whether a judge is interested, so it reads at body
   colour rather than as a muted caption. */
.game-hero-pitch { margin: 0; }

/* Question-and-answer pairs (entries/_entry_answers.html) — a spec sheet, not a
   run of loose text.

   Two columns with a hairline between rows: questions line up down a narrow
   left column so the eye can scan them, answers sit in the wider right one
   where it can settle. That is the same shape as the tables the organizer's
   pages use, which is the point — this block used to look like it belonged to
   a different site.

   The question is muted and a shade smaller; the answer is body colour at body
   size, always, whatever type of field produced it. It was the other way round
   before, and inconsistently: "Yes" in body colour, a whole paragraph muted. */
.entry-answers {
  display: grid;
  grid-template-columns: minmax(8rem, 16rem) 1fr;
  column-gap: 1.5rem;
  margin: 1.2rem 0 0;
}
.entry-answers dt,
.entry-answers dd {
  border-top: 1px solid var(--color-border);
  padding: 0.75rem 0;
  margin: 0;
}
/* The question carries weight rather than dimness. It was muted grey, which is
   the right treatment for a caption nobody has to read and the wrong one for
   the half of the row that says what the other half MEANS — an answer of "60"
   is useless if you cannot make out the question above it. Bold and a shade
   smaller reads as a key without shouting over the answer. */
.entry-answers dt { font-weight: 600; font-size: 0.95rem; }
/* pre-wrap so the line breaks someone typed into a paragraph survive being
   read back. */
.entry-answers dd { white-space: pre-wrap; }

/* Too narrow for two columns: the question sits directly above its answer, and
   the rule goes above the PAIR rather than between its halves. */
@media (max-width: 40rem) {
  .entry-answers { grid-template-columns: 1fr; }
  .entry-answers dt { padding-bottom: 0.1rem; }
  .entry-answers dd { border-top: 0; padding-top: 0.1rem; }
}

@media (max-width: 640px) {
  .ranker { flex-direction: column; }
  .ranker__column { width: 100%; }
}

/* The registration form's live cover preview: the art confirms "yes, that's
   my game" beside the URL field, small on purpose. (Everywhere else a game's
   art renders through public/_cover_frame.html.) */
.cover-image {
  width: 180px;
  height: auto;
  border-radius: 6px;
  border: 1px solid var(--color-border);
  flex-shrink: 0;
}
.cover-preview { margin: 0 0 1.2rem; }
.cover-preview p { margin: 0 0 0.3rem; }

/* Flatpickr date picker (event create/edit). flatpickr.min.css loads in the
   <head> before site.css (via the vendor_css block) so these same-specificity
   rules win the cascade. Themed with the app's own variables to match light and
   dark, and — the whole point of adopting it — past (below-min) days render as
   visible grey instead of the near-invisible faint the default theme uses. */
.flatpickr-calendar {
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 8px;
  box-shadow: 0 6px 20px rgba(0, 0, 0, 0.28);
  color: var(--color-text);
}
.flatpickr-calendar.arrowTop::before { border-bottom-color: var(--color-border); }
.flatpickr-calendar.arrowTop::after { border-bottom-color: var(--color-surface); }
.flatpickr-calendar.arrowBottom::before { border-top-color: var(--color-border); }
.flatpickr-calendar.arrowBottom::after { border-top-color: var(--color-surface); }

/* Header — month name, year, and the prev/next arrows. */
.flatpickr-months .flatpickr-month,
.flatpickr-current-month input.cur-year,
.flatpickr-months .flatpickr-prev-month,
.flatpickr-months .flatpickr-next-month {
  color: var(--color-text);
  fill: var(--color-text);
}
.flatpickr-months .flatpickr-prev-month:hover svg,
.flatpickr-months .flatpickr-next-month:hover svg { fill: var(--color-accent); }

/* Month dropdown — strip the native <select> chrome (that grey gradient) and
   render it as plain themed text. */
.flatpickr-current-month .flatpickr-monthDropdown-months {
  appearance: none;
  -webkit-appearance: none;
  background: var(--color-surface);
  color: var(--color-text);
  border-radius: 4px;
  padding: 0 0.3ch;
}
.flatpickr-current-month .flatpickr-monthDropdown-months:hover {
  background: var(--color-accent-soft);
}
.flatpickr-current-month .flatpickr-monthDropdown-months .flatpickr-monthDropdown-month {
  background-color: var(--color-surface);
  color: var(--color-text);
}
/* Year spinner arrows (shown on hover). */
.flatpickr-current-month .numInputWrapper span.arrowUp::after { border-bottom-color: var(--color-muted); }
.flatpickr-current-month .numInputWrapper span.arrowDown::after { border-top-color: var(--color-muted); }
.numInputWrapper span { border-color: var(--color-border); }
.numInputWrapper span:hover { background: var(--color-accent-soft); }

span.flatpickr-weekday { color: var(--color-muted); }

/* Days. Selectable days stay bright, including spill-over from adjacent months. */
.flatpickr-day { color: var(--color-text); border-radius: 6px; }
.flatpickr-day.prevMonthDay,
.flatpickr-day.nextMonthDay { color: var(--color-text); }
.flatpickr-day:hover,
.flatpickr-day:focus {
  background: var(--color-accent-soft);
  border-color: var(--color-accent-soft);
  color: var(--color-text);
}
.flatpickr-day.today { border-color: var(--color-accent); }
.flatpickr-day.today:hover { background: var(--color-accent-soft); color: var(--color-text); }
.flatpickr-day.selected,
.flatpickr-day.selected:hover {
  background: var(--color-accent);
  border-color: var(--color-accent);
  color: var(--color-on-accent);
}
/* Past / below-min days: visibly greyed (not the default's near-invisible faint)
   and not selectable. */
.flatpickr-day.flatpickr-disabled,
.flatpickr-day.flatpickr-disabled:hover {
  color: var(--color-muted);
  background: transparent;
  cursor: not-allowed;
}

/* ---------------------------------------------------------------------------
   Jury room. The jury works down a list of hundreds of games, so the room is
   grouped exactly as the public showcase will be — podium, runners-up,
   honorable mentions, then the collapsed tail — and each row carries a single
   control instead of a bank of buttons.
   --------------------------------------------------------------------------- */
.jury-counts { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: baseline; margin: 0 0 1.4rem; }
.podium-slots { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; margin-bottom: 2rem; }
@media (max-width: 700px) { .podium-slots { grid-template-columns: 1fr; } }
.podium-slot { border-top: 3px solid var(--color-border); }
.podium-slot--1 { border-top-color: var(--color-gold); }
.podium-slot--2 { border-top-color: var(--color-silver); }
.podium-slot--3 { border-top-color: var(--color-bronze); }
.podium-slot-label { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--color-muted); margin: 0 0 0.35rem; }
.podium-slot--1 .podium-slot-label { color: var(--color-gold); }
.podium-slot--2 .podium-slot-label { color: var(--color-silver); }
.podium-slot--3 .podium-slot-label { color: var(--color-bronze); }
.podium-slot-game { font-weight: 600; font-size: 1.1rem; margin: 0 0 0.2rem; }
.jury-rank { color: var(--color-muted); font-variant-numeric: tabular-nums; }
.jury-tier-form { gap: 0.4rem; }
/* The no-JS fallback: once form_validation.js has run, the select posts itself
   and the Move button is redundant. */
.js-ready .jury-tier-submit { display: none; }
.jury-tail { margin-top: 2rem; }
.jury-tail summary { cursor: pointer; font-weight: 600; padding: 0.5rem 0; }

/* Screen-reader-only label text: present for assistive tech, invisible on
   screen. The clipped-rect idiom, which (unlike display:none) keeps the text
   in the accessibility tree. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* ---------------------------------------------------------------------------
   The Showcase (public/results.html). A festival's published page is a shop
   window, not a report card: a podium, then galleries of celebrated games.
   --------------------------------------------------------------------------- */

/* The podium. 1st spans the row with its art alongside; 2nd and 3rd share the
   row beneath with art on top. The hierarchy is size and position — the medal
   is a thin edge and a coloured label, never a fill. */
.podium-showcase {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 1.2rem;
  margin-bottom: 1rem;
}
.podium-card { position: relative; border-top: 3px solid var(--color-border); padding: 0; }
/* center, not stretch: the art keeps its own aspect (it has a fixed width and
   ratio, so it never stretches) while the short text column sits against the
   middle of it. Top-aligning left all the slack in one wide-open bottom-right
   corner; centring splits it evenly above and below. */
/* Two independent things, and they used to be one. WHICH PLACE a card holds
   decides its medal; whether it is ALONE IN ITS ROW decides whether it spans
   the width. They coincided for as long as first place was the only card that
   could be alone — until a withdrawal left a lone second or third, which has
   to widen while staying silver or bronze. */
.podium-card--place-1 { border-top-color: var(--color-gold); }
.podium-card--place-2 { border-top-color: var(--color-silver); }
.podium-card--place-3 { border-top-color: var(--color-bronze); }
.podium-card--wide { grid-column: 1 / -1; display: flex; gap: 0; align-items: center; }
.podium-card-art { display: block; flex-shrink: 0; }
/* The winner's art leads its own card. At 46% it came out SMALLER than the
   half-width 2nd/3rd cards' art (400px against 424px) and narrower than its own
   blurb — the hierarchy inverted itself. 62% (set on .podium-card-media above)
   makes it ~1.6x the runners-up. */
.podium-card-body { padding: 1.2rem 1.4rem 1.4rem; }
.podium-place {
  font-size: 0.8rem;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-weight: 700;
  margin: 0 0 0.3rem;
}
.podium-card--place-1 .podium-place { color: var(--color-gold); }
.podium-card--place-2 .podium-place { color: var(--color-silver); }
.podium-card--place-3 .podium-place { color: var(--color-bronze); }
.podium-card-title { margin: 0 0 0.2rem; font-size: 1.35rem; }
/* The card's one real link. Inherits the heading's colour so the card stays
   calm, and earns the accent on hover/focus so it's discoverable. */
.podium-card-title a { color: inherit; text-decoration: none; }
.podium-card-title a:hover { color: var(--color-accent); text-decoration: underline; }
.podium-card-title a:focus-visible { color: var(--color-accent); }
.podium-card--wide .podium-card-title { font-size: 1.7rem; }
@media (max-width: 700px) {
  .podium-showcase { grid-template-columns: 1fr; }
  .podium-card--wide { display: block; }
  .podium-card--wide .podium-card-media { width: 100%; }
}

.showcase-tier-note { margin: -0.6rem 0 1.2rem; }

/* The capsule galleries. Steam-style: cover art, title beneath, and a panel of
   detail on hover. auto-fill keeps the rows full at any width without media
   queries; the fixed art box means 200 lazy-loading images can't shuffle the
   page under the reader as they arrive.

   A tier arrives as a run of BANDS (see showcase.banded) — consecutive grids
   that differ only in column width, so some covers run large and the wall of
   rectangles breaks up. One mixed grid would be the obvious way to do that and
   doesn't work: a capsule is art of a fixed shape plus a title of a fixed
   height, so its proportions change with its width, and a double-width cover
   spanning two rows comes up ~35px short — a hole under every large tile.
   Bands sidestep it because every capsule inside one band is identical.

   The margin below a band equals the gap inside it, so a reader sees one
   continuous gallery with a rhythm rather than a stack of sections. */
.capsule-grid {
  display: grid;
  gap: 1rem;
  margin-bottom: 1rem;
}
/* Both sizes share the page's grid lines: a feature column is exactly two
   normal columns plus the gap between them, so band edges line up down the
   page however many columns the viewport allows. */
.capsule-grid--normal { grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); }
.capsule-grid--feature { grid-template-columns: repeat(auto-fill, minmax(396px, 1fr)); }
/* A name set at the small size looks stranded under art twice as wide. */
.capsule-grid--feature .capsule-title { font-size: 1.05rem; }
.capsule { position: relative; }
.capsule-link {
  display: block;
  text-decoration: none;
  color: inherit;
  border-radius: 8px;
}

.capsule-title {
  display: block;
  margin-top: 0.45rem;
  font-size: 0.9rem;
  font-weight: 600;
  line-height: 1.35;
}
.capsule-link:hover .cover-frame,
.capsule-link:focus-visible .cover-frame { border-color: var(--color-accent); }
.capsule-link:hover .capsule-title { color: var(--color-accent); }

/* The detail panel, shared by the gallery capsules and the podium cards. Steam's
   homepage carries no descriptions on its cards either — art and a name, with
   the words on hover. Anchored to its host's left edge and capped in width, so
   it can never overflow the viewport at the edges of the grid. Hidden with
   visibility (not display) so it stays in the accessibility tree. */
.hover-panel {
  position: absolute;
  top: 0;
  left: 100%;
  margin-left: 0.6rem;
  /* Wider than the capsule it belongs to, and deliberately so: a panel exactly
     the width of a cover blends straight back into the grid it is trying to
     rise out of. Overlapping its neighbours is what makes it read as floating
     above the page — the way Steam's and itch's do. */
  width: 19rem;
  z-index: 30;
  padding: 0.85rem 1rem;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 8px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.45);
  visibility: hidden;
  opacity: 0;
  transition: opacity 0.12s ease;
}
/* Flipped by form_validation.js when there isn't room on the right — the whole
   reason the panel originally sat below the capsule instead of beside it. */
.hover-panel--flip-left {
  left: auto;
  right: 100%;
  margin-left: 0;
  margin-right: 0.6rem;
}
/* Same idea vertically: near the bottom of the viewport, align the panel's
   bottom edge with the capsule's instead of its top. */
.hover-panel--align-bottom { top: auto; bottom: 0; }

/* A podium card spans too much width to have a side to open into, so its panel
   lays over the bottom of its own cover art — the featured-item caption
   treatment. It still overlaps imagery (which is what makes a panel read as
   floating rather than as another cell), and it can never leave the card. */
.podium-card-media { position: relative; }
.podium-card--wide .podium-card-media { width: 62%; flex-shrink: 0; }
.podium-card .hover-panel {
  top: auto;
  bottom: 0.6rem;
  left: 0.6rem;
  right: auto;
  margin: 0;
  width: calc(100% - 1.2rem);
  max-width: 24rem;
  background: rgba(20, 20, 20, 0.94);
}
.capsule:hover .hover-panel,
.capsule:focus-within .hover-panel,
.podium-card:hover .hover-panel,
.podium-card:focus-within .hover-panel { visibility: visible; opacity: 1; }
/* The panel is a link, so the whole surface is clickable — but it should read
   as a card, not as a big blue anchor. */
.hover-panel { color: inherit; text-decoration: none; }
.hover-panel-title { display: block; font-weight: 700; margin: 0 0 0.1rem; }
.hover-panel-studio { display: block; font-size: 0.85rem; color: var(--color-muted); }
.hover-panel-blurb { display: block; margin: 0.5rem 0 0.6rem; font-size: 0.88rem; }
/* The one remaining "this goes to Steam" affordance, now that the button is
   gone from the card. Styled like a link without being a nested anchor. */
.hover-panel-cue { display: block; font-size: 0.88rem; color: var(--color-accent); }
.capsule:hover .hover-panel-cue,
.podium-card:hover .hover-panel-cue { text-decoration: underline; }
/* Touch devices have no hover: the panel would either stick open after a tap
   or flash on the way to the store link. The capsule is the link, so drop it. */
@media (hover: none) {
  .hover-panel { display: none; }
}

/* Where to go after browsing one gallery. */
.showcase-more { display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: 1rem; }
.showcase-more-card { display: block; text-decoration: none; color: inherit; }
.showcase-more-card strong { display: block; margin-bottom: 0.25rem; }
.showcase-more-card:hover { border-color: var(--color-accent); }
.showcase-more-card:hover strong { color: var(--color-accent); }

/* Concluded festivals on the home page, led by their games (_showcase_teaser).
   The strip is one link into the gallery: covers at a scannable size, laid out
   so a narrow window drops columns instead of squeezing them. */
.showcase-teaser-title { margin: 0 0 0.3rem; }
.showcase-teaser-title a { text-decoration: none; color: inherit; }
.showcase-teaser-title a:hover { color: var(--color-accent); }
.showcase-teaser-blurb { margin: 0 0 0.9rem; }
.showcase-strip {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
  gap: 0.6rem;
  margin-bottom: 0.9rem;
}
.showcase-strip img {
  width: 100%;
  aspect-ratio: 16 / 9;
  object-fit: cover;
  display: block;
  border: 1px solid var(--color-border);
  border-radius: 6px;
}
.showcase-strip:hover img { border-color: var(--color-accent); }
.showcase-teaser-link { margin: 0; }

/* A quiet pointer to a past gallery, offered beside an offer (the event page's
   pitch) or below a form (the registration ladder) — never above either, so it
   can't pull someone out of the thing they came to do. */
.showcase-proof { color: var(--color-muted); margin: 1.2rem 0 0; }

/* An entrant's own placement (my_entry) — the one rank the site ever shows,
   and only to its owner. Big number, quiet explanation. */
.placement-figure { font-size: 1.6rem; margin: 0 0 0.4rem; }
.placement-figure strong { font-size: 2.2rem; }

/* What withdrawing does, listed on the maker's Withdraw card
   (entries/_withdraw_card.html). Indented like the lists in a question popup:
   a browser's own 40px would start the bullets well in from the heading they
   belong to. */
.withdraw-effects, .withdraw-effects ul { padding-left: 1.25rem; }
.withdraw-effects { margin: 0.6rem 0; }
.withdraw-effects ul { margin: 0.2rem 0 0; }
.withdraw-effects li { margin: 0.2rem 0; }

/* Verification code: six one-digit boxes that read as one field. Big enough
   to check against an email at a glance, and six separate boxes so the row
   can't be mistaken for a normal text input.

   The row fills whatever holds it — the boxes share out the width rather than
   each taking a fixed one. It lives in two places of different widths (inline
   on the log-in page, and lifted into the dialog), and a fixed width that
   filled one left the other ending short of its own text with dead space
   beside it. Sharing out the width squares the row off against the text above
   it in both, and means the narrow-screen rule below only has to lower the
   floor rather than re-tune a second set of widths.

   UP TO A CEILING, and the ceiling is the boxes being square. Sharing out the
   width is right inside the dialog and wrong in the card that holds this
   without scripting: that card is the page's full width, so the six boxes
   stretched across all of it and ran far past the sentence above them
   (Christian, 2026-09-04). Written as the sum it is — six box heights and the
   five gaps between them — so it follows those two numbers instead of adding a
   third that would go stale beside them. The dialog sits a shade under it, so
   nothing there moves. */
.code-boxes {
  display: flex;
  gap: 0.5rem;
  margin: 1.4rem 0 1.1rem;
  max-width: calc(6 * 3.2rem + 5 * 0.5rem);
}
/* Selector written as input.code-box, not .code-box: the shared form-field rule
   is input[type="text"], … which outranks a bare class on specificity and would
   otherwise keep forcing width:100% on every box. */
input.code-box {
  flex: 1 1 0;
  min-width: 2.6rem;
  height: 3.2rem;
  padding: 0;
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 1.6rem;
  text-align: center;
}
input.code-box:focus { border-color: var(--color-accent); outline: 2px solid var(--color-accent-soft); }
/* Being wrong recolours the same two things being right recolours — the border
   hugging the box, and the ring outside it — so moving between the two states
   changes hue and nothing else.
   Without this the site-wide error rule repaints only the RING, and the ring is
   where the two states are least alike: soft enough to disappear against this
   card while the box is accepting, bright while it is not. So the mark somebody
   is actually looking at jumps from the border to a ring two pixels further
   out, and jumps back the moment they start retyping. The border was also left
   green while the ring went red, which is a box saying it is fine and wrong at
   the same time.
   Worth settling here rather than waiting for the site-wide focus pass, for two
   reasons that are particular to this card (Christian, 2026-08-29): it puts the
   cursor in the first box itself, so this mark is in front of everybody rather
   than only the people who arrive by keyboard; and being refused and starting
   again are seconds apart, so both marks are seen one after the other by the
   same pair of eyes. */
.form-field:has(.errorlist) input.code-box:focus {
  border-color: var(--color-danger);
  outline-color: var(--color-danger-soft);
}
@media (max-width: 420px) {
  .code-boxes { gap: 0.35rem; }
  input.code-box { min-width: 2.2rem; height: 2.8rem; font-size: 1.3rem; }
}

/* The site's own hero, for every page that isn't inside a festival. A shallow
   strip rather than a full 1920x622 banner: pages should never look bare, but
   the site's own furniture must not shove the page's content down the way
   festival art legitimately does. */
/* Both heroes are links to their own root (see _page_banner.html), so they are
   anchors — middle-click and open-in-new-tab work as anyone would expect. They
   are meant to look exactly as they did before: no underline, no colour of
   their own, and the pointer cursor is affordance enough. */
a.site-banner { color: inherit; text-decoration: none; }
.festival-banner-link { display: block; }
/* The hand, over every part of a hero — images included, and not left to the
   browser's default for a link. An <img> is a drag source unless told
   otherwise, and a browser that has armed a drag shows an arrow instead: on
   the one element whose whole job is being clickable, that reads as "this
   isn't a link". The templates also mark these images draggable="false", so
   neither half depends on the other. */
a.site-banner, a.site-banner *,
.festival-banner-link, .festival-banner-link * { cursor: pointer; }

/* A finished rung 1 leads back to the game details. It stays looking like the
   rung it is — the ladder must not start reading as a row of buttons — so it
   inherits the rung's colour and only underlines on hover. */
.entry-step-link { color: inherit; text-decoration: none; }
.entry-step-link:hover, .entry-step-link:focus-visible { text-decoration: underline; }

.site-banner {
  display: flex;
  align-items: center;
  gap: 1rem;
  background: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 10px;
  padding: 0.8rem 1.1rem;
  margin-bottom: var(--hero-gap);
}
.site-banner-logo {
  width: 52px;
  height: 52px;
  flex-shrink: 0;
  background: #ffffff;
  border-radius: 8px;
  padding: 3px;
  box-sizing: content-box;
}
.site-banner-text { margin: 0; font-weight: 600; line-height: 1.3; }
.site-banner-text span {
  display: block;
  font-weight: 400;
  font-size: 0.9rem;
  color: var(--color-muted);
}

/* ---------------------------------------------------------------------------
   The cover frame (public/_cover_frame.html). ONE shape for every game's art,
   everywhere: podium and gallery differ in size, never in proportion.
   --------------------------------------------------------------------------- */

/* The frame IS Steam's store-art shape (460x215), not a compromise between the
   two sources. Christian, 2026-08-03: the festival's destination is a Steam
   Sales Event, so Steam art is the standard and itch is the exception that
   letterboxes. Steam covers land edge to edge with no bars at all; itch's
   1.26:1 gets side bars, filled by the blur below. `contain` still guarantees
   neither is ever cut. */
.cover-frame {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: var(--cover-aspect);
  overflow: hidden;
  border-radius: 8px;
  border: 1px solid var(--color-border);
  background: var(--color-surface);
  /* No content-visibility here, deliberately. It was tried (2026-08-04) to skip
     the cost of two hundred offscreen blurs and did not help — removing the
     blurs themselves did (see .cover-frame-fill). What it did do was defer
     rendering until the moment the content was needed, so a scroll that jumps
     rather than glides — an anchor link, a restored scroll position, the End
     key — could land on a screenful of blank frames. A frame holds one
     absolutely-positioned image inside a fixed aspect-ratio box, so there was
     almost no layout to skip in return. */
}
/* The letterbox filler: the same image, blown up past the frame and blurred to
   a wash of the art's own colours, so the bars beside an odd-shaped cover don't
   read as a mistake. Purely decorative, and INSERTED BY form_validation.js —
   only for the covers whose proportions actually leave bars. The frame is
   Steam's shape, so most store art fits it exactly and never gets one of these.

   Keep it that way. When every cover carried a fill, a two-hundred-game
   showcase took about a second to open a hover panel in Safari (and nothing in
   Chrome); at ~19 fills it is instant. Blur is by far the most expensive thing
   on this page, so its cost belongs to the covers that show it and no others.

   scale() hides the soft edge blur() leaves at the boundary. */
.cover-frame-fill {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: blur(22px) saturate(1.3) brightness(0.75);
  transform: scale(1.2);
}
/* The real thing: whole, never cropped. */
.cover-frame-art {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* ---------------------------------------------------------------------------
   The screenshot frame (_screenshot_frame.html). The same idea as .cover-frame
   and deliberately NOT the same class: cover art is Steam's 460×215 and a
   screenshot is whatever a judge's capture produced, so one shape could never
   serve both. Two constants, two frames; an image knows which it is by the
   frame it sits in, and nothing declares a per-image aspect anywhere.

   Sizing is the caller's: the frame fills the width it is given, so a table
   cell, a grid card and a review panel differ in size and agree on shape.
   --------------------------------------------------------------------------- */
.screenshot-frame {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: var(--screenshot-aspect);
  overflow: hidden;
  border-radius: 6px;
  border: 1px solid var(--color-border);
  background: var(--color-surface);
}
/* Whole, never cropped — the point of the exercise. A creator deciding whether
   a screenshot really shows their game must be looking at all of it. */
.screenshot-frame-art {
  position: relative;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}
/* The letterbox filler, as on .cover-frame-fill: the same image blown up and
   blurred, so the bars beside an odd-shaped capture read as deliberate. Added
   by form_validation.js and only where there are bars to fill.

   Blur is the most expensive thing on any page here — two hundred of them once
   cost a second per hover in Safari — so it is kept off the frames that can't
   show it. A frame carrying data-no-fill never gets one. */
.screenshot-frame-fill {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: blur(18px) saturate(1.3) brightness(0.75);
  transform: scale(1.2);
}
@media (prefers-reduced-motion: no-preference) {
  .cover-frame-art { transition: transform 0.18s ease; }
  .capsule-link:hover .cover-frame-art { transform: scale(1.03); }
}
