/* Borrowed from ScatterSports @ f7fe90b7. Diverges from here — do not sync blindly. */
/* ScatterSports brand layer.
   Standalone raw CSS (NOT compiled by Tailwind) so edits deploy via a normal restart,
   not the heavy image rebuild. Uses DaisyUI v5 theme vars (--color-*) so everything
   tracks light/dark automatically. Loaded in base.html AFTER output.css. */

:root {
  /* ─── SPACING SCALE ──────────────────────────────────────────────────────────────
     Eight steps, and they are TAILWIND'S steps. There were 27 hand-picked values across
     73 declarations — .1 / .12 / .15, .35 / .4, .75 / .8 / .85 — differences of half a
     pixel that nobody chose and nobody can see, but which every new rule had to guess
     between. Pick from these.

     The important part is not that there are eight. It is `var(--spacing)`: Tailwind v4
     emits that into :root and builds every utility as calc(var(--spacing) * N), so p-4,
     gap-3 and mt-6 all come from it. brand.css loads after output.css, so deriving from
     the same variable makes `gap-3` in a template and `var(--sp-5)` in a component the
     SAME 12px, permanently.

     Before this, the two were unrelated number sets. A template using utilities and a
     component using brand CSS could not agree even in principle, which is what "different
     fonts, sizes and shadings" actually was. One scale, one source, both consumers.

     Snapping moved 40 declarations, none by more than 3.2px.

     NOT everything numeric belongs here. The tokens below are derived from the control's
     line box rather than the rhythm, and get names instead of steps — a scale is for
     rhythm, metrics get names. */
  --sp-1: calc(var(--spacing) * 0.5);   /*  2px  — Tailwind's 0.5 */
  --sp-2: calc(var(--spacing) * 1);     /*  4px  — 1   */
  --sp-3: calc(var(--spacing) * 1.5);   /*  6px  — 1.5 */
  --sp-4: calc(var(--spacing) * 2.5);   /* 10px  — 2.5 */
  --sp-5: calc(var(--spacing) * 3);     /* 12px  — 3   */
  --sp-6: calc(var(--spacing) * 4);     /* 16px  — 4   */
  --sp-7: calc(var(--spacing) * 5);     /* 20px  — 5   */
  --sp-8: calc(var(--spacing) * 6);     /* 24px  — 6   */

  --ff-label-room: 1.05rem;  /* padding holding the label above the control it names */
  --ff-flag-inset: 2.5rem;   /* clears autocomplete.js's absolutely-positioned flag box */

  --radius-brand: 0.9rem;
  --shadow-glow-primary: 0 10px 22px -6px color-mix(in oklch, var(--color-primary) 45%, transparent);
}

/* --- Intent-based container widths (replace ad-hoc max-w-* over time) --- */
.container-content { max-width: 48rem; margin-inline: auto; width: 100%; } /* reading / detail */
.container-form    { max-width: 56rem; margin-inline: auto; width: 100%; } /* dense authoring */
.container-wide    { max-width: 80rem; margin-inline: auto; width: 100%; } /* dashboards / directories */

/* --- Brand buttons (use WITH .btn): <button class="btn btn-brand"> --- */
.btn-brand {
  border: 0;
  color: var(--color-primary-content);
  background-image: linear-gradient(135deg, var(--color-primary), var(--color-secondary));
  transition: filter .15s ease, transform .06s ease, box-shadow .15s ease;
}
.btn-brand:hover { filter: brightness(1.08); }
.btn-brand:active { transform: scale(.97); }

/* Elevated/glow primary — lifts on hover. For hero CTAs / data surfaces. */
.btn-glow {
  box-shadow: var(--shadow-glow-primary);
  transition: box-shadow .18s ease, transform .18s ease;
}
.btn-glow:hover {
  box-shadow: 0 18px 30px -6px color-mix(in oklch, var(--color-primary) 60%, transparent);
  transform: translateY(-2px);
}
.btn-glow:active { transform: translateY(0); }

/* Opt-in press feedback for any button. */
.btn-press:active { transform: scale(.96); }

/* Animated sheen on hover (for the occasional marquee CTA). */
.btn-sheen { position: relative; overflow: hidden; }
.btn-sheen::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(120deg, transparent 30%, color-mix(in oklch, white 35%, transparent) 50%, transparent 70%);
  transform: translateX(-120%); transition: transform .6s ease;
}
.btn-sheen:hover::after { transform: translateX(120%); }

/* --- "Pop" buttons (à la buttons.png): pill + gradient + thick dark border + hard
   offset shadow + bold uppercase. Use WITH .btn and a colour variant:
   <button class="btn btn-pop btn-pop-pink">Sign up</button> --- */
.btn-pop {
  border-radius: 9999px;
  border: 3px solid #0b1020;
  color: #fff;
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  background-image: linear-gradient(135deg, var(--color-primary), var(--color-secondary));
  box-shadow: 5px 6px 0 0 #0b1020;
  transition: transform .08s ease, box-shadow .08s ease, filter .12s ease;
}
.btn-pop:hover { transform: translate(2px, 2px); box-shadow: 3px 3px 0 0 #0b1020; filter: brightness(1.05); }
.btn-pop:active { transform: translate(5px, 6px); box-shadow: 0 0 0 0 #0b1020; }
.btn-pop-pink   { background-image: linear-gradient(135deg, #ff6ec4, #d6249f); }
.btn-pop-violet { background-image: linear-gradient(135deg, #7b6cff, #3b82f6); }
.btn-pop-teal   { background-image: linear-gradient(135deg, #2ee6c5, #10b981); }
.btn-pop-blue   { background-image: linear-gradient(135deg, #3aa0ff, #1565d8); }
.btn-pop-rose   { background-image: linear-gradient(135deg, #ff6b6b, #dc2626); }  /* red = stop/cancel */
/* Dark theme: swap the near-black ring/shadow for a light one so the hard-shadow stays visible. */
[data-theme="dark"] .btn-pop { border-color: rgba(255,255,255,.28); box-shadow: 5px 6px 0 0 rgba(255,255,255,.18); }
[data-theme="dark"] .btn-pop:hover { box-shadow: 3px 3px 0 0 rgba(255,255,255,.18); }
[data-theme="dark"] .btn-pop:active { box-shadow: 0 0 0 0 rgba(255,255,255,0); }

/* ─── Help chip (see helptext app + {% help "slug" %}) ─────────────────────────────
   Bright bold "?" that reveals its help in a popover on hover/focus/tap.

   The popover is a REAL ELEMENT (.qhelp-pop), not a ::after. It used to be a pseudo-element
   reading attr(data-tip), which was cheap and could never hold anything interactive — no
   report control, no real paragraphs, nothing addable later. */
.qhelp-wrap { position:relative; display:inline-flex; vertical-align:middle; }
.qhelp { display:inline-flex; align-items:center; justify-content:center; width:16px; height:16px;
  border-radius:9999px; border:1.5px solid currentColor; color:var(--color-base-content); opacity:.6;
  font-size:11px; font-weight:800; cursor:help; vertical-align:middle;
  line-height:1; text-transform:none; }
/* Accent, not primary, and NO outline:none. This one line had both of the faults the focus
   pass was opened to fix: indigo measures 3.4:1 on the dark theme, which 11px bold text does
   not get away with, and `outline:none` deleted the only focus indicator the chip had. */
.qhelp:hover, .qhelp:focus, .qhelp[aria-expanded="true"] {
  opacity:1; border-color:var(--ff-accent); color:var(--ff-accent); }
.qhelp:focus-visible { outline:2px solid var(--ff-accent); outline-offset:2px; }
/* Drops BELOW the chip so it never runs off the top of the screen; max-width keeps it inside
   narrow viewports. [aria-expanded="true"] is the TOUCH path — hover does not exist on a phone
   and this is a PWA used on touchlines (js/help-chip.js toggles it). */
.qhelp-pop { display:none; position:absolute; left:0; top:calc(100% + 6px); z-index:2000;
  width:400px; max-width:min(400px, 90vw); background:var(--color-base-100);
  color:var(--color-base-content); border:1px solid var(--color-base-300); border-radius:8px;
  padding:9px 12px; font-size:13.5px; font-weight:400; line-height:1.4; letter-spacing:0;
  text-align:left; box-shadow:0 8px 22px rgba(0,0,0,.4); cursor:auto; white-space:normal; }
.qhelp-wrap:hover .qhelp-pop,
.qhelp-wrap:focus-within .qhelp-pop,
.qhelp[aria-expanded="true"] + .qhelp-pop { display:block; }
.qhelp-pop p { margin:0 0 6px; }
.qhelp-pop p:last-of-type { margin-bottom:0; }
/* The report control sits under the text, separated — a safety net for the reader, not a call
   to action, so it must not compete with the help itself. */
.qhelp-pop .help-report { margin-top:7px; padding-top:6px;
  border-top:1px solid var(--color-base-300); width:100%; text-align:left; }

/* A chip inside a FLOATING label (.ss-form .ff > label) has to opt back into pointer events:
   the label sets pointer-events:none so clicks fall through to the input beneath it, which
   would otherwise make the chip unclickable. The chip must live inside the label rather than
   after it — the label is position:absolute, so a sibling chip drops out of its flow and lands
   under the input instead of beside the text. */
.ss-form .ff > label .qhelp-wrap { pointer-events:auto; }
.ss-form .ff > label .qhelp { cursor:help; }

/* ─── Softer system-wide semantics (Hugh: the orange + red are too harsh) ──────────
   Override DaisyUI's --color-warning (bright orange) → pale amber, and --color-error
   (neon red) → a calmer red. brand.css loads after output.css so these win. */
html[data-theme] {
  --color-warning: #fde68a;          /* pale amber, not bright orange */
  --color-warning-content: #78350f;  /* dark text on the pale warning */
  --color-error: #dc5b5b;            /* calmer red than the neon default */
  --color-error-content: #ffffff;
}

/* ═══════════════════════════════════════════════════════════════════════════════
   AUTHORING-FORM HOUSE STYLE  —  opt in with `.ss-form` on a wrapper/<form>.
   ═══════════════════════════════════════════════════════════════════════════════
   OWNS:   the field/label/section/button vocabulary for authoring forms.
   OPT-IN: every rule below is scoped to `.ss-form` (buttons excepted — `.gs` is a new
           name, verified unused). Nothing restyles until a form asks for it, so forms
           can be converted one at a time.
   WHY raw CSS: brand.css is NOT compiled by Tailwind, so this ships on a restart rather
           than an image rebuild — AND it cannot silently no-op the way an uncompiled
           utility does (`gap-x-8`/`gap-y-1` are absent from output.css: they render NO gap).
           Never express this layout in Tailwind utilities for that reason.
   DESIGN: fields are sized to the CONTENT they hold (a 2-digit day is not as wide as a
           street), the box becomes an underline, and the accent colour is a FOCUS signal
           only — a resting blue label on the dark theme is unreadable.
   ─────────────────────────────────────────────────────────────────────────────── */

/* Native controls must be told the colour scheme. `data-theme` is a DaisyUI attribute the
   BROWSER does not read, so <select> renders light-mode (black-on-black) on the dark theme.
   Global on purpose: this is a bug fix, not a style choice. */
[data-theme="dark"]  { color-scheme: dark; }
[data-theme="light"] { color-scheme: light; }

/* Controls read at the page's own body size (1rem/16px). They were .95rem, which is close
   enough to look like a rendering artefact rather than a decision — the picker's dropdown
   options inherit 16px from the body, so the text you were choosing FROM and the text you
   ended up WITH were different sizes. The resting label matches too, since it is the same
   text before it floats. */
/* ═══ DESIGN TOKENS — GLOBAL ══════════════════════════════════════════════════════
   These were declared on `.ss-form`, which meant the type scale and the colour roles were
   reachable ONLY inside an authoring form. Every other page in a ~46-app project had no
   access to the scale it was supposed to be consistent with, so "the house style" was a
   fence around one form rather than a system. That is the actual cause of the drift.

   It also broke a real rule: .qhelp is used site-wide and refers to --ff-accent, which
   resolved to nothing outside a form — so the help chip had no hover or focus colour on
   every page that is not this one.

   html[data-theme] rather than :root so the per-theme overrides further down (which must
   also be global) win by the same mechanism, without a specificity fight. */
html[data-theme] {
  /* ── TYPE SCALE — four steps, and no others ─────────────────────────────────────
     There were THIRTEEN distinct font sizes in this file, including .8 / .82 / .85 /
     .875 / .88rem — five values nobody can tell apart on screen but which all had to be
     remembered separately, which is exactly how two rows of the same control ended up
     different sizes. Every size below is one of these four. If a new size seems
     necessary, it almost certainly isn't.

     The VALUES now live in input.css's `@theme static` block, not here. That is what makes
     them Tailwind utilities as well (text-body, text-meta, …), so a template and a component
     name the same size instead of each inventing one. These four are aliases; changing a size
     means changing it in input.css and rebuilding, and both sides move together. */
  --fs-section: var(--text-section);  /* section headings */
  --fs-body:    var(--text-body);     /* controls and the values in them */
  --fs-meta:    var(--text-meta);     /* secondary text: checkbox labels, links, options */
  --fs-label:   var(--text-label);    /* field labels, hints, errors */

  /* ── TEXT COLOUR — one source, four steps ───────────────────────────────────────
     --ff-quiet is the SINGLE knob for every non-full-strength piece of text on the form.
     Change it here and every label, hint, floated label and muted string follows; it used
     to be five independent color-mix() expressions with no shared source, so recolouring
     "the small text" meant editing five declarations and missing some.

     --ff-accent names the gold that the sport caret already used. It was reaching for
     --color-warning, i.e. the semantic role meaning "warning" — so anything else adopting
     that gold would have been declaring itself a warning, and would break the day warning
     colours are retuned. A colour you want to reuse needs a name of its own.

     ALPHAS ARE SET FOR CONTRAST IN BOTH THEMES, not by eye. The previous values passed in
     dark and failed in light — label 3.40, hint 2.92 against white, i.e. below even the
     large-text floor — because nobody had opened this form in light mode. Measured after
     every change; see docs/claude/FORM_HOUSE_STYLE.md. */
  --ff-quiet:  var(--color-base-content);
  /* Accent defaults to the pale amber the caret already used — correct on the dark theme
     (12.72:1) and useless on the light one, where it measures 1.25:1 against white. Since it
     is now the FOCUS RING colour as well as the caret, an invisible accent means invisible
     focus, so it is overridden per theme below rather than left as one value. */
  --ff-accent: var(--color-warning);

  --ff-ink:   var(--color-base-content);
  --ff-muted: color-mix(in oklch, var(--ff-quiet) 82%, transparent);
  /* A disabled control is a STATE, not a quieter shade of the same thing, so it keeps its
     own token rather than being folded into --ff-muted. */
  --ff-disabled: color-mix(in oklch, var(--ff-quiet) 55%, transparent);

  --ff-line:  color-mix(in oklch, var(--ff-quiet) 34%, transparent);
  --ff-label: color-mix(in oklch, var(--ff-quiet) 82%, transparent);
  --ff-float: color-mix(in oklch, var(--ff-quiet) 82%, transparent);
  --ff-hint:  color-mix(in oklch, var(--ff-quiet) 74%, transparent);
  --btn-min:  8rem;
}

/* ─── KEYBOARD FOCUS ────────────────────────────────────────────────────────────────
   There were TWO :focus-visible rules in the whole house style, and none of them covered
   the controls this form is built from — .ff-action, the logo trigger, the crop dialog's
   buttons. A keyboard user could tab through the form with no idea where they were, which
   is WCAG 2.4.7 and not a preference.

   :focus-visible, not :focus, so a mouse click does not draw a ring — that is why the old
   pattern of styling :focus got removed by people who found it ugly, and then never came
   back. outline rather than border/box-shadow: it does not affect layout, so nothing
   reflows when focus moves, and it renders over neighbouring elements instead of being
   clipped by them.

   The accent is the ring colour: it is the highest-contrast token we have in both themes
   (12.72:1 on dark) and it already means "this is the interactive bit" on the caret. */
.ss-form :is(a, button, input, select, textarea, summary, [tabindex]):focus-visible,
.searchable-select-dropdown :is(button, [role="option"], [data-action]):focus-visible {
  outline: 2px solid var(--ff-accent);
  outline-offset: 2px;
  border-radius: .2rem;
}
/* NO EXCEPTIONS for the underlined fields, and that is a measured decision rather than a
   stylistic one. The tempting version of this rule suppressed the ring on .ff controls on the
   grounds that they "already show focus" via the underline. Measured, that underline was the
   ONLY indicator and it was not one:

     focused underline vs idle underline    dark 1.14:1     light 3.84:1

   WCAG 2.4.11 (AA in 2.2) wants at least 3:1 between the focused and unfocused states, so on
   the default theme the indicator was invisible. Even fixed (see --ff-accent on the underline
   below, which measures 4.28 / 3.16), a 1.5px line along one edge is well short of 2.4.11's
   other half — an area equal to a 2px perimeter of the control. The ring supplies that, so
   every focusable thing keeps it and the recoloured underline is the mouse-user bonus. */

/* rows + content-width tokens */
/* Space BETWEEN sections. Not a Tailwind gap utility: output.css is prebuilt here, so an
   uncommon step like gap-7 compiles to nothing and the sections silently collapse together.
   Adjacent-sibling margin instead — it cannot no-op, and it costs nothing before the first
   section or after the last. */
.ss-form .ff-sec + .ff-sec { margin-top:var(--sp-8); }

.ss-form .ff-row { display:flex; flex-wrap:wrap; column-gap:var(--sp-8); row-gap:var(--sp-5); align-items:flex-start; }
.ss-form .f-2ch{width:4.5rem}  .ss-form .f-3ch{width:5.25rem} .ss-form .f-4ch{width:6rem}
.ss-form .f-6ch{width:7.5rem}  .ss-form .f-mon{width:9rem}    .ss-form .f-name{width:12.5rem}
.ss-form .f-med{width:16rem}   .ss-form .f-wide{width:21rem}  .ss-form .f-max{width:100%}
.ss-form .f-half{width:50%}    .ss-form .f-two3{width:66%}
/* Sized in `ch` rather than rem because the requirement is literal: hold the LONGEST VALUE.
   The longest sport is "Middle/Long Distance" (20 chars), and a team name is asked to be
   about twenty characters, so both fields land on the same token — which is why the two
   controls line up. 22ch not 20ch: a select carries a dropdown arrow and a text input a
   caret, and neither is part of the character count. */
.ss-form .f-20ch{width:22ch}
/* A four-across row. f-med (16rem) x4 plus three 1.8rem gaps is 62.4rem against a 56rem
   container, so four never fit; f-geo is 20% narrower and the row's gap is tightened to
   1.2rem, giving 4x12.8 + 3x1.2 = 54.8rem. Used by Player Geography, where the four fields
   are one statement (citizenship, birth country, birth state, birth city) and reading them
   as three-then-one broke the sentence. */
.ss-form .f-geo{width:12.8rem}
.ss-form .ff-row--4up{column-gap:var(--sp-7)}
/* Content-width item on a field row. .ff reserves 1.05rem above each control for its floated
   label; anything without a label (a link, a chip) has to match that or it rides high against
   the inputs beside it. */
.ss-form .ff-top{padding-top:var(--ff-label-room)}

/* BASELINE for every control and label in the form, wrapped in .ff or not.
   The .ff rules below only reach controls inside a .ff wrapper, so sections that render a
   bare <select> (Attributes) fell through to DaisyUI's .select default of 14px — a size in
   no scale, sitting beside the fields it should match. Same for DaisyUI's .label, which sets
   its own size. A control is the body size because it is a control, not because someone
   remembered to wrap it. */
.ss-form input, .ss-form select, .ss-form textarea { font-size:var(--fs-body); }
.ss-form .label, .ss-form .label-text { font-size:var(--fs-body); }

/* the field
   WRAPPER-PROOF ON PURPOSE. Other scripts inject wrappers around our controls at runtime:
   password-toggle.js moves a password <input> into `div.pw-toggle-wrap` (to hang the eye
   button off it), and autocomplete.js rebuilds selects entirely. A child combinator
   (`.ff > input`) and an adjacent-sibling label (`input + label`) BOTH break the moment a
   wrapper appears — the field loses its underline and the label never floats. So: style by
   DESCENDANT, and drive the floating label from `:has()` on the .ff itself, which does not
   care how deeply the control is nested. */
/* padding-top reserves the floated label's own line. At .45rem the label (.7rem tall, sitting
   at top:0) ended up touching the value below it. The RESTING label offset has to move by the
   same amount, or the label stops sitting over the input it belongs to. */
.ss-form .ff { position:relative; padding-top:var(--ff-label-room); }
/* No label, no reserve. The 1.05rem above exists to hold a floated label; on a field
   whose name sits INSIDE the control there is nothing to hold, and the empty strip
   pushed it a line below the searchable pickers sharing its row. :has() means the rule
   follows the markup instead of needing a modifier class on every converted field. */
.ss-form .ff:not(:has(> label)) { padding-top:0; }
/* Options inherit the UA default (14px) rather than the control they belong to, so an open
   dropdown rendered a size that appears nowhere else on the page. */
.ss-form .ff select option, .ss-form select option { background:var(--color-base-100);
  color:var(--color-base-content); font-size:var(--fs-body); }
.ss-form .ff .pw-toggle-wrap { width:100%; }
/* line-height is stated on BOTH control families on purpose. Padding and font-size already
   matched, but a native <select> takes its height from the UA's own line box while a text
   input takes it from the font — so Club sat ~4px below the picker sharing its row. Fixing the
   line box is what actually equalises them. */
.ss-form .ff input, .ss-form .ff select, .ss-form .ff textarea {
  /* height:auto/min-height:0 override DaisyUI's .select, which the Django widget still carries
     and which forces a fixed control height. line-height alone could not win that, so the
     select stayed 40px against the picker's 32 and its underline sat 8px low. */
  width:100%; height:auto; min-height:0;
  padding:var(--sp-2) 0 var(--sp-2); font-size:var(--fs-body); font-family:inherit; line-height:1.5;
  color:var(--color-base-content); background:transparent; border:0; border-radius:0;
  border-bottom:1.5px solid var(--ff-line); outline:none; transition:border-color .18s ease;
}
.ss-form .ff textarea { resize:vertical; min-height:3.2rem; }
/* Number fields are TYPED here, not nudged. The UA spin buttons put a control inside the
   field that no other field has, they only appear on hover so the field changes shape under
   the cursor, and stepping 1-at-a-time to a roster of 24 is not a real interaction. The field
   still validates as a number and phones still get the numeric keypad. */
.ss-form .ff input[type="number"] { appearance:textfield; -moz-appearance:textfield; }
.ss-form .ff input[type="number"]::-webkit-outer-spin-button,
.ss-form .ff input[type="number"]::-webkit-inner-spin-button { -webkit-appearance:none; margin:0; }
/* Accent, not --color-primary. Indigo measured 1.14:1 against the idle underline on the dark
   theme — a state change nobody can see. The accent is 4.28 (dark) / 3.16 (light). Applies to
   :focus, not :focus-visible, so mouse users get the signal too; keyboard users also get the
   ring above. Every focus underline in this file uses this token — do not reintroduce primary. */
.ss-form .ff input:focus, .ss-form .ff select:focus, .ss-form .ff textarea:focus {
  border-bottom-color:var(--ff-accent); }
/* The label is ALWAYS up. There is no resting state and nothing that moves.

   It used to rest on the input line like a placeholder and float on focus/fill. That looks
   good on one field and falls apart across a form, because the label's position then encodes
   whether the field happens to hold a value: on a section whose fields were prefilled the
   first line of ink sat 16.8px below the heading, and on a section whose fields were empty it
   sat 40px below. Same markup, same spacing, visibly different gap — measured, not guessed.
   You cannot align both states at once; pulling the empty row up puts the label through the
   heading the moment somebody types.

   So the state goes away. Every field starts its ink at the same place as a checkbox row
   (~17px), the gap can never depend on prefill, and the label cannot collide with anything.

   It also deletes the machinery: `:placeholder-shown` (which was once inverted, so labels
   rendered on top of placeholder text), the `.is-filled` class that JS had to keep in sync,
   and the :-webkit-autofill special case that existed only because autofill does not clear
   :placeholder-shown. Three sources of bugs removed by not having the state at all.

   Fields whose placeholder IS their heading (Team Name, Name Schedule) have no <label> and
   are handled by the `:not(:has(> label))` rule above — they keep the in-field look. */
.ss-form .ff > label {
  position:absolute; left:0; top:0; font-size:var(--fs-label); letter-spacing:.03em;
  pointer-events:none; color:var(--ff-float); transition:color .16s ease; }
/* Focused label goes BRIGHT, not blue. --color-primary is indigo, and indigo text on the
   dark theme is unreadable (the same rule that bans text-primary for body/link text). The
   focus signal is full-strength base-content on the label plus the primary UNDERLINE. */
.ss-form .ff:has(input:focus) > label,
.ss-form .ff:has(select:focus) > label,
.ss-form .ff:has(textarea:focus) > label { color:var(--color-base-content); }

/* Live validation drives the underline, straight from the swapped-in markup — the server
   returns .validation-success / .validation-error inside the field's hint div, so no JS and
   no extra classes are needed. Full-strength colours: the muted mix was barely visible. */
/* Error INK is deliberately brighter than --color-error. The global error colour was calmed
   down (#dc5b5b) for fills and alerts, but as small text on the near-black theme it has too
   little contrast to read. Fills keep the calm red; text and rules get the bright one. */
/* Accent per theme. Same role, same meaning, legible on both — a pale amber reads on
   near-black and disappears on white, so light gets a deep amber instead. */
[data-theme="dark"]  { --ff-accent:#fde68a; }
[data-theme="light"] { --ff-accent:#7a5300; }
[data-theme="dark"]  .ss-form { --ff-err-ink:#ff7b7b; }
[data-theme="light"] .ss-form { --ff-err-ink:#c62828; }

.ss-form .ff:has(.validation-success) input { border-bottom-color:var(--color-success); }
.ss-form .ff:has(.validation-error)   input { border-bottom-color:var(--ff-err-ink); }
.ss-form .ff:has(.validation-success) .ff-hint { color:var(--color-success); }
.ss-form .ff:has(.validation-error)   .ff-hint { color:var(--ff-err-ink); }

/* Reserve a line for the hint so a field doesn't jump (and shove the submit button around)
   the moment validation text appears. */
.ss-form .ff-hint, .ss-form .ff-err { min-height:.9rem; }
/* a <select> always holds a value, so its label rests in the floated position */
.ss-form .ff.ff-select > label { top:0; font-size:var(--fs-label); letter-spacing:.03em; }

/* Chrome paints an autofill background that beats `background:transparent`. The reliable
   mask is a huge inset shadow in the surface colour (the long transition alone does NOT
   work in current Chrome), plus text-fill-color so the value stays legible. */
.ss-form .ff input:-webkit-autofill,
.ss-form .ff input:-webkit-autofill:hover,
.ss-form .ff input:-webkit-autofill:focus {
  -webkit-box-shadow:0 0 0 100px var(--color-base-100) inset;
  box-shadow:0 0 0 100px var(--color-base-100) inset;
  -webkit-text-fill-color:var(--color-base-content);
  caret-color:var(--color-base-content);
  transition:background-color 9999s ease-out;
}

/* states */
.ss-form .ff-req::after { content:"*"; color:var(--ff-err-ink); margin-left:var(--sp-1); }
.ss-form .ff-hint { font-size:var(--fs-label); margin-top:var(--sp-1); line-height:1.2; color:var(--ff-hint); }
.ss-form .ff-err  { font-size:var(--fs-label); margin-top:var(--sp-2); color:var(--ff-err-ink); }
.ss-form .ff.has-error input, .ss-form .ff.has-error select { border-bottom-color:var(--ff-err-ink); }
.ss-form .ff.has-error > label { color:var(--ff-err-ink); }
/* Server-rendered field errors land in the hint div (it doubles as the hx-target for live
   validation). Without this they'd print in muted grey and read as a hint, not a failure —
   which is exactly how a rejected signup looked like a page that "just redrew". */
.ss-form .ff.has-error .ff-hint { color:var(--ff-err-ink); }
/* Live validation OUTRANKS a stale server error. `.has-error` is stamped on the server when a
   POST is rejected and nothing ever removes it, so a field the user has since fixed kept its
   red underline while the hint underneath said "✅ Strong password" — a red rule with no error
   anywhere on screen. These rules come after the .has-error block on purpose: same
   specificity, later wins, so a success from the validation endpoint clears both the underline
   and the superseded server message. */
.ss-form .ff.has-error:has(.validation-success) input { border-bottom-color:var(--color-success); }
.ss-form .ff.has-error:has(.validation-success) > label { color:var(--ff-float); }
.ss-form .ff:has(.validation-success) .ff-err { display:none; }
.ss-form .ff.is-ok > input { border-bottom-color:color-mix(in oklch, var(--color-success) 70%, transparent); }
.ss-form .ff > input:disabled, .ss-form .ff > select:disabled {
  color:var(--ff-disabled);
  border-bottom-style:dashed; cursor:not-allowed; }

/* ── components/searchable_select.html inside an .ss-form ────────────────────────
   The shared picker ships DaisyUI `select select-bordered` on its visible input and
   renders its OWN <label>, so it can't use the floating-label pattern. Give it the same
   underline and the same small caption instead, scoped to .ss-form so every other page
   that uses this component is untouched.
   The dropdown is PORTALED to <body> by searchable-select.js, so `.ss-form .searchable-
   select-dropdown` would never match — the script copies the `--select-*` custom
   properties across, so theming has to go through those vars, set on the wrapper. */
.ss-form .searchable-select-field > label {
  display:block; padding:0; margin-bottom:var(--sp-1); font-size:var(--fs-label); font-weight:600;
  letter-spacing:.03em; color:var(--ff-float); }
.ss-form .searchable-select-wrapper { width:100%;
  --select-dropdown-bg: var(--color-base-100);
  --select-dropdown-border: color-mix(in oklch, var(--color-base-content) 15%, transparent);
  --select-option-hover-bg: color-mix(in oklch, var(--color-base-content) 10%, transparent);
  --select-option-selected-bg: color-mix(in oklch, var(--color-success) 18%, transparent);
  --select-option-selected-text: var(--color-base-content);
  --select-child-bg: transparent;
  --select-child-text: color-mix(in oklch, var(--color-base-content) 75%, transparent); }
.ss-form .searchable-select-input {
  height:auto; min-height:0; border:0; border-radius:0; box-shadow:none; outline:none;
  background:transparent; color:var(--color-base-content);
  border-bottom:1.5px solid var(--ff-line); padding:var(--sp-2) var(--sp-7) var(--sp-2) 0;
  font-size:var(--fs-body); font-family:inherit; line-height:1.5; transition:border-color .18s ease; }
.ss-form .searchable-select-input:focus { border-bottom-color:var(--ff-accent); }
.ss-form .searchable-select-input:disabled {
  border-bottom-style:dashed; cursor:not-allowed;
  color:var(--ff-disabled); }

/* grouped small inputs (e.g. month / day / year) */
.ss-form .ff-group { margin-top:var(--sp-1); }
.ss-form .ff-group > .ff-group-label { display:block; font-size:var(--fs-label); letter-spacing:.03em;
  font-weight:600; margin-bottom:var(--sp-3); color:var(--ff-float); }
.ss-form .ff-group-inline { display:flex; column-gap:var(--sp-6); align-items:flex-start; }

/* section head: a rule, not a card.
 *
 * GLOBAL, not `.ss-form .sec-head`. It was fenced inside .ss-form, which meant a section
 * heading — the most obviously reusable thing in the whole file — could only be used on an
 * authoring form. A record page needing one had no choice but to build a lookalike, and one
 * duly appeared (`.ss-card-head`, on the profile) that matched the size but drew its rule as
 * a border instead of a flex ::after. Two classes, one job, guaranteed to drift.
 *
 * This is the SAME mistake the type-scale comment at the top of this block describes: scoping
 * a shared thing to one surface "was a fence around one form rather than a system… the actual
 * cause of the drift". Unfencing it costs nothing: nothing outside a form used the class,
 * so no existing page changes. */
.sec-head { display:flex; align-items:center; gap:var(--sp-5); margin:0 0 var(--sp-6); }
/* Title Case, not ALL CAPS. Shouting a heading costs legibility (caps kill word-shape, which
   is what fast readers actually match on) and buys nothing here, because the rule beside it
   already does the separating. The markup was always Title Case — only this line was
   uppercasing it — so the fix is to stop, not to retype every heading. */
.sec-head h3 { font-size:var(--fs-section); font-weight:700; letter-spacing:.02em;
  white-space:nowrap; color:var(--color-base-content); }
.sec-head::after { content:""; flex:1; height:1px;
  background:color-mix(in oklch, var(--color-base-content) 14%, transparent); }
/* An action that belongs to the section (+ Add sport, + Create) sits AFTER the rule, hard
   right, instead of crowding the title. order:1 puts it past the ::after, which is otherwise
   always the last child. */
.sec-head > .sec-action { order:1; flex:0 0 auto; }

/* ── A section that COLLAPSES: <details class="ss-sec"><summary class="sec-head"> ──
 *
 * Same heading, same rule, same right-aligned action — it just gains a caret and a click
 * target. Nothing changes for the sections that stay open, because all of this is scoped to
 * summary.sec-head.
 *
 * The chevron geometry is copied from .nav-folder-head above deliberately: the sidebar already
 * taught this app what "a thing that opens" looks like, and a second caret drawn differently
 * would be the toggle problem again in miniature.
 *
 * list-style + ::-webkit-details-marker both needed — Safari draws its own triangle from the
 * second, everyone else from the first. */
summary.sec-head { cursor:pointer; list-style:none; }
summary.sec-head::-webkit-details-marker { display:none; }
summary.sec-head::before { content:""; width:.42rem; height:.42rem; flex:0 0 auto;
  border-right:1.5px solid currentColor; border-bottom:1.5px solid currentColor;
  transform:rotate(-45deg); transition:transform .15s ease; opacity:.7; }
details[open] > summary.sec-head::before { transform:rotate(45deg); }
summary.sec-head:hover::before { opacity:1; }
/* The action inside a summary must not toggle the section when clicked — a link that both
   navigates and collapses the thing behind it is a control that does two jobs badly. */
summary.sec-head > .sec-action { position:relative; z-index:1; }
.ss-form .ff-actions { margin-top:var(--sp-6); display:flex; gap:var(--sp-4); flex-wrap:wrap; }
/* A field COLUMN that lines up with a `.gs-narrow` submit button: same 67% width, same
   centring, so the inputs sit directly over the button instead of running the full width of
   the card while the button is inset. Fields inside stay f-max — the column owns the width. */
.ss-form .ff-col { width:67%; margin-inline:auto; }

/* Alignment for the EXISTING autocomplete components (address/components/*_select.html).
   autocomplete.js emits: .relative wrapper > <input class="input w-full pr-8">, a .ac-dropdown
   panel, and on selection adds .pl-16 plus an icon box `.absolute.inset-y-0.left-0 … pl-3 pr-2`
   holding an `h-5 w-8` flag = .75+2+.5 = 3.25rem (hence pl-16). We only restyle it — no JS change. */
.ss-form .ff-geo .form-control > .label { padding-bottom:var(--sp-1); }
.ss-form .ff-geo .label-text { font-size:var(--fs-label); letter-spacing:.03em; font-weight:600; color:var(--ff-float); }
.ss-form .ff-geo input.input { height:auto; min-height:0; border:0; border-radius:0; background:transparent;
  border-bottom:1.5px solid var(--ff-line); padding:var(--sp-3) var(--sp-8) var(--sp-2) 0; font-size:var(--fs-body); }
.ss-form .ff-geo input.input:focus { box-shadow:none; border-bottom-color:var(--ff-accent); }
.ss-form .ff-geo .relative > div.absolute.inset-y-0 { padding-left:0; padding-right:var(--sp-3); }
.ss-form .ff-geo input.input.pl-16 { padding-left:var(--ff-flag-inset); }
.ss-form .ff-geo .ac-dropdown { font-size:var(--fs-meta); border-radius:.55rem;
  border-color:color-mix(in oklch, var(--color-base-content) 15%, transparent); }

/* ─── GO / STOP buttons ──────────────────────────────────────────────────────────
   Green = go, red = stop (the standing convention). Quiet at rest — a coloured rim
   over the page surface — and the face COMMITS to the colour on hover/focus. No idle
   motion: animated rims read as decoration and tire on the eleventh form; keep those
   for a hero CTA. `min-width` (not a fixed width) keeps Save and Cancel the same size
   while still fitting longer labels.
   Markup: <button class="gs"><span>Save</span></button>  — the rim is the button, the
   <span> is the face; the two-element structure is what allows rim ≠ fill.           */
.gs { --gs:var(--color-success); --gs-ink:var(--color-success-content); --btn-min:8rem;
  position:relative; display:inline-block; padding:1.5px; border:0; border-radius:999px;
  cursor:pointer; font:inherit; line-height:1; background:color-mix(in oklch, var(--gs) 55%, transparent); }
.gs-stop { --gs:var(--color-error); --gs-ink:var(--color-error-content); }
.gs > span { display:block; border-radius:inherit; background:var(--color-base-100);
  color:var(--color-base-content); min-width:var(--btn-min); text-align:center;
  padding:var(--sp-4) var(--sp-8); font-size:var(--fs-meta); font-weight:650; letter-spacing:.03em;
  transition:background .16s ease, color .16s ease, transform .08s ease; }
.gs:hover > span, .gs:focus-visible > span { background:var(--gs); color:var(--gs-ink); }
.gs:active > span { transform:scale(.975); }
.gs:focus-visible { outline:2px solid color-mix(in oklch, var(--gs) 70%, transparent); outline-offset:3px; }

.gs[disabled] { opacity:.45; pointer-events:none;
  background:color-mix(in oklch, var(--color-base-content) 22%, transparent); }
.gs[disabled] > span { color:var(--ff-muted); }

/* `.gs-gated` — for buttons that start DISABLED and unlock once a form validates
   (Create account, Save on a wizard step…). These break the quiet-at-rest rule ON PURPOSE:
   the fill IS the message that the form is now valid to submit, so the state change is
   visible without hovering. Plain `.gs` keeps the calm resting style — a button that was
   always pushable has no state change to announce.
   Disabled → flat grey · Enabled → SOLID green · Hover → deepen slightly. */
.gs-gated:not([disabled]) > span { background:var(--gs); color:var(--gs-ink); }
.gs-gated:not([disabled]):hover > span { filter:brightness(1.08); }

/* FORM IS READY → the submit button fills green immediately, without waiting for a hover.
   Driven purely by the validation markup the server already swaps in: the confirm-password
   check has passed AND nothing anywhere is in an error state. No JS, so it works on the
   standalone page too (which never loads the modal's button-gating script).
   `:not([disabled])` keeps a still-gated button grey. */
.ss-form:has(#password-confirm-validation .validation-success):not(:has(.validation-error)) .gs:not([disabled]) > span {
  background:var(--gs); color:var(--gs-ink);
}
/* ARMED → every required field in the form holds a value, so the button fills green to say
   "this is submittable now". Generic on purpose: `:not(:has(input:required:placeholder-shown))`
   means "no required input is still empty" — the fields carry placeholder=" " for the floating
   label, so :placeholder-shown IS "empty". Opt in by putting `gs-arm` on the <form>; nothing
   fills until a form asks for it. Chrome autofill counts, since it sets a real value.
   Login has no live validation to wait for, which is why it needs its own signal. */
.ss-form form.gs-arm:not(:has(input:required:placeholder-shown)) .gs:not([disabled]) > span {
  background:var(--gs); color:var(--gs-ink);
}

.gs-block, .gs-block > span { width:100%; }
/* Two thirds of the row, centred — a full-width bar reads as a page action; the login button
   is a control inside a card. Same height and label, 33% less width. */
.gs-block.gs-narrow { display:block; width:67%; margin-inline:auto; }

@media (prefers-reduced-motion: reduce) { .ss-form *, .gs, .gs > span { transition:none !important; } }

/* Small shared form bits used by the auth screens (still `.ss-form`-scoped). */
.ss-form .ff-formerr { border:1px solid var(--color-error); border-radius:.5rem; padding:var(--sp-4) var(--sp-5);
  margin-bottom:var(--sp-6); font-size:var(--fs-meta); color:var(--color-error);
  background:color-mix(in oklch, var(--color-error) 10%, transparent); }
.ss-form .ff-between { display:flex; align-items:center; justify-content:space-between; gap:var(--sp-6);
  flex-wrap:wrap; margin-top:var(--sp-6); }
/* ── .ff-check is RETIRED. The app has ONE switch: `toggle toggle-sm toggle-success`. ──
 *
 * It was a hand-built pill, and it lost to DaisyUI on the only measure that decides these
 * things in practice: reach. 2 call sites against ~38, because it was scoped `.ss-form
 * .ff-check` and therefore did not RENDER outside an authoring form. On a table or a detail
 * page it silently degraded to a bare checkbox, so anyone working there copied the
 * neighbouring DaisyUI switch — and the neighbour's colour and size came with it. That is
 * how one app ended up with five toggle colours in three sizes.
 *
 * The lesson is not "DaisyUI is better". It is that a component nobody can use everywhere is
 * not a standard, it is a local dialect, and scoping a shared thing to one surface is the
 * same mistake .sec-head made.
 *
 * The rule is enforced now, not merely written down: tools/toggle_audit.sh fails on any
 * toggle class outside the canonical string, and /styleguide documents it. Layout for the
 * label+switch row is .ss-check below; the switch itself is DaisyUI's.
 *
 * Nothing here is left behind on purpose — a dead rule that still renders is exactly what
 * lets a retired component creep back. */
/* An inline text action inside a form — "Edit", "Upload Logo". One class instead of each
   call site re-deciding between text-xs and text-sm and whether to underline. Underline, not
   colour: indigo text is unreadable on the dark theme (see the focus-label note above). */
.ss-form .ff-action { font-size:var(--fs-meta); text-decoration:underline; cursor:pointer;
  color:var(--ff-ink); background:none; border:0; padding:0; }
.ss-form .ff-action:hover { opacity:.7; }
/* The logo row's label sits at the same secondary size as the action beside it. */
.ss-form .logo-hover { font-size:var(--fs-meta); }
/* .ff-action fades the whole control on hover. The preview panel lives INSIDE this control, so
   that faded the preview too — the form behind it showed straight through the logo, which read
   as a broken image rather than a dimmed one. Fade the WORD, not the control. Opacity on a
   parent cannot be undone by a child, so the reset has to happen here. */
.ss-form .logo-hover.ff-action:hover { opacity:1; }
.ss-form .logo-hover.ff-action:hover .logo-label { opacity:.7; }

.ss-form .ff-link { font-size:var(--fs-meta); color:var(--ff-muted);
  text-decoration:none; border-bottom:1px solid transparent; }
.ss-form .ff-link:hover { color:var(--color-base-content); border-bottom-color:currentColor; }

/* ─── Sidebar folders ────────────────────────────────────────────────────────────
   The sidebar's `section` captions became collapsible <details> (navigation/
   sidebar_menu.html). Styled to read as a folder heading, not a control: the caption
   keeps the small uppercase treatment it always had, plus a chevron that turns. */
.nav-folder { margin-bottom:var(--sp-1); }
.nav-folder-head { list-style:none; cursor:pointer; display:flex; align-items:center; gap:var(--sp-3);
  padding:var(--sp-3) var(--sp-3); border-radius:.5rem; font-size:var(--fs-label); font-weight:600;
  letter-spacing:.06em; text-transform:uppercase;
  color:var(--ff-muted); }
.nav-folder-head::-webkit-details-marker { display:none; }   /* Safari's default triangle */
.nav-folder-head::before { content:""; width:.42rem; height:.42rem; flex:0 0 auto;
  border-right:1.5px solid currentColor; border-bottom:1.5px solid currentColor;
  transform:rotate(-45deg); transition:transform .15s ease; }
.nav-folder[open] > .nav-folder-head::before { transform:rotate(45deg); }
.nav-folder-head:hover { background:color-mix(in oklch, var(--color-base-content) 8%, transparent);
  color:var(--color-base-content); }
.nav-folder-body { padding-left:var(--sp-3); margin-bottom:var(--sp-3); }
/* Fallback highlight for a link the SERVER did not mark selected — a second copy of the
   current page in another folder, or a query-string URL simple_menu's path match never
   sees (the dev folder's /?auth=… entries). When the server did select something, the
   script copies that item's classes instead and this never applies. */
.nav-link-active { background:var(--color-base-300); color:var(--color-base-content); }

/* Auth screens: a centred, calm card (replaces the old white/green "sports-card"). */
.ss-auth { min-height:100dvh; display:flex; align-items:center; justify-content:center; padding:var(--sp-6); }
/* …and the page must not scroll. `.ss-auth` is 100dvh INSIDE `.sports-container`, whose 20px
   top+bottom padding (accounts/base.html inline <style>) pushed the document 40px past the
   viewport — a scrollbar on every auth page regardless of how short the form is. Specificity
   (0,2,0) beats the plain `.sports-container` rule, so load order does not matter. */
.sports-container:has(.ss-auth) { padding:0; max-width:none; }
.ss-auth-card { width:100%; max-width:26rem; background:var(--color-base-100); border-radius:1rem;
  padding:var(--sp-8) var(--sp-8) var(--sp-7); box-shadow:0 18px 40px -18px rgba(0,0,0,.5);
  border:1px solid color-mix(in oklch, var(--color-base-content) 10%, transparent); }
.ss-auth-card h1 { font-size:1.2rem; font-weight:700; letter-spacing:.01em; margin-bottom:var(--sp-2); }
.ss-auth-sub { font-size:var(--fs-meta); margin-bottom:var(--sp-8);
  color:var(--ff-muted); }
.ss-auth-foot { margin-top:var(--sp-5); padding-top:var(--sp-4); font-size:var(--fs-meta); text-align:center;
  border-top:1px solid color-mix(in oklch, var(--color-base-content) 12%, transparent);
  color:var(--ff-muted); }

/* ── Auth-screen density fixes ──────────────────────────────────────────────────
   A 5-field signup form should not need scrolling. */
.ss-form .ff-hint-auto { min-height:0; }          /* fields with no validation endpoint */
.ss-auth-card h1.ss-auth-tight { margin-bottom:var(--sp-4); }

/* Turnstile renders with data-appearance="interaction-only" (set in CustomSignupForm), so it
   occupies NO space unless Cloudflare actually needs to challenge. No scaling hacks needed —
   the wrapper collapses to nothing, and expands on its own if a challenge appears. */
.ss-form .ff-turnstile { margin-top:0 !important; }
.ss-form .ff-turnstile:empty { display:none; }

/* Links must LOOK like links — muted grey with no underline read as plain text. */
.ss-form .ff-link, .ss-auth-foot a { color:var(--color-base-content); text-decoration:underline;
  text-underline-offset:3px; text-decoration-thickness:1px; border-bottom:0; }
.ss-form .ff-link:hover, .ss-auth-foot a:hover { color:var(--color-success); }

/* Draggable modal header — see static/js/draggable-modal.js. Cursor is the only affordance
   the user gets, so it matters: grab at rest, grabbing while held. */
[data-drag-handle] { cursor:grab; touch-action:none; }
[data-drag-handle]:active { cursor:grabbing; }

/* ── Address composer ───────────────────────────────────────────────────────────
   Lifted verbatim out of the page-local <style> block in address/form_v3.html and
   re-scoped from `#addr3` (an ID, so page-only) to `.addr-composer` (a class), so the
   SAME composer renders identically on the page and inside the draggable modal.

   This is not cosmetic tidying — it is load-bearing. Seven server-rendered step
   partials (v3/_picked, _manual, _details, _city_box, _city_results, _search_results,
   _cancel) use .drop / .drop-list / .drop-item / .grid2 / .stepnum and are htmx-injected
   AFTER first paint. Scoped to an ID that only the page carried, they rendered unstyled
   in any other host — and, being server-rendered mid-flow, not until you clicked past
   step 1.

   NOT YET the .ff floating-label house style: these fields are still plain DaisyUI
   .input/.select, which `.ss-form` does not target. Converting the seven partials to .ff
   is a separate pass; until then these rules keep the density they had. */
.addr-composer .input, .addr-composer .select { height:2.25rem; min-height:2.25rem; font-size:.875rem; }
.addr-composer .label { padding:var(--sp-1) 0; }
.addr-composer .label-text { font-size:.8rem; }
.addr-composer #a3-type:required:invalid { border-color:var(--color-error, #dc2626) !important; }
.addr-composer #id_visibility { max-width:16rem; }

.addr-composer .grid2 { display:grid; grid-template-columns:1fr 1fr; gap:var(--sp-5); }
@media (max-width:640px) { .addr-composer .grid2 { grid-template-columns:1fr; } }

/* Type-ahead dropdown shared by the address search and the city picker. */
.addr-composer .drop { position:relative; }
.addr-composer .drop-list { position:absolute; z-index:60; left:0; right:0; margin-top:var(--sp-2);
  background:var(--color-base-100); border:1px solid var(--color-base-300);
  border-radius:.5rem; box-shadow:0 10px 25px rgba(0,0,0,.15); max-height:18rem; overflow-y:auto; }
.addr-composer .drop-list:empty { display:none; }
.addr-composer .drop-item { display:block; width:100%; text-align:left; padding:var(--sp-3) var(--sp-4); font-size:.85rem; }
.addr-composer .drop-item:hover { background:var(--color-base-200); }

/* Step numerals — "1 Find your address", "2 Confirm", "3 Details". */
.addr-composer .stepnum { display:inline-flex; width:1.4rem; height:1.4rem; align-items:center;
  justify-content:center; border-radius:9999px; background:var(--color-primary); color:#fff;
  font-size:var(--fs-label); font-weight:700; margin-right:var(--sp-3); }

/* Toolbar-sized .gs — the standard button is built for a form footer (min-width 8rem,
   .7rem/1.6rem padding) and towers over a row of btn-sm controls when a primary action sits
   in a table toolbar. Drops the min-width and matches DaisyUI's btn-sm height. */
.gs-sm { --btn-min:0; }
.gs-sm > span { padding:var(--sp-3) var(--sp-5); font-size:var(--fs-meta); letter-spacing:.02em; }

/* ── Team / competition authoring sections ──────────────────────────────────────
   These forms are Django-widget rendered: the label is a bare `<label class="label">`,
   not the `.label-text` span the .ff-geo rules target, so without this they keep DaisyUI's
   default label and sit at a different size and weight from every converted field beside
   them. Same values as `.ff-geo .label-text` on purpose — one label look per form.

   `.ff-sec` is a plain <section>, not a card: the heading rule (.sec-head) is the divider,
   which is why nine stacked sections read as one form rather than nine boxes. */
.ss-form .ff-sec label.label { display:block; padding:0 0 var(--sp-1); font-size:var(--fs-label);
  letter-spacing:.03em; font-weight:600; color:var(--ff-float); }
.ss-form .ff-sec label.label.font-semibold { font-weight:600; }

/* Logo control — the facility form's pattern (templates/venues/create_v3.html), lifted out of
   that page's `#venv3` scope so every authoring form can use it. The logo is deliberately NOT
   shown inline: a form is for entering values, and a 96px thumbnail sitting in the middle of a
   field row costs more vertical space than the information is worth. Hover the label to see it. */
/* The portaled dropdown carries the field's font-size as an inline style (see
   searchable-select.js). Its rows must INHERIT that rather than restate a size, or the copy
   is pointless. Unscoped because the panel lives on <body>, outside .ss-form. */
/* Size the picker's own subtree, not just the control inside it. The wrapper and the panel
   are plain <div>s, so `.ss-form input, select, textarea` never reached them: a CLOSED panel
   sat at the inherited 16px and only became correct once opened and portaled (where the JS
   copies the field's size across). Setting it here means the panel is right before it is ever
   shown, and the JS copy only has to survive the move to <body>. */
.ss-form .searchable-select-field,
.ss-form .searchable-select-wrapper,
.ss-form .searchable-select-dropdown { font-size:var(--fs-body); }

.searchable-select-dropdown .searchable-select-option,
.searchable-select-dropdown [data-action="create"],
.searchable-select-dropdown .searchable-select-empty { font-size:inherit; font-weight:inherit; }

/* AutocompleteSelect renders its "+ Add" row with text-primary. Indigo on the near-black
   theme is the one colour banned everywhere else on this form, and the component's classNames
   cannot be partially overridden (config merging is shallow — passing one key drops the rest),
   so it is corrected here instead. Scoped to .ss-form: other pages keep the component default. */
.ss-form [data-action="create"] { color:var(--color-base-content); text-decoration:underline; }
.ss-form [data-action="create"]:hover { background:var(--color-base-200); color:var(--color-base-content); }

.ss-form .logo-hover { position:relative; }
/* Opens DOWNWARD. It used to open upward (bottom:135%), which was fine while the logo sat in
   the middle of the form — but the control now lives on the first row, and a 333px panel above
   a trigger at y=133 renders at y=-207: entirely off the top of the viewport, so hovering
   appeared to do nothing. Below the trigger there is always the rest of the form to open into. */
.ss-form .logo-hover .logo-pop { display:none; position:absolute; z-index:70;
  top:calc(100% + var(--sp-3)); left:0;
  padding:var(--sp-3); border:1px solid var(--ff-line); border-radius:.4rem;
  background:var(--color-base-100); box-shadow:0 6px 20px rgb(0 0 0 / .35); }
/* Only pops when there IS something to show — data-has is set by the file picker, so this
   lights up for a just-chosen file as well as a saved one. Without the guard, hovering an
   empty logo field opened a blank box. */
.ss-form .logo-hover:hover .logo-pop[data-has="1"] { display:block; }
/* Deliberately oversized: the point of the hover is to check you picked the right image,
   and a 180px thumbnail of a crest is too small to tell two team badges apart. */
.ss-form .logo-pop img { max-width:320px; max-height:320px; object-fit:contain; display:block; }

/* Checkbox grid — replaces the languages <select multiple>, and with it a pile of CSS that
   existed only to make a list box behave (appearance:none to stop DaisyUI collapsing it to one
   line, height:auto to get the real row count, min-height to stop it jumping). None of that is
   needed once the options are checkboxes.

   auto-fill rather than a fixed column count, so it reflows with the container instead of
   needing a breakpoint per width; the 9rem floor keeps a long language name off two lines. */
/* NOT .ff-check — that name is already taken by the house TOGGLE SWITCH (login's "keep me
   signed in", sport details), which restyles input[type=checkbox] into a sliding pill. Reusing
   it here turned ten languages into ten toggles. These are .ff-opt: real checkboxes, because a
   list of languages is a set you tick, not ten independent on/off switches. */
.ss-form .ff-opts--wide { --opt-min:13rem; }
.ss-form .ff-opts { display:grid; grid-template-columns:repeat(auto-fill, minmax(var(--opt-min, 9rem), 1fr));
  gap:var(--sp-1) var(--sp-6); margin-top:var(--sp-2);
  /* The grid is the last thing in its section and sat hard against the next section's
     heading rule, so two unrelated blocks read as one. */
  margin-bottom:var(--sp-5); }
.ss-form .ff-opt { display:flex; align-items:center; gap:var(--sp-3); font-size:var(--fs-meta);
  cursor:pointer; padding:var(--sp-1) 0; }
.ss-form .ff-opt span { line-height:1.2; }
/* ─── The house checkbox: ONE definition ────────────────────────────────────────────
   Size and colour were being restated at every call site — "checkbox checkbox-xs
   checkbox-success" on six widgets in teams/forms.py and again on every language box in the
   template — which is exactly how two sets of tickboxes on one page ended up different sizes.
   DaisyUI v5 drives both through custom properties (--size, --input-color), so setting them
   once here means a call site only has to say `class="checkbox"` and gets the house look.

   Applies automatically inside .ss-form; use .ss-check to get the same checkbox anywhere else
   on the site. Destructive ones keep checkbox-error and go red — that distinction is meaning,
   not styling. (The toggle SWITCH is not here at all: it is DaisyUI's
   `toggle toggle-sm toggle-success`, one string app-wide — see the retirement note on
   .ff-check above and the Toggles section of /styleguide.) */
.ss-form input.checkbox,
.ss-check {
  --size: calc(var(--size-selector, .25rem) * 4);
  --input-color: var(--color-success);
  color: var(--color-success-content);
}
.ss-form input.checkbox.checkbox-error,
.ss-check.checkbox-error { --input-color: var(--color-error); }

/* Square, not round — and for EVERY checkbox on the form, not just the language grid.
   DaisyUI v5 derives the control's corner from --radius-selector, which is pill-shaped on this
   theme, so checkboxes came out circular. A circle means "pick exactly one" to anyone who has
   used a form, which is wrong for all of these.
   Targeting input.checkbox rather than input[type=checkbox] on purpose: it catches the widget
   classes Django emits (Status, Languages, delete-logo). Nothing needs excluding any more:
   the switch is DaisyUI's `toggle`, which this selector never matched. */
.ss-form input.checkbox,
.ss-check { border-radius:.25rem; }

/* ─── Sport picker: make the caret look like the control it is ──────────────────────
   The caret is what opens a category's sub-sports, but it read as decoration: a small grey
   square the same size as the text beside it. Widened horizontally so it is obviously a
   button rather than a glyph, and outlined in amber for contrast against the near-black
   dropdown — the one place a strong colour earns its keep here, because it is pointing at
   the only interactive thing in the row.

   NOT scoped to .ss-form on purpose: the dropdown is portaled to <body> (see
   searchable-select.js), so it is not inside the form at all. It is also the same control
   with the same meaning on every picker, so it should look the same everywhere.

   Overrides output.css, which is prebuilt and cannot be rebuilt here — including its
   expanded state, which turned the caret BLUE (#60a5fa/#1e3a8a) on the dark theme. */
.searchable-select-carat {
  width: 2.1rem;
  height: 1.35rem;
  padding:var(--sp-1) var(--sp-3);
  border: 1.5px solid var(--color-warning);
  border-radius: .3rem;
  color: var(--color-warning);
  background: color-mix(in oklch, var(--color-warning) 10%, transparent);
}
.searchable-select-carat:hover {
  border-color: var(--color-warning);
  background: color-mix(in oklch, var(--color-warning) 22%, transparent);
}
/* Expanded: filled, so "this one is open" is readable at a glance down the list. */
.searchable-select-option--parent.is-expanded .searchable-select-carat,
[data-theme="dark"] .searchable-select-option--parent.is-expanded .searchable-select-carat {
  border-color: var(--color-warning);
  background: color-mix(in oklch, var(--color-warning) 32%, transparent);
  color: var(--color-warning);
}
[data-theme="dark"] .searchable-select-carat { border-color: var(--color-warning); }
[data-theme="dark"] .searchable-select-carat:hover {
  border-color: var(--color-warning);
  background: color-mix(in oklch, var(--color-warning) 22%, transparent);
}


/* ─── .ss-onesize — one text size for everything except headings ────────────────────
   Opt-in per form (Create Team uses it). Values, labels, hints and option rows all sit at
   --fs-meta; only section headings stay larger. This is the payoff of the token scale: the
   whole page changes density by redefining two variables, with no rule edited anywhere.

   The .ff reserve grows with it. That padding exists to hold the floated label clear of the
   value, and it was measured for a .7rem label — leave it alone and a .82rem label sits on
   top of the text it belongs to. */
/* :where() contributes ZERO specificity, so this modifier sits at the same weight as the base
   rules and never has to out-shout them. Written as a plain `.ss-form.ss-onesize` descendant
   it scored (0,3,1) against the base (0,2,2) and silently reinstated the label reserve on
   label-less fields — so they dropped below the pickers beside them, reintroducing the exact
   misalignment the base rule exists to prevent. The fix then was to restate the exemption
   here; duplicating a rule to win a specificity fight is how a stylesheet starts to rot, and
   :where() removes the fight instead of winning it. */
.ss-form.ss-onesize { --fs-body:var(--fs-meta); --fs-label:var(--fs-meta); }
:where(.ss-form.ss-onesize) .ff { padding-top:var(--sp-7); }
/* Eight rules used to live here restating the FLOATED label position so it could beat the
   resting one. With no resting state there is nothing to beat, so they are gone. This is the
   argument for removing state rather than styling it: the special cases were never the
   feature, they were the cost of the feature. */

/* ── DETAIL PAGES — a dense label/value grid ────────────────────────────────────
   The read-only counterpart to the authoring form's row vocabulary. Detail pages were
   built from DaisyUI cards holding a two-column grid, which spends a card's padding on
   four sides, a shadow, and a full row of vertical rhythm on every label/value pair —
   the team page ran to 2.4 screens to show about twenty facts.

   Same four-step type scale and same quiet-text tokens as the forms, so a value on a
   detail page and the field that produced it are the same size. No box: a heading and a
   hairline already say "these belong together", which is what the card was buying. */
.ss-dt-head {
  font-size: var(--fs-label);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.08em;
  color: var(--ff-label);
  padding-bottom: 0.25rem;
  margin-bottom: 0.5rem;
  border-bottom: 1px solid var(--ff-line);
}

/* Two columns on a phone, four on a desktop. The old grid was fixed at two and left
   half the width empty on the screens this page is actually used on. */
.ss-dl {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  column-gap: var(--sp-8);
  row-gap: var(--sp-4);
}

@media (min-width: 640px) {
  .ss-dl { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}

@media (min-width: 1280px) {
  .ss-dl { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}

.ss-dl dt {
  font-size: var(--fs-label);
  color: var(--ff-label);
  line-height: 1.3;
}

.ss-dl dd {
  font-size: var(--fs-meta);
  font-weight: 500;
  color: var(--ff-ink);
  margin: 0;
  line-height: 1.35;
}

/* "Any" means no restriction. It still has to be readable — it is the answer to the
   question the label asks — but it should not compete with a value that was chosen. */
.ss-any {
  color: var(--ff-disabled);
  font-weight: 400;
}

/* Description clamped to three lines, expandable. Flex so the toggle can sit BELOW the
   text: <summary> must be the first child of <details>, and a "show more" above the
   thing it reveals reads backwards. */
.ss-desc {
  display: flex;
  flex-direction: column;
}

.ss-desc > div {
  display: -webkit-box;
  -webkit-line-clamp: 3;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.ss-desc[open] > div {
  display: block;
}

.ss-desc summary::-webkit-details-marker { display: none; }

/* Green dot = this field actually constrains something. The page shows the whole
   specification, so the dot is what separates the eight fields that were qualified from
   the twenty-one that were left open, without making the reader compare every value. */
.ss-dot {
  display: inline-block;
  width: 6px;
  height: 6px;
  border-radius: 9999px;
  background: var(--color-success);
  vertical-align: middle;
  margin-right: 0.35rem;
}

/* Follower count — reads the bookmarks the heart has always been writing. */
.ss-followers {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  font-size: var(--fs-label);
  color: var(--ff-label);
}

/* ── .ss-facts — read-only label/value pairs, sized to their content ──────────────────
 *
 * OWNS:   the presentation of a record's facts on a DETAIL page (profile, facility, team).
 * REUSES: the same tokens as .ss-form — --fs-label/--fs-body for type, --ff-label/--ff-ink
 *         for colour, --sp-* for rhythm. Per §8.2 a new surface takes the tokens WITHOUT
 *         opting into .ss-form, because .ss-form's opinions (underlined controls, section
 *         rules) belong to inputs, and none of this is an input.
 *
 * WHY IT EXISTS: the profile page rendered every fact as
 *     <div class="flex justify-between"><span>Username</span><span>Minor_14</span></div>
 * so a nine-character username was stretched across ~1100px with a void in the middle, one
 * fact per row, and the eye had to travel the full width to pair a label with its value.
 * Six facts filled a screen.
 *
 * A wrapping flex row is the whole trick: each pair takes the width of its OWN content and
 * they pack across the row, so short facts sit together and a long one gets the room it needs.
 * No breakpoints to maintain — the browser fits what fits.
 *
 * Label and value sit on ONE line, "Username: Minor_14", not stacked. The colon pairs them
 * more tightly than position does, and stacking doubled the vertical space for values this
 * short. (This is the one place that departs from §8.3's label-above rule, which is about
 * FIELDS — there the label has to clear a control; here there is nothing to clear.)
 */
/* FLEX, not grid. `repeat(auto-fit, minmax(9rem, max-content))` reads like content sizing and
 * is the opposite: auto-fit decides HOW MANY tracks from the 9rem MINIMUM, so on a 1500px row
 * it created nine 9rem tracks and spent every pixel before any track could grow toward its
 * max-content. Measured, every track came out 144–147px — "Age: 14" was given 144px it did not
 * want, and the email needed 204px and got 147, so it painted straight over "Zodiac west".
 * Uniform tracks with an overflowing cell is precisely the row sizing the content.
 *
 * A wrapping flex row has no track concept to get wrong: each pair is `flex: 0 0 auto`, so it
 * is exactly as wide as it is, and the line breaks when the next pair does not fit. */
.ss-facts {
  display: flex;
  flex-wrap: wrap;
  column-gap: var(--sp-8);
  row-gap: var(--sp-4);
  align-items: baseline;
}
.ss-facts > div {
  min-width: 0;
  display: flex;
  align-items: baseline;
  gap: var(--sp-2);
  /* Sized by its own content, never stretched to a track and never squeezed below it. */
  flex: 0 0 auto;
  /* The one guard: a value longer than the whole row wraps inside the page instead of running
     off the side of it. Only bites when a single pair exceeds 100% of the row. */
  max-width: 100%;
}
/* --fs-meta, NOT --fs-label. 0.7rem is sized for a label FLOATING ABOVE a field, where it is
 * the only thing on its line and has whitespace doing half the work. Inline against a 1rem
 * value it reads as a misprint — Hugh: "why is this font tiny". One step down (0.82rem) is
 * enough to subordinate the label without making it look like an error. */
.ss-facts dt {
  font-size: var(--fs-meta);
  line-height: 1.3;
  color: var(--ff-label);
  white-space: nowrap;
}
.ss-facts dt::after { content: ":"; }
.ss-facts dd {
  font-size: var(--fs-body);
  line-height: 1.35;
  font-weight: 600;
  color: var(--ff-ink);
  margin: 0;
  /* `break-word`, and neither of the two values tried before it.
   *
   * `anywhere` broke mid-word in a narrow column — "United States" one letter per line, an
   * email split as "minor_14@example.in / valid" — because it also shrinks the box's own
   * min-content width, inviting the squeeze it was meant to survive.
   *
   * `normal` then refused to break at all, which is what let the email overflow its cell and
   * overprint the neighbouring fact.
   *
   * `break-word` is the one that does the job: it leaves intrinsic sizing alone, so the pair
   * is still measured at its full unbroken width and normally sits on one line — it breaks
   * only as a last resort, when the value genuinely cannot fit the row. */
  overflow-wrap: break-word;
  min-width: 0;
}
/* A fact whose value is long prose (an address, a bio) can opt out of packing and take the
   whole row to itself. `flex-basis:100%` forces a line break before and after it. */
.ss-facts > div.ss-facts-wide { flex: 1 0 100%; }

/* ── .ss-stack / .ss-sec — a record page is SECTIONS DOWN THE PAGE, not a wall of cards ──
 *
 * OWNS:   the layout of a detail page: every section full width, one under the next.
 * REUSES: .sec-head for the heading and its hairline divider — the SAME component
 *         /teams/create/ uses, which is why this now matches it. --sp-* for rhythm.
 *
 * WHY IT EXISTS. The profile was a 3-column grid of shadowed cards. Hugh: "i asked for all
 * these tables to be under each other… get rid of all the boxes".
 *
 * The boxes were not merely ugly, they caused the layout bugs. A card in the third column is
 * ~9rem of usable width, so "United States" broke to ONE LETTER PER LINE, an email split
 * mid-domain, and a 6-column sports table got its own scrollbar — while the row above held a
 * single email address in a full column of empty space. Content squeezed and padded at once.
 *
 * Neither /teams/create/ nor /leagues/ has a single grid-cols on it. Their rows are sized by
 * the content; a grid-cols-3 sizes the content by the row. That is the whole difference.
 *
 * .ss-sec and .ss-sec-body carry NO declarations, deliberately. They were written as overrides
 * of DaisyUI's .card (background, shadow, radius, padding); the markup no longer carries
 * .card, so they reset nothing. The names remain as semantic hooks — the divider belongs to
 * .sec-head and the spacing to .ss-stack.
 */
.ss-stack { display: block; }
.ss-stack > * + * { margin-top: var(--sp-8); }


/* ─── Embedded help (see {% help_block "slug" %}) ────────────────────────────────
   The same DB text, shown IN the page instead of behind a chip. For the cases a tooltip
   cannot carry: a paragraph explaining a whole section, a warning that must be read rather
   than discovered, or a screen someone meets once and needs walking through.
   Quiet by default — this is guidance, not an alert, so it takes no accent colour. */
.help-block { border-left:2px solid var(--color-base-300); padding:2px 0 2px 10px; margin:6px 0 10px;
  font-size:13.5px; line-height:1.45; color:var(--color-base-content); opacity:.72; }
.help-block p { margin:0 0 6px; }
.help-block p:last-child { margin-bottom:0; }

/* "Report a translation problem" — only rendered on machine-translated help (help_tags.py),
   so it never appears under English copy. Quiet by default: it is a safety net for the reader,
   not a call to action, and it sits under help text that is usually fine. */
.help-report { display:inline-block; margin-top:4px; padding:0; border:0; background:none;
  font-size:11.5px; line-height:1.3; color:var(--color-base-content); opacity:.45;
  text-decoration:underline; text-underline-offset:2px; cursor:pointer; }
.help-report:hover, .help-report:focus-visible { opacity:.85; }
.help-report:focus-visible { outline:2px solid var(--ff-accent); outline-offset:2px; }

/* ─── Show/hide help, site-wide ──────────────────────────────────────────────────
   `data-help="off"` on <html> hides every chip and every embedded help block. Set before
   first paint by the inline script in base.html; toggled by js/help-toggle.js.

   display:none rather than visibility or opacity — a chip that still occupies space when
   hidden shifts every label it sits beside, so turning help off would reflow the form. */
html[data-help="off"] .qhelp-wrap,
html[data-help="off"] .help-block { display:none; }

/* The header button. Green = showing, red = hidden — the state, not the action; the tooltip
   carries the action ("Hide help"), because a control labelled with what it currently IS reads
   as a status light and gets clicked by mistake. */
.help-toggle-icon { display:inline-flex; align-items:center; justify-content:center;
  width:20px; height:20px; border-radius:999px; font-size:13px; font-weight:700;
  line-height:1; border:1.5px solid currentColor; }
#help-toggle.help-on  { color:var(--color-success, #16a34a); }
#help-toggle.help-off { color:var(--color-error, #dc2626); }
#help-toggle.help-off .help-toggle-icon { opacity:.75; }


/* ─── searchable_select, multiple mode ───────────────────────────────────────────
   Chips for chosen values. Sits BELOW the input so adding a fourth item never reflows the
   control you are typing into — a chips-inside-the-box layout grows the field and pushes the
   dropdown, which is how you lose your place halfway through picking age groups. */
.searchable-select-chips { display:flex; flex-wrap:wrap; gap:4px; margin-top:6px; }
.searchable-select-chips:empty { display:none; }
.searchable-select-chip { display:inline-flex; align-items:center; gap:4px;
  padding:2px 6px 2px 8px; border-radius:999px; font-size:12px; line-height:1.5;
  background:var(--color-base-200); border:1px solid var(--color-base-300);
  color:var(--color-base-content); }
.searchable-select-chip-x { border:0; background:none; cursor:pointer; padding:0 2px;
  font-size:14px; line-height:1; color:var(--color-base-content); opacity:.5; }
.searchable-select-chip-x:hover { opacity:1; color:var(--color-error); }
.searchable-select-chip-x:focus-visible { outline:2px solid var(--ff-accent); outline-offset:1px; }
