/* ==========================================================
   Scale — type and radius
   ==========================================================

   Two scales, and nothing below may use a raw px value for
   either. They are not colours: these are identical under both
   themes, which is why they sit in their own block above the
   colour one rather than inside it.

   The type scale replaced 26 distinct font sizes — every half
   step from 8.5px to 16px, chosen per element by eye. Each one
   was defensible and the set was not: nothing shared a size, so
   nothing read as belonging to the same family. Eight steps at
   roughly a 1.2 ratio, widening at display sizes.

   --fs-base lands on 16px, which is also the size iOS needs a
   field to be before it stops zooming the page on focus. That
   rule under @media (pointer: coarse) is now a floor the design
   already meets rather than an exception fighting it.

   The radius scale replaced 15 values — every integer from 2 to
   12. Four steps and a pill, by the job each does: an inline
   mark, a control, a card, an overlay.
   ========================================================== */

:root {
  --fs-2xs:  10px;   /* board cells, badges, uppercase micro-labels */
  --fs-xs:   12px;   /* hints, meta, field labels, small buttons    */
  --fs-sm:   14px;   /* card body, blurbs, nav, tabs                */
  --fs-base: 16px;   /* body, form fields, primary buttons          */
  --fs-lg:   19px;   /* lead paragraphs, panel headings, wordmark   */
  --fs-xl:   23px;   /* section headings                            */
  --fs-2xl:  32px;   /* grade letter, hero headline on a phone      */
  --fs-3xl:  42px;   /* hero headline                               */

  --r-xs:    4px;    /* badges, board cells, bars, swatches         */
  --r-sm:    8px;    /* buttons, inputs, selects, seats             */
  --r-md:   12px;    /* cards and panels                            */
  --r-lg:   16px;    /* overlays: player sheet, dialog, rail, chat  */
  --r-pill: 999px;   /* chips, pills, tabs, the two FABs            */
}

/* ==========================================================
   Juke — stylesheet
   Colours are defined once here, at the top, and reused
   everywhere below by name. Change one line, change the
   whole site.

   There are two sets of them. The dark set lives on :root
   and is therefore what you get by default, including on a
   page that never runs a line of JavaScript. The light set
   overrides it only when the reader has asked for light and
   the toggle has written data-theme="light" onto <html>.

   The rule that keeps this honest: no rule below may name a
   colour that changes between the two themes. If a literal
   hex appears past this block it is either brand navy, which
   is dark in both themes, or a position colour, which is a
   mid tone that works on either.
   ========================================================== */

:root {
  /* ---- dark: the default ---- */

  /* Juke navy and Juke orange, both sampled from the logo artwork itself
     rather than typed from memory. The header is dark under both themes,
     so these do not move when the theme does. */
  --navy-deep:   #0C1A2E;   /* header gradient, dark end */
  --navy:        #152A49;   /* the logo's own navy        */
  --navy-soft:   #24405F;
  --navy-mid:    #1E3E68;   /* the lift in a live header  */
  --navy-lift:   #13497A;   /* the blue end of a gradient */
  --navy-glow:   #0E5A93;

  /* The one warm colour in the brand. It is the swoosh, and it is strong
     enough to carry dark text but not white — see the note on --orange-cta,
     which is the same hue pushed dark enough to sit under white. */
  --orange:      #ED6011;
  /* The navy half of the logo, on a normal surface. White here because the
     default theme is dark; the light theme puts the real brand navy back. */
  --mark-ink:    #FFFFFF;
  /* The primary action colour, everywhere and only. White on it is 5.18:1,
     and it does not move between themes, which is why a rule may name it.

     It used to share the job with --blue: .cta was orange and .primary was
     blue, so the same control — "Resume" on a saved draft — was orange on
     the landing page and blue three lines into the draft view, from the
     same two words in the same codebase. A first-time visitor pressed a
     big orange button and then had to find a blue one to actually start.

     Blue keeps everything it was already better at: focus rings, the
     selected tab, the header when the clock is yours. Orange means act,
     blue means state. See the note on .draft-btn for the one place a
     button does something and is deliberately still blue. */
  --orange-cta:  #C2410C;
  --orange-cta-hover: #A8380A;   /* was typed as a literal in .cta:hover */
  /* Orange *as text on a surface*, which is a different problem again — the
     same split as --blue and --link. The brand orange reads 5.49:1 on this
     dark page and only 3.11:1 on the light one, so the light block swaps it
     for the darker value, which is 4.83:1 there and 3.54:1 here. Neither
     works in both, so neither may be named directly by a rule. */
  --orange-ink:  #ED6011;

  /* The header changes colour with the state of the draft, not with the
     theme, so it keeps its own blues and reds. The ones below named
     --blue and --danger do move, which is why these are separate.

     Every stop here carries white text, so every stop has to clear 4.5:1
     against white on its own. That is a stricter rule than it sounds:
     --hdr-cyan sits at 130% and so is never fully painted, and it would
     have been tempting to check only the colour the box actually reaches.
     Tying the requirement to a stop percentage means moving that stop
     silently breaks the contrast, so each stop passes by itself and any
     interpolation between two passing stops passes with them.

     --hdr-cyan was #12A3DC, which is 2.88 — the worst contrast anywhere
     in the app. The lightest colour actually painted was 3.65, and the
     header carries a 16px headline, a 12px pick line and a 10px label on
     top of it. */
  --hdr-blue-deep: #0A548A;   /* white 7.93 */
  --hdr-blue:      #0E6BB0;   /* white 5.60 */
  --hdr-cyan:      #0E7DA9;   /* white 4.63, was #12A3DC at 2.88 */
  --hdr-red-deep:  #7A1F1A;   /* white 10.3 */
  --hdr-red:       #B43026;   /* white 6.19 */
  --hdr-red-lift:  #D04439;   /* white 4.60, was #D0453A at 4.57 */

  /* Blue as a filled button always carries white text, so it can only go so
     light before that text stops being readable. It brightens a little from
     the light theme's blue, to lift off a dark card, and no further.
     Blue as *text* has the opposite problem and needs --link instead. */
  --blue:        #1776BE;
  --blue-dark:   #125F9C;   /* hover: deeper, the same way round as in light */
  --blue-pale:   #16334C;
  --blue-line:   #2C567D;
  --cyan:        #35BDEE;
  --link:        #62B8F2;   /* blue on a surface, rather than under white text */

  /* Three tones, and the lightest of them is pinned by contrast rather than
     by taste. --ink-light is the tertiary tone — captions, meta, uppercase
     labels — and it is used 58 times, almost always at --fs-2xs or --fs-xs,
     which is small text and therefore needs 4.5:1.

     It was #76838F and it missed on every surface it lands on: 4.43 on
     --sunken, 4.18 on --card, 3.83 on --well and 3.54 on --fill. That is
     122 elements, and it was not a rounding error — the light theme's
     #94A1AC was worse still, bottoming out at 2.31.

     The binding surface is --fill, the lightest thing it sits on in dark
     and the darkest in light. --fill-hi is lighter again but never carries
     this tone: every rule that hovers to --fill-hi either sets --ink or
     starts from --ink-mid, which clears it at 5.30. */
  --ink:         #E6ECF3;
  --ink-mid:     #A2B0BE;   /* 6.21 on --fill */
  --ink-light:   #8E9BA9;   /* 4.85 on --fill, the worst surface it meets */

  --page:        #0E151E;
  --card:        #18212D;
  --line:        #2A3441;
  --hairline:    #222C38;   /* the rules between rows inside a card */
  --sunken:      #131C26;   /* a panel set into a card: stat boxes, table heads */
  --well:        #1E2836;   /* slot chips, round numbers */
  --fill:        #232E3C;   /* empty board cells, small buttons */
  --fill-hi:     #2D3948;   /* those buttons, hovered */
  --track:       #242F3D;   /* the unfilled part of a bar or meter */
  --board-hd:    #212B38;

  --danger:      #F0837A;
  --good:        #4FBF7F;

  --row-bad:     #2A171B;   /* a whole row tinted with bad news */
  /* ---- yours ----

     Identity, and it is a third meaning after the two this file already
     names: orange acts, blue states, and gold is *whose*. It had been blue
     in four of the six places a seat is marked, navy in a fifth and
     translucent white in the sixth — and blue was already carrying focus
     rings, the selected tab, --link, .draft-btn and the header when the
     clock is yours. A colour doing five jobs is not a signal.

     --mine and --ring-edge do not move between themes, and that is the
     point rather than an oversight. A ring on the board lands on six
     position solids — which are themselves fixed across themes — and on an
     empty cell, which is near-black in one theme and near-white in the
     other. Measured, no single value survives that: gold clears 3:1 on
     every solid (3.2 worst) and on a dark empty cell (9.5) and reaches
     1.26 on a light one; the keyline is the exact complement, 4.1 on the
     solids and 16.7 on a light cell and 1.2 on a dark one. So the ring is
     a *pair* — 2px of gold with 1px of keyline inside it — and between
     them one always clears 3:1 on whatever it is sitting on, while the
     two clear 11.7 against each other so the pair always reads as an
     edge. --live-ring is white and takes the identical construction for
     the identical reason.

     **Gold is a mark and never type.** #FFD166 as text is 1.4:1 on a light
     card, which is why the chat author's name stays --link: that is a name
     in a paragraph, not a ring on a cell. */
  --mine:        #FFD166;   /* the ring on a seat that is yours */
  --ring-edge:   #0B1017;   /* what makes a ring survive any surface under it */
  --live-ring:   #FFFFFF;   /* the pick on the clock, wherever it lands */
  --row-mine:    #221C10;   /* a whole row tinted as yours      */
  --pale-mine:   #332608;   /* your line in the room standings  */

  /* Tints: a soft background with a legible foreground on it. Used by
     the injury chips, the tier chips, the bye bars and the callouts. */
  --t-good-bg:#12301F;   --t-good-fg:#6FD79E;
  --t-bad-bg:#3A1D22;    --t-bad-fg:#F2909A;
  --t-ir-bg:#431A20;     --t-ir-fg:#EE8590;
  --t-warn-bg:#33270F;   --t-warn-fg:#E3B268;
  --t-bye-bg:#33240F;    --t-bye-fg:#E4AC63;
  --t-info-bg:#16293C;   --t-info-fg:#75BAE8;
  --t-purple-bg:#251E3C; --t-purple-fg:#B7A4EE;
  --t-orange-bg:#38230F; --t-orange-fg:#E79A62;
  --t-neutral-bg:#232D38; --t-neutral-fg:#A5B3BF;

  /* Position colours. The -bg / -fg pair is a chip or an avatar; the bare
     name is a solid block on the draft board, which always carries white
     text and so is the same in both themes.

     "Always carries white text" is a contrast requirement, and for four of
     the six it was not being met. The board sets names at --fs-2xs, so the
     bar is 4.5:1, and they measured: TE 2.68, WR 3.12, QB 3.96, RB 3.97 —
     80 elements. Only K (4.52) and DST (4.86) passed.

     The fix darkens lightness alone and leaves hue and saturation where
     they were, so the board still colour-codes the way it always has. Each
     lands at 4.61-4.62, a margin over the bar rather than exactly on it.
     The cost is concentrated in TE and WR: measured as CIE76 dE, TE moves
     19 and WR 14.4, which is a change you will see, while RB moves 4.9 and
     K 1.4, which you will not. DST did not move at all.

     What matters more than any single shift is that the six are still
     telling apart from each other: the closest pair is RB/DST, and it goes
     from 28.1 to 25.0 against a just-noticeable threshold near 2.3.

     If a seventh position is ever added, check white on it before adding
     it here. That check is the reason four of these values are not the
     ones sampled off the original artwork. */
  /* Seat colours, for the avatar beside a chat message. Solids under white
     text, exactly like the position blocks below and defined here for the
     same reason: they are the same value under both themes, so a rule may
     name one without needing a light override. Eight, cycled, because a
     fourteen-team room has more chairs than there are hues worth telling
     apart at eighteen pixels across.

     Five of these were the same hex as a position colour and so carried
     the same failure; they take the same darkened value. seat-5 was 4.23
     on its own account. seat-6 and seat-7 already passed. */
  --seat-0:#2A7BB1; --seat-1:#208553; --seat-2:#D43E39; --seat-3:#7F62CF;
  --seat-4:#AA6419; --seat-5:#118093; --seat-6:#B5427F; --seat-7:#607483;

  --qb-bg:#3A1D22;  --qb-fg:#F0929B;  --qb:#D43E39;   /* white 4.62 */
  --rb-bg:#12293A;  --rb-fg:#74B9E6;  --rb:#2A7BB1;   /* white 4.62 */
  --wr-bg:#12301F;  --wr-fg:#6ED79C;  --wr:#208553;   /* white 4.62 */
  --te-bg:#33240F;  --te-fg:#E9AE63;  --te:#AA6419;   /* white 4.61 */
  --k-bg:#251E3C;   --k-fg:#B4A0EC;   --k:#7F62CF;    /* white 4.61 */
  --dst-bg:#232B33; --dst-fg:#A8B7C2; --dst:#607483;  /* white 4.86 */

  /* ---- elevation ----

     Four steps, and no rule below may write a raw box-shadow for a lifted
     surface. They replaced fourteen distinct shadow values across eighteen
     declarations, every one picked locally by eye — the same failure as the
     26 font sizes and 15 radii, and the one DESIGN-DIRECTION.md names as the
     difference between "clean" and "designed": nobody notices any single
     shadow, and the sum of them is what reads as a product.

     Two things make this a system rather than a list.

     **One light source, and it is directly above.** Every step offsets down
     the Y axis only. A shadow that leans left beside one that leans right is
     two light sources in one composition, which is the thing the eye catches
     without being able to name it. The door is the single exception and says
     so where it sits: it is a lit object with its own interior light, not a
     card on a page.

     **Two layers, always.** A tight, dark contact shadow for where the object
     meets the surface, and a broad, soft ambient one for the room light it
     blocks. One blurred layer is what flat design does; both is what makes a
     surface look like it is actually above another one. That is most of the
     difference between these and what they replaced.

     They live here rather than beside the type and radius scales because a
     shadow is a colour: on a dark page it has to be near-black and deep to
     register at all, and the same value on a light page reads as grime.

     Three things below still write a shadow by hand, and none of them is a
     lifted surface: `.sug.top` is a blue state glow rather than a height,
     `--team` is a ring drawn as a shadow because a border would eat the
     headshot, and the doorway's handle, placard and sill are detail on a lit
     object with an interior light of its own. The door says so where it sits.
     Anything else with a literal here is a rule that forgot. */
  --elev-1: 0 1px 2px rgba(3, 8, 16, .48);
  --elev-2: 0 2px 4px rgba(3, 8, 16, .42), 0 6px 14px rgba(3, 8, 16, .36);
  --elev-3: 0 4px 8px rgba(3, 8, 16, .42), 0 14px 30px rgba(3, 8, 16, .40);
  --elev-4: 0 8px 16px rgba(3, 8, 16, .46), 0 28px 60px rgba(3, 8, 16, .48);
  /* The one that casts upwards, and the single exception to the overhead
     light. A sheet rising from the bottom edge of the screen has no floor
     below it to cast onto, so the shadow's whole job there is to separate its
     top edge from the page it is covering. Named rather than written inline
     so "no raw box-shadow for a lifted surface" stays literally true. */
  --elev-up: 0 -8px 28px rgba(3, 8, 16, .44);

  /* Tells the browser to draw the parts we don't control — select popups,
     checkboxes, scrollbars — to match. Without it a dark page gets white
     dropdowns. */
  color-scheme: dark;
}

:root[data-theme="light"] {
  /* ---- light: only when it is asked for ---- */

  --blue:        #0E6BB0;
  --blue-dark:   #0A548A;
  --blue-pale:   #E8F2FA;
  --blue-line:   #C9DCF9;
  --cyan:        #12A3DC;
  --link:        #0E6BB0;
  /* The darker orange as text here: 4.83:1 on this page, where the brand
     value would be 3.11:1 and fail at any size the slogan is set in. */
  --orange-ink:  #C2410C;

  /* The same three tones, and the one place the two themes could not simply
     mirror each other.

     --ink-light has to reach #5E6A76 to clear 4.5:1 on --fill (#EDF0F3),
     and #5A6875 — where --ink-mid sat — is 4.83 against 5.00. Two tones
     0.17 apart is one tone with two names, so making the tertiary legible
     had to push the secondary along with it. --ink-mid moves to #4C5763,
     which restores a 1.61 gap on --fill and lines up with dark's 1.36.

     So the ramp is set from the bottom in both themes: --ink-light is
     whatever the worst surface allows, and --ink-mid is placed above it.
     Body text getting darker in light is the side effect, and it is the
     right side effect. */
  --ink:         #16202C;
  --ink-mid:     #4C5763;   /* 6.44 on --fill */
  --ink-light:   #5E6A76;   /* 4.83 on --fill, the worst surface it meets */

  --page:        #F5F7FA;
  --card:        #FFFFFF;
  --line:        #E4E9EF;
  --hairline:    #F1F4F7;
  --sunken:      #F7F9FB;
  --well:        #F0F3F6;
  --fill:        #EDF0F3;
  --fill-hi:     #E6EBF1;
  --track:       #EDF1F5;
  --board-hd:    #E9EDF1;

  /* Same geometry, a fraction of the weight, and tinted with the page's own
     navy rather than black — a neutral grey shadow on a cool light page reads
     as dirt. See the note on the dark set for why there are two layers. */
  --elev-1: 0 1px 2px rgba(20, 35, 56, .10);
  --elev-2: 0 2px 4px rgba(20, 35, 56, .07), 0 6px 14px rgba(20, 35, 56, .10);
  --elev-3: 0 4px 8px rgba(20, 35, 56, .08), 0 14px 30px rgba(20, 35, 56, .12);
  --elev-4: 0 8px 16px rgba(20, 35, 56, .10), 0 28px 60px rgba(20, 35, 56, .18);
  --elev-up: 0 -8px 28px rgba(20, 35, 56, .16);

  --mark-ink:    #152A49;

  --danger:      #C0392F;
  --good:        #1B7343;

  --row-bad:     #FEF6F7;
  /* --mine, --ring-edge and --live-ring are deliberately absent here: they
     are built as a pair that survives both themes. See the dark block. */
  --row-mine:    #FFF8E8;
  --pale-mine:   #FDF3DA;

  --t-good-bg:#E6F5EC;   --t-good-fg:#1B7343;
  --t-bad-bg:#FBE1E5;    --t-bad-fg:#A32530;
  --t-ir-bg:#F6D5D8;     --t-ir-fg:#86202A;
  --t-warn-bg:#FCF0DC;   --t-warn-fg:#8A5A11;
  --t-bye-bg:#FCEBD5;    --t-bye-fg:#95580F;
  --t-info-bg:#DCE9F7;   --t-info-fg:#15618F;
  --t-purple-bg:#EDE7FB; --t-purple-fg:#5B41A8;
  --t-orange-bg:#FBE4D3; --t-orange-fg:#9A4B14;
  --t-neutral-bg:#E4E8EC; --t-neutral-fg:#46555F;

  --qb-bg:#FBE1E5;  --qb-fg:#B02A37;
  --rb-bg:#D7EBF8;  --rb-fg:#15618F;
  --wr-bg:#D9F2E1;  --wr-fg:#1B7343;
  --te-bg:#FCEBD5;  --te-fg:#95580F;
  --k-bg:#E9E2F8;   --k-fg:#59409F;
  --dst-bg:#E2E8ED; --dst-fg:#46555F;

  color-scheme: light;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--page);
  color: var(--ink);
  font-family: Inter, system-ui, sans-serif;
  font-size: var(--fs-base);
  -webkit-font-smoothing: antialiased;
}

[hidden] { display: none !important; }

/* ---------- sticky header ----------
   The header, tabs and action bar are wrapped in one sticky
   container so they pin together and nothing has to guess
   the height of the thing above it.                        */

.sticky-top { position: sticky; top: 0; z-index: 30; }

/* The draft header and the site header are the same bar wearing different
   content: same surface, same centred column, same border. The Alpine mountain
   ridges that used to sit on top of this are gone with the rest of that brand.

   The two states that need your attention still take colour, because a bar
   that turns red at ten seconds is doing real work. Everything else, a CPU
   being on the clock included, is the resting state and matches the homepage. */
.appbar {
  position: relative; overflow: hidden;
  background: var(--card);
  border-bottom: 1px solid var(--line);
  color: var(--ink);
  transition: background .35s ease, color .35s ease, border-color .35s ease;
}

.appbar-inner {
  max-width: 1120px; margin: 0 auto; width: 100%;
  display: flex; align-items: center; gap: 12px;
  padding: 10px 18px;
}

.appbar.my-turn,
.appbar.urgent { color: #fff; border-bottom-color: transparent; }

.appbar.my-turn { background: linear-gradient(120deg, var(--hdr-blue-deep) 0%, var(--hdr-blue) 50%, var(--hdr-cyan) 130%); }
.appbar.urgent  { background: linear-gradient(120deg, var(--hdr-red-deep) 0%, var(--hdr-red) 60%, var(--hdr-red-lift) 100%); }

/* On a coloured bar the mark reverses and the toggle goes translucent white,
   the way both did on navy. */
.appbar.my-turn .mark,
.appbar.urgent .mark { --mark-ink: #fff; }

/* Both header toggles share .hdrbtn and nothing else.

   The sound button was given the class `theme-toggle` first, purely to
   inherit its look — and that class is not a style, it is a control:
   themeBtns() selects it, syncThemeButton() writes aria-pressed onto
   everything it finds, and the delegated click handler switches the theme on
   anything matching it. So the sound button loaded showing "on" it had never
   been given and would have changed the theme when pressed.

   Exactly the collision this project already has a rule about — .home was
   taken, .avatar was taken, initials() was taken. Checking the sheet for a
   name catches the first two; this one needed checking the *script* as well,
   because the clash was in behaviour rather than in appearance. */
.sound-toggle .i-sound-off { display: none; }
.sound-toggle[aria-pressed="false"] .i-sound-on { display: none; }
.sound-toggle[aria-pressed="false"] .i-sound-off { display: block; }

.appbar.my-turn .hdrbtn,
.appbar.urgent .hdrbtn {
  border-color: rgba(255,255,255,.22);
  background: rgba(255,255,255,.10); color: #fff;
}

.appbar.my-turn .hdrbtn:hover,
.appbar.urgent .hdrbtn:hover { background: rgba(255,255,255,.20); border-color: rgba(255,255,255,.38); }

.appbar.my-turn .hdrbtn:focus-visible,
.appbar.urgent .hdrbtn:focus-visible,
.appbar.my-turn .home:focus-visible,
.appbar.urgent .home:focus-visible { outline-color: #fff; }

/* the app icon's peak, on the navy it was drawn for */
/* The mark is a button now, so it needs to carry the flex sizing the SVG
   used to, and to look pressable without becoming a box in the header. */
.home {
  flex: none;
  display: flex; align-items: center;
  border: 0; background: none;
  color: inherit; cursor: pointer;
  font-size: var(--fs-sm);        /* icon only — see the note on .theme-toggle */
  transition: opacity .15s ease;
  /* The mark is 31 by 36, and a thumb wants 44 square. The padding grows the
     hit area and the negative margin takes the space back, so nothing moves
     on the page. 7px rather than 5: five only reached 41 wide, which missed
     the number this rule exists to hit, and it missed it quietly because the
     hit area is invisible. 31 + 14 = 45. Measured, not assumed. */
  padding: 7px; margin: -7px;
}

/* The chevron sits tight against the mark and reads as one control with it,
   which is the point: two separate buttons in a header this narrow would be
   two ways out rather than one. */
.i-back { flex: none; margin-right: 2px; opacity: .9; }
.home:hover { opacity: .78; }
.home:focus-visible { outline: 2px solid var(--blue); outline-offset: 4px; border-radius: var(--r-sm); }

/* The shield is taller than it is wide — 662 by 774 in the artwork — so it
   cannot be sized as a square without squashing it. The drop shadow it used to
   carry existed to lift it off the mountain texture, which is gone. */
.mark {
  width: 31px; height: 36px; min-width: 31px; flex: none;
}

/* The logo is drawn navy-on-light. On a light surface it needs its navy
   back or it is white-on-white and only the swoosh survives; on the draft
   header, which is navy under both themes, it stays reversed. */
.mark-body   { fill: var(--mark-ink); }
.mark-accent { fill: var(--orange); }

/* The mark is drawn once in a <symbol> and cloned by <use>, and CSS cannot
   select into the shadow tree that creates — ".appbar .mark-body" would match
   nothing. Custom properties do inherit into it, so any per-header override
   sets a variable on the <svg> rather than a fill on the path. */

.appbar-text { min-width: 0; }

/* Team names run to "Bone-Thugs-N-Montgomery", and on a narrow phone the
   header cannot hold that next to the counter and the toggle. It has to end
   in an ellipsis rather than be sliced through a letter by the bar's own
   overflow, which is what happens if only nowrap is set. */
.appbar-text h1 {
  margin: 0;
  font-family: Poppins, sans-serif;
  font-size: var(--fs-base); font-weight: 700; letter-spacing: -0.01em;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.appbar-text p {
  margin: 0; font-size: var(--fs-xs); color: var(--ink-mid);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

/* The pick and the league on one line, with a divider drawn rather than
   typed — a literal middot in the markup would still be there when the
   league half is hidden below 760px, leaving a line that ends in a dot. */
.appbar-league::before {
  content: "\00b7"; margin: 0 6px; color: var(--ink-light);
}
.appbar.my-turn .appbar-league::before,
.appbar.urgent .appbar-league::before { color: #fff; }

/* Under 760 the header is the pick, the counter and two toggles, and the
   league summary is the one of the four that can wait. It is reference
   rather than state: nothing about it changes during a draft. */
@media (max-width: 760px) { .appbar-league { display: none; } }

/* Below 560 the mark comes off the draft header and the chevron carries the
   way out on its own.

   Measured rather than chosen: "You're on the clock!" is about 165px at 16px
   bold, and at 390px the header has 143px left for it once the padding, the
   chevron, the mark, the counter and two toggles have taken theirs — so the
   most important sentence in the app was rendering as "You're on the c…".
   Dropping the mark returns 174px and it fits.

   It is the one place the draft header stops matching the landing one, and
   the trade is the right way round: a chevron alone is the universal back
   affordance on a phone, and the brand is still on every other screen. */
@media (max-width: 560px) {
  .appbar .home .mark { display: none; }
  .appbar .i-back { margin-right: 0; }
  /* And the spacing tightens with it. Measured at 390px: the header had 147px
     for a headline that wants about 175, and the mark alone does not close
     that. 18px of padding, three 12px gaps and the counter's own 10px inset
     are 30 more, spent on air either side of a sentence that is being cut in
     half. */
  .appbar-inner { padding-left: 12px; padding-right: 12px; gap: 8px; }
  .appbar-right { gap: 8px; padding-left: 6px; }
}

/* Solid, not rgba(255,255,255,.82).

   Washing white out to signal "secondary" is close to free on a dark
   surface and expensive on a saturated one, and this is the most saturated
   surface in the app. At 12px it needs 4.5:1, and the lightest end of the
   blue gradient gives pure white 4.64 — so the minimum workable alpha is
   0.98, and even a solid #F2F6F9 only reaches 4.27. There is no opacity
   that reads as secondary and stays legible; the choice is a false one.

   So hierarchy here comes from size and weight instead, which is what it
   should have come from on a colour this strong: 12px regular under a 16px
   bold headline. Same for .count-label below, which was .68 and measured
   2.48 — the worst contrast on the page a manager stares at while their
   own clock runs down. */
.appbar.my-turn .appbar-text p,
.appbar.urgent .appbar-text p { color: #fff; }

.appbar-right {
  margin-left: auto;
  display: flex; align-items: center; gap: 12px;
  padding-left: 10px;
}

.count { text-align: right; }

.count-label {
  display: block; font-size: var(--fs-2xs); letter-spacing: .1em;
  text-transform: uppercase; color: var(--ink-light); font-weight: 600;
}

.appbar.my-turn .count-label,
.appbar.urgent .count-label { color: #fff; }

.count-value {
  display: block; font-family: Poppins, sans-serif;
  font-size: var(--fs-xl); font-weight: 700; line-height: 1.1;
  font-variant-numeric: tabular-nums;
}

/* At 360 the headline was still short, and the counter is where the pixels
   are — but not where you would look for them. The value is the big number
   and shrinking it does nothing: "Time left" and "0:59" measure 60px each,
   and 0.1em of tracking over nine uppercase characters is most of it. But
   they measure 60px *each*, so trimming either one on its own changes
   nothing at all — the block is as wide as its widest child either way, and
   both first attempts here looked reasonable and moved the header by zero
   pixels. Both together return the seven that let "You're on the clock!"
   finish its own sentence on a 360px phone. --fs-lg is still on the type
   scale and the digits are still the largest thing on the right.

   It sits *below* the two rules it overrides, and it has to: a media query
   carries no extra specificity, so at equal weight the later declaration
   wins whether or not a query wrapped it. Written above them first, it
   matched — matchMedia agreed it matched — and changed nothing, which reads
   exactly like a rule that is not applying at all. */
@media (max-width: 400px) {
  .count-label { letter-spacing: .04em; }
  .count-value { font-size: var(--fs-lg); }
}


/* ---------- header toggles ----------
   It lives on the header, which is dark under both themes, so it is
   styled once against navy rather than switching with everything else. */

/* There is one toggle per header, so the base is styled for a normal surface
   and the draft header — which is navy under both themes — overrides it. */
.hdrbtn {
  flex: none;
  display: flex; align-items: center; justify-content: center;
  /* Icon only, so this changes nothing on screen — but a button with no
     font-size of its own takes the browser's 13.333px, which is the one
     value on the page that is not on the scale. Set so an audit of "is
     everything on the scale" can be believed. */
  font-size: var(--fs-sm);
  width: 34px; height: 34px;
  border: 1px solid var(--line); border-radius: 50%;
  background: var(--fill); color: var(--ink-mid);
  cursor: pointer;
  transition: background .15s ease, border-color .15s ease, color .15s ease;
}

.hdrbtn:hover { background: var(--fill-hi); color: var(--ink); }
.hdrbtn:focus-visible { outline: 2px solid var(--blue); outline-offset: 3px; }


/* One icon each way round: the sun offers the light theme, so it shows
   while the page is dark, and the moon shows while it is light. */
.theme-toggle .i-moon { display: none; }
:root[data-theme="light"] .theme-toggle .i-sun  { display: none; }
:root[data-theme="light"] .theme-toggle .i-moon { display: block; }

/* ---------- tabs ---------- */

/* ---------- the tab row ----------

   Navigation on the left, the things you do to the draft on the right, in one
   band. They were stacked: a 53px tab bar over a 43px action bar, under a
   57px header, which is 153px of chrome before a single player's name — 17%
   of a 900px screen spent on furniture, for four controls nobody presses
   twice in a draft.

   The 43px goes to the board rather than being pocketed, so nothing below
   this moves and the board gains a whole round.

   The band carries the paint. Neither child does, which is what lets the
   wrapper be the thing that hides: an empty band with a background and a
   bottom border still draws a line across the screen. */
.tabrow {
  display: flex; align-items: center; gap: 14px;
  padding: 0 12px;
  background: var(--card); border-bottom: 1px solid var(--line);
}
.tabrow[hidden] { display: none; }

.tabs {
  display: flex; gap: 6px; padding: 10px 0;
  flex: 1 1 auto; min-width: 0;
  overflow-x: auto;
}

.tabs button {
  flex: none; font-family: Inter, sans-serif;
  font-size: var(--fs-sm); font-weight: 600; color: var(--ink-mid);
  background: none; border: 0; border-radius: var(--r-pill);
  padding: 8px 16px; cursor: pointer; white-space: nowrap;
}

.tabs button.on { background: var(--blue); color: #fff; }

/* ---------- action bar ---------- */

/* Right of the tabs, and it does not shrink: these are the controls somebody
   reaches for when something has gone wrong, and a Discard button that has
   scrolled out of reach is worse than one taking up room. */
.actionbar {
  display: flex; gap: 8px; padding: 8px 0;
  flex: none;
}

.mini {
  flex: none; font-family: Inter, sans-serif;
  font-size: var(--fs-xs); font-weight: 600; color: var(--ink-mid);
  background: var(--fill); border: 0; border-radius: var(--r-sm);
  padding: 6px 12px; cursor: pointer; white-space: nowrap;
}

.mini:hover { background: var(--fill-hi); }
.mini.danger { color: var(--danger); }

/* The way on to the next mock. Reads as an invitation, next to a Discard
   that reads as a warning, so the two are not mistaken for each other. */
.mini.go { background: var(--orange-cta); color: #fff; }
.mini.go:hover { background: var(--orange-cta-hover); }
.mini:disabled { opacity: .45; cursor: default; }
.mini.active { background: var(--navy); color: #fff; }
.mini.active:hover { background: var(--navy-soft); }

/* Stacked again on a phone. The tab strip already scrolls sideways at this
   width, and feeding the action buttons into the same scroller puts "Discard
   draft" behind five tabs — which is the one control that must not be hard to
   reach. Vertical space is scarcer here and this spends 43px of it; that is
   the right way round.

   Below the rules it overrides, and that is not a filing preference: a media
   query carries no specificity, so at equal weight source order still
   decides. Written above them — which is where it went the first time — the
   query matched, every rule in it was inert, and the tab strip rendered
   471px wide inside a 390px phone. */
@media (max-width: 760px) {
  .tabrow { flex-direction: column; align-items: stretch; gap: 0; padding: 0; }
  .tabs { padding: 10px 12px; border-bottom: 1px solid var(--line); }
  .actionbar { padding: 8px 12px; overflow-x: auto; }
}


/* ---------- live pick ticker ---------- */

.ticker {
  display: flex; align-items: center; gap: 11px;
  padding: 8px 13px;
  background: linear-gradient(90deg, var(--navy) 0%, var(--navy-mid) 100%);
  color: #fff;
  animation: tickIn .28s ease;
}

@keyframes tickIn {
  from { opacity: 0; transform: translateY(-5px); }
  to   { opacity: 1; transform: none; }
}

.tick-pick {
  flex: none; font-family: Poppins, sans-serif; font-weight: 700;
  font-size: var(--fs-xs); color: var(--cyan); width: 34px;
  font-variant-numeric: tabular-nums;
}

.tick-body { min-width: 0; flex: 1; }

.tick-team {
  font-size: var(--fs-2xs); letter-spacing: .1em; text-transform: uppercase;
  color: rgba(255,255,255,.62); font-weight: 700;
}

.tick-name {
  display: flex; align-items: center; gap: 6px;
  font-size: var(--fs-sm); font-weight: 600; margin-top: 2px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.tick-tm { font-size: var(--fs-xs); color: rgba(255,255,255,.72); font-weight: 400; }

.ticker .mini {
  background: rgba(255,255,255,.16); color: #fff;
}

.ticker .mini:hover { background: rgba(255,255,255,.28); }

/* ---------- panels ---------- */

.panel { display: none; padding: 14px 12px 48px; }
.panel.on { display: block; }

/* ---------- setup screen ---------- */

.setup {
  max-width: 460px; margin: 18px auto;
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r-md); padding: 24px 22px 22px;
}

.setup { border-top: 3px solid var(--orange); }

.setup h2 {
  margin: 0 0 20px; font-family: Poppins, sans-serif;
  font-size: var(--fs-xl); font-weight: 700; letter-spacing: -0.02em;
}

.field { margin-bottom: 20px; }

.field label {
  display: block; margin-bottom: 7px;
  font-size: var(--fs-xs); font-weight: 700; letter-spacing: .05em;
  text-transform: uppercase; color: var(--ink-mid);
}

.field-row { display: flex; gap: 8px; }

select {
  flex: 1; width: 100%;
  font-family: Inter, sans-serif; font-size: var(--fs-sm); color: var(--ink);
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r-sm); padding: 10px 12px; cursor: pointer;
}

/* Three dropdowns across a row is comfortable when the options are "QB 1" and
   "RB 2". The league row's options are words, and on a phone that row gives
   each select 96px: "14 rounds" alone wants 67px of it, and the dropdown
   arrow the browser draws wants about 16 more, so the text ends up hard
   against the arrow with nothing between them. Widening the padding does not
   help — the arrow sits inside it and simply moves too. The row needs more
   width, so below 480px it takes two lines: the league's size on the first,
   scoring on its own. */
@media (max-width: 480px) {
  .field-row.wordy { flex-wrap: wrap; }
  .field-row.wordy > select { flex: 1 1 calc(50% - 4px); }
  .field-row.wordy > select:last-child { flex-basis: 100%; }
}

/* ---------- 16px, or iOS zooms the page ----------

   Safari zooms in on any field whose text is under 16px the moment it takes
   focus, and it does not zoom back out afterwards. So typing one line into
   the chat left the whole draft magnified and the manager pinching their way
   back to it — every time they said anything. Every field in the app was
   under the line: the selects at 14.5, chat and the GIF search at 12.5.

   16px is a threshold in someone else's browser, not a typographic opinion,
   which is why it is a flat override rather than a change to each rule. It is
   scoped to coarse pointers so the desktop sizes are untouched, and it covers
   selects as well as inputs because they zoom too.

   The other fix is `maximum-scale=1` on the viewport, and it is worse: it
   stops the zoom by taking pinch-zoom away from everybody, including the
   people who need it to read at all. Measured after the change at 375px and
   at 481px — no row overflows, and the league row still fits three across
   above its wrapping breakpoint.

   `!important` because every field here is styled through a class —
   `.chatform input` beats a bare `input` on specificity, and the first
   version of this rule silently moved the selects and left the chat box, the
   name and the player search exactly as they were. This is not a preference
   competing with those rules; it is a floor underneath all of them, and any
   field added later gets it without anybody remembering to come back here. */
@media (pointer: coarse) {
  input, select, textarea { font-size: var(--fs-base) !important; }
}

.hint { margin: 7px 0 0; font-size: var(--fs-xs); color: var(--ink-light); }

/* A roster that does not add up to the round count, or a league too big for
   the player pool. The draft cannot start until it is fixed. */
.hint.bad { color: var(--danger); font-weight: 600; }

.ghost {
  flex: none; font-family: Inter, sans-serif;
  font-size: var(--fs-sm); font-weight: 600; color: var(--link);
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r-sm); padding: 10px 16px; cursor: pointer;
}

.ghost:hover { background: var(--blue-pale); border-color: var(--blue-line); }

/* ---------- the scoring editor ----------
   A native <details>, so open and closed cost no JavaScript and keyboard
   and screen reader behaviour come for free. */

/* A collapsed box on the setup screen. Scoring was the only one for a long
   while; League and Draft with friends joined it when the screen stopped
   putting sixty controls in front of somebody who wanted to press one
   button. Each carries a summary of what is inside, so closed is still
   informative — the point is to answer "what league is this" without
   opening anything. */
.setupbox {
  background: var(--sunken); border: 1px solid var(--line);
  border-radius: var(--r-sm); margin-bottom: 20px;
}

.setupbox > summary {
  display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap;
  padding: 12px 15px; cursor: pointer;
  font-size: var(--fs-xs); font-weight: 700;
  letter-spacing: .05em; text-transform: uppercase; color: var(--ink-mid);
}

.setupbox > summary:hover { color: var(--ink); }

.box-sum {
  font-size: var(--fs-xs); font-weight: 500; letter-spacing: 0;
  text-transform: none; color: var(--ink-light);
}

.setupbox > div, .setupbox > .hint, .setupbox > button { margin-left: 15px; margin-right: 15px; }
.setupbox .section-label { margin: 14px 15px 6px; }
.setupbox > button { margin-bottom: 15px; }
.setupbox .hint { margin-bottom: 14px; }
/* The boxes hold .field blocks, whose own bottom margin would double up
   against the box padding. */
.setupbox .field:last-child { margin-bottom: 15px; }

.rulegrid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px 10px; }
@media (max-width: 420px) { .rulegrid { grid-template-columns: 1fr; } }

.rule { display: flex; align-items: center; gap: 8px; justify-content: space-between; }
.rule span { font-size: var(--fs-sm); color: var(--ink-mid); }

/* A yardage rule takes the divisor a league says out loud and shows the
   multiplier the engine uses underneath it, so neither number is a mystery.
   It spans both grid columns because it carries three controls, not one. */
.rule.per-yard { grid-column: 1 / -1; align-items: flex-start; }

.divisor { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; }
.divisor i { font-style: normal; font-size: var(--fs-xs); color: var(--ink-light); }
.divisor input { width: 62px; }
.divisor b {
  flex-basis: 100%; font-weight: 500;
  font-size: var(--fs-2xs); color: var(--ink-light);
  font-variant-numeric: tabular-nums;
}

/* A rule Sleeper does not forecast. It is information, not a warning: the
   rule works, it just cannot move a projection that never carried the stat.
   So it takes the muted ink rather than a colour that reads as an error. */
.rule .past {
  display: inline-block; margin-left: 6px;
  font-style: normal; font-size: var(--fs-2xs); font-weight: 700;
  letter-spacing: .05em; text-transform: uppercase;
  color: var(--ink-light);
  background: var(--fill); border-radius: var(--r-xs); padding: 1px 5px;
}

.hint .past {
  font-style: normal; font-weight: 700; font-size: var(--fs-2xs);
  letter-spacing: .05em; text-transform: uppercase;
  background: var(--fill); border-radius: var(--r-xs); padding: 1px 5px;
}

.rule input {
  flex: none; width: 66px; text-align: right;
  font-family: Inter, sans-serif; font-size: var(--fs-sm); color: var(--ink);
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r-sm); padding: 5px 7px;
}

.rule input:focus { outline: 2px solid var(--blue); outline-offset: 1px; border-color: var(--blue); }

/* iOS Safari zooms the whole page when it focuses an input smaller than 16px.
   With thirty-eight of them that would make the editor unusable on a phone,
   so they grow rather than the page jumping. */
@media (max-width: 640px) {
  .rule input { font-size: var(--fs-base); width: 74px; }
}

.primary {
  font-family: Inter, sans-serif; font-weight: 600; color: #fff;
  background: var(--orange-cta); border: 0; border-radius: var(--r-sm);
  padding: 9px 18px; font-size: var(--fs-sm); cursor: pointer;
}

.primary:hover { background: var(--orange-cta-hover); }

/* Not white on --ink-light. That pairing was 3.88:1 and became 2.83:1 when
   --ink-light was lightened to clear its own contrast bar — a disabled
   control is exempt from the rule, so nothing failed, and the label quietly
   got harder to read anyway. Muted text on a muted fill is the more honest
   way to say "off" in any case, and it measures 4.85. */
.primary:disabled {
  background: var(--fill); color: var(--ink-light); cursor: not-allowed;
}
.primary.big { width: 100%; padding: 14px; font-size: var(--fs-base); }

/* ---------- position filter ---------- */

.pos-filter { display: flex; gap: 4px; margin-bottom: 12px; overflow-x: auto; }

.pos-filter button {
  flex: none; font-family: Inter, sans-serif;
  font-size: var(--fs-sm); font-weight: 600; color: var(--ink-mid);
  background: none; border: 0; padding: 7px 12px;
  cursor: pointer; border-bottom: 2px solid transparent;
}

.pos-filter button.on { color: var(--link); border-bottom-color: var(--link); }

/* The have/need count that turns the filter into a roster-need display.
   Written by renderPlayerFilter(), and absent before a draft starts. */
.pos-filter .need {
  display: inline-block; margin-left: 5px;
  font-size: var(--fs-xs); font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--ink-light);
}

/* Short of a starting slot is the only state worth colouring. A filled slot
   is the normal case and does not need to shout about itself.

   `.met` used to take `--good`, which contradicted the line above it and
   read as a stop sign: green "1/1" against TE the moment you took one, on a
   roster that may hold three. It is the resting state now and inherits the
   muted count colour. The only other mark is `.full`, which is drawn when
   the app genuinely has no more of that position to offer — struck through,
   because that is a thing that has run out rather than a thing gone well. */
.pos-filter button.short .need { color: var(--t-warn-fg); }
.pos-filter button.full  .need { text-decoration: line-through; opacity: .75; }

/* ---------- injury designations ---------- */

.inj {
  display: inline-block;
  font-size: var(--fs-2xs); font-weight: 700; letter-spacing: .04em;
  padding: 2px 5px; border-radius: var(--r-xs);
  background: var(--fill); color: var(--ink-mid);
}

.inj.Q   { background: var(--t-warn-bg);    color: var(--t-warn-fg); }
.inj.D   { background: var(--t-orange-bg);  color: var(--t-orange-fg); }
.inj.O   { background: var(--t-bad-bg);     color: var(--t-bad-fg); }
.inj.IR  { background: var(--t-ir-bg);      color: var(--t-ir-fg); }
.inj.PUP { background: var(--t-purple-bg);  color: var(--t-purple-fg); }
.inj.NFI { background: var(--t-purple-bg);  color: var(--t-purple-fg); }
.inj.SUS { background: var(--t-neutral-bg); color: var(--t-neutral-fg); }

/* a top-150 player who has already been ruled out — the ADP feed
   always lags the injury feed by a few days */
tr.conflict { background: var(--row-bad); }
tr.conflict .nm { color: var(--t-bad-fg); }

/* ---------- avatars ---------- */

.avatar {
  position: relative; overflow: hidden;
  width: 46px; height: 46px; flex: none;
  border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-family: Poppins, sans-serif; font-weight: 700; font-size: var(--fs-base);
  letter-spacing: -0.02em;
}

.avatar.QB  { background: var(--qb-bg);  color: var(--qb-fg); }
.avatar.RB  { background: var(--rb-bg);  color: var(--rb-fg); }
.avatar.WR  { background: var(--wr-bg);  color: var(--wr-fg); }
.avatar.TE  { background: var(--te-bg);  color: var(--te-fg); }
.avatar.K   { background: var(--k-bg);   color: var(--k-fg); }
.avatar.DST { background: var(--dst-bg); color: var(--dst-fg); }

.avatar.sm { width: 34px; height: 34px; font-size: var(--fs-xs); }

/* The headshot sits on top of the initials. If it fails to load it
   removes itself and the initials show through, so a missing photo
   is never a broken image icon. */
.avatar img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; object-position: top center;
}

.avatar img.logo { object-fit: contain; padding: 5px; }

.freshness {
  margin: 14px 0 0; text-align: center;
  font-size: var(--fs-xs); color: var(--ink-light);
}

.freshness a { color: var(--link); text-decoration: none; }
.freshness a:hover { text-decoration: underline; }

/* ---------- tier and warning chips ---------- */

.chip {
  display: inline-block;
  font-size: var(--fs-2xs); font-weight: 700; letter-spacing: .02em;
  padding: 2px 7px; border-radius: var(--r-pill);
  background: var(--track); color: var(--ink-mid);
}

.chip.tier   { background: var(--t-neutral-bg); color: var(--t-neutral-fg); }
.chip.thin   { background: var(--t-warn-bg);    color: var(--t-warn-fg); }
.chip.last   { background: var(--t-bad-bg);     color: var(--t-bad-fg); }
.chip.bye    { background: var(--t-bye-bg);     color: var(--t-bye-fg); }

/* Where this app's model and the market disagree. Green is not "good player",
   it is "cheaper than he projects"; orange is the reverse. Both are already
   tint pairs that carry legible text under either theme. */
.chip.val    { background: var(--t-good-bg);    color: var(--t-good-fg); }
.chip.reach  { background: var(--t-orange-bg);  color: var(--t-orange-fg); }

/* ---------- suggestion cards ---------- */

.sug {
  display: flex; align-items: center; gap: 13px;
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r-md); padding: 12px 14px; margin-bottom: 9px;
}

.sug.top { border-color: var(--blue-line); box-shadow: 0 2px 10px rgba(44,107,237,.09); }

.sug-body { flex: 1; min-width: 0; }

.sug-name {
  font-family: Poppins, sans-serif;
  font-size: var(--fs-base); font-weight: 600; letter-spacing: -0.01em;
  line-height: 1.2;
}

.sug-meta {
  display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
  margin-top: 4px; font-size: var(--fs-xs); color: var(--ink-mid);
}

.sug-stats { margin-top: 3px; font-size: var(--fs-xs); color: var(--ink-light); }

.value-up   { color: var(--good); font-weight: 600; }
.value-down { color: var(--ink-light); }

/* ---------- the players toolbar ----------

   Both of these class names were in the markup with nothing behind them, and
   an unstyled rule fails quietly: the search box came out as a raw browser
   widget — Arial, an inset border, a grey the theme never chose — and the two
   toggles flowed as loose inline text. On a phone the second label broke
   across lines, stranding its checkbox on the row above and leaving "Roomy
   rows" sitting under the search box looking like a heading.

   A label must never break, which is why .check is inline-flex and nowrap:
   a checkbox on one line and its words on the next is not a control any
   more, it is two pieces of furniture. */
.toolbar {
  display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
  margin-bottom: 12px;
}

/* Matches the select on the setup screen, because they are the same kind of
   thing and were the same kind of thing before this rule existed. */
.toolbar input[type="search"] {
  flex: 1 1 190px; min-width: 0;
  font-family: Inter, sans-serif; font-size: var(--fs-sm); color: var(--ink);
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r-sm); padding: 9px 12px;
  -webkit-appearance: none; appearance: none;
}
.toolbar input[type="search"]::placeholder { color: var(--ink-light); }
.toolbar input[type="search"]:focus-visible {
  outline: 2px solid var(--blue); outline-offset: 1px; border-color: var(--blue);
}

.check {
  display: inline-flex; align-items: center; gap: 6px;
  white-space: nowrap; cursor: pointer;
  font-size: var(--fs-sm); color: var(--ink-mid); flex: none;
}
.check input { cursor: pointer; flex: none; margin: 0; }

/* On a phone the search and one toggle just fit, which put "Hide drafted"
   hard against the right edge and "Roomy rows" alone underneath. The search
   takes its own row instead and the two toggles sit together, which is the
   pairing they read as anyway. */
@media (max-width: 560px) {
  .toolbar input[type="search"] { flex-basis: 100%; }
}


/* ---------- player table ---------- */

table {
  width: 100%; border-collapse: collapse;
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r-md); overflow: hidden;
}

th {
  text-align: left; font-size: var(--fs-2xs); letter-spacing: .09em;
  text-transform: uppercase; color: var(--ink-light); font-weight: 700;
  padding: 10px 12px; background: var(--sunken); border-bottom: 1px solid var(--line);
}

td { padding: 10px 12px; border-bottom: 1px solid var(--hairline); vertical-align: middle; }
tr:last-child td { border-bottom: 0; }

.num { text-align: right; font-variant-numeric: tabular-nums; }
th.num { text-align: right; }

.nm { display: block; font-weight: 600; font-size: var(--fs-sm); line-height: 1.25; }

.meta {
  display: flex; align-items: center; gap: 6px;
  margin-top: 3px; font-size: var(--fs-xs); color: var(--ink-mid);
}

.badge {
  display: inline-block; min-width: 30px; text-align: center;
  font-size: var(--fs-2xs); font-weight: 700; letter-spacing: .02em;
  padding: 2px 6px; border-radius: var(--r-xs);
}

.badge.QB  { background: var(--qb-bg);  color: var(--qb-fg); }
.badge.RB  { background: var(--rb-bg);  color: var(--rb-fg); }
.badge.WR  { background: var(--wr-bg);  color: var(--wr-fg); }
.badge.TE  { background: var(--te-bg);  color: var(--te-fg); }
.badge.K   { background: var(--k-bg);   color: var(--k-fg); }
.badge.DST { background: var(--dst-bg); color: var(--dst-fg); }

/* Blue, and staying blue, while every other action in the app went orange.

   "One primary action colour" is a rule about the primary action — the one
   thing a screen is asking you to do. This is a row control, repeated on
   every player in a table that is 200 rows long, and 200 orange buttons is
   not emphasis, it is wallpaper. It would also outshout the actual primary
   on the same screen, which is the point of having one.

   So the division is by rank, not by whether the control does something:
   orange for the page-level action, blue for a repeated inline one. */
.draft-btn {
  font-family: Inter, sans-serif; font-size: var(--fs-sm); font-weight: 600;
  color: #fff; background: var(--blue); border: 0;
  border-radius: var(--r-sm); padding: 7px 14px; cursor: pointer; white-space: nowrap;
}

.draft-btn:hover { background: var(--blue-dark); }
.draft-btn:disabled { background: var(--fill); color: var(--ink-light); cursor: default; }

tr.drafted { background: var(--sunken); }
tr.drafted .nm { color: var(--ink-light); text-decoration: line-through; }
tr.drafted .meta, tr.drafted .num { opacity: .45; }

/* Overall: this app's own number, not the feed's, so it is the one figure in
   the row set in the ink colour rather than the muted one. */
.ovr { font-weight: 700; color: var(--ink); }
.ovr.good { color: var(--good); }
tr.drafted .ovr { color: var(--ink-light); }

/* ---------- compact rows ----------
   Drafting is comparison, and comparison needs rows adjacent. Compact is the
   default on the player table; the toolbar toggle removes the class rather
   than adding one, so the roomy version is the plain table styling above and
   nothing here has to be undone twice. */

#playerTable.compact td { padding: 5px 12px; }
#playerTable.compact .nm { font-size: var(--fs-sm); }
#playerTable.compact .meta { margin-top: 1px; font-size: var(--fs-xs); gap: 5px; }
#playerTable.compact .badge { min-width: 26px; font-size: var(--fs-2xs); padding: 1px 5px; }
#playerTable.compact .chip { font-size: var(--fs-2xs); padding: 1px 5px; }
#playerTable.compact .inj { font-size: var(--fs-2xs); padding: 1px 4px; }
#playerTable.compact .draft-btn { font-size: var(--fs-xs); padding: 5px 11px; }

/* ---------- draft board ---------- */

.board-scroll { overflow-x: auto; padding-bottom: 6px; }

/* Ten teams is only the default here. renderBoard() overwrites the column
   count and the minimum width to match the league that is actually set. */
.board {
  display: grid;
  grid-template-columns: 30px repeat(10, minmax(74px, 1fr));
  gap: 3px; min-width: 780px;

  /* Every round row is exactly one card tall, stated once here rather than as
     a min-height on the cells.

     A floor on the cell does not give a uniform board: a row is as tall as its
     tallest cell, so rows holding a card take the card's natural height and
     rows that are still empty take the floor. Set them to 56 and the cards
     came out 58, so the board had two row heights and every row jumped 2px the
     moment its first pick landed — on a pane simultaneously trying to keep the
     live pick centred. Matching the floor to the content by eye is the same
     bug waiting for a font to change.

     `grid-template-rows: auto` is the header, which sizes to itself; every
     round row after it is implicit and takes `grid-auto-rows`. */
  grid-template-rows: auto;
  grid-auto-rows: 58px;
}

.board .hd {
  background: var(--board-hd); color: var(--ink-mid);
  font-size: var(--fs-2xs); font-weight: 700; text-align: center;
  padding: 5px 3px 4px; border-radius: var(--r-xs);
}
/* The name owns the truncation, not the head. It used to sit on .hd itself,
   which was fine while a head held one line of text and clips the roster
   strip the moment it holds two. */
.board .hd-name {
  display: block;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* What each team has, four chips wide. See rosterStrip(): the positions are
   derived from FORCED_LATE rather than listed, so a league that changes shape
   cannot leave this behind.

   Every chip carries its own ground — white on a position solid, which is the
   contract those colours were darkened to meet — so the strip is legible on
   the board head and on the navy of your own column alike, and the header
   behind it is never part of the sum. */
.board .hd-roster {
  display: flex; gap: 2px; justify-content: center; margin-top: 3px;
}
.board .hd-pos {
  flex: 1 1 0; min-width: 0;
  border-radius: var(--r-xs);
  font-size: var(--fs-2xs); font-weight: 700; line-height: 1.3;
  color: #fff;
}
/* Nothing there yet, and drawn rather than dropped: the gap is the fact
   somebody is reading the strip for. Muted fill and the tertiary tone, which
   is the same pair a disabled control uses and clears its own bar — a
   position colour at zero would say "has one". */
.board .hd-pos.none { background: var(--fill); color: var(--ink-light); }
.board .hd-pos.QB  { background: var(--qb); }
.board .hd-pos.RB  { background: var(--rb); }
.board .hd-pos.WR  { background: var(--wr); }
.board .hd-pos.TE  { background: var(--te); }
.board .hd-pos.K   { background: var(--k); }
.board .hd-pos.DST { background: var(--dst); }

/* The head of the marked column, so the gold reads as one continuous column
   rather than as a run of separately outlined cells. Navy and white stay —
   gold is a mark and never type. */
.board .hd.me { background: var(--navy); color: #fff; box-shadow: inset 0 -3px 0 var(--mine); }

.board .rd {
  background: var(--well); color: var(--ink-light);
  font-size: var(--fs-2xs); font-weight: 700;
  display: flex; align-items: center; justify-content: center; border-radius: var(--r-xs);
}

/* No min-height here: the row owns it, above. Two places declaring how tall a
   cell is means one of them is wrong the first time the card changes. */
.board .cell {
  background: var(--fill); border-radius: var(--r-xs);
  padding: 5px 6px;
  font-size: var(--fs-2xs); line-height: 1.25; color: #fff;
}

/* Centred both ways. It was already centred horizontally and pinned to the
   top vertically, which was 6px above and 25 below at the old 42px height and
   became 6 above and 41 below when the cell grew to fit a card. Nobody would
   have called the old one wrong; the new one reads as a number that has slipped
   its box. Same failure as the lopsided tab pill, and found the same way —
   by eye first, then measured. */
/* --ink-mid rather than --ink-light, so the pick code and the overall number
   in the corner are not the same weight. Two numbers in one cell at one tone
   is two numbers competing; the code is the one being read. */
.board .cell.empty {
  position: relative;
  color: var(--ink-mid); font-variant-numeric: tabular-nums;
  display: flex; align-items: center; justify-content: center; gap: 4px;
}
/* How far away the pick is. Corner, tertiary tone, and out of the flow so it
   cannot push the code off centre. */
.board .cell-ovr {
  position: absolute; top: 3px; right: 5px;
  font-size: var(--fs-2xs); font-weight: 400; color: var(--ink-light);
}
/* The clock owns the cell it is in, so the corner number stands down. Two
   facts in a 74px box is one too many and the countdown is the one being
   watched — the same rule the direction arrow follows one line up. */
.board .cell.now .cell-ovr { display: none; }
/* Two rings, and they answer two different questions.

   Gold is *whose*: every cell in your column, drafted or not. The empty ones
   are the half that was missing — the class only went on a filled cell, so
   the board could tell you where you had been and not where you were going,
   which is the one thing a snake board is for. Four rounds out, in a
   twelve-team room, "when do I pick again" was a counting exercise.

   White is *where the draft is*, and it moves a cell at a time. It used to be
   a dashed blue border, which is the same blue as the outline beside it, the
   focus ring, the selected tab and the header on your turn.

   Both are drawn as inset box-shadows rather than as a border or an outline,
   and that is not a style preference:

   - A `border` is inside the box under `box-sizing: border-box`, so 2px of it
     eats 2px of the cell's padding — the on-the-clock cell was 4px narrower
     inside than every other cell on the board, and its text sat 2px higher.
   - An `outline` does not follow `border-radius` in every engine, and it
     cannot be layered, which the keyline needs.

   The keyline is the second half of the pair. See --mine: no single colour
   clears 3:1 on both a position solid and an empty cell in both themes, so
   the ring is gold-then-keyline and one of the two always has the surface. */
.board .cell.now {
  background: var(--card); color: var(--link); font-weight: 700;
  box-shadow: inset 0 0 0 2px var(--live-ring), inset 0 0 0 3px var(--ring-edge);
}
.board .cell.mine {
  box-shadow: inset 0 0 0 2px var(--mine), inset 0 0 0 3px var(--ring-edge);
}
/* Your own turn is both facts at once, so it draws both: white on the inside,
   where the draft is, and your gold immediately outside it. Letting one win
   would throw away the other, and this is the single cell on the board where
   they coincide. */
.board .cell.now.mine {
  box-shadow:
    inset 0 0 0 2px var(--live-ring),
    inset 0 0 0 3px var(--ring-edge),
    inset 0 0 0 5px var(--mine),
    inset 0 0 0 6px var(--ring-edge);
}

.board .cell.QB  { background: var(--qb); }
.board .cell.RB  { background: var(--rb); }
.board .cell.WR  { background: var(--wr); }
.board .cell.TE  { background: var(--te); }
.board .cell.K   { background: var(--k); }
.board .cell.DST { background: var(--dst); }

/* A card is four facts and a face: who, what and where, which way the order
   is travelling, and which pick it was. Laid out as a column so the foot can
   be pushed to the bottom whatever the name above it does. */
.board .cell:not(.empty) { display: flex; flex-direction: column; }

.board .cell b {
  display: block; font-weight: 700;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* No opacity here, and that is the whole point of this rule.

   It was `opacity: .85`, which measured 3.74 to 4.02 against the six position
   solids — every one of them under the bar, on every card on the board. The
   solids were darkened to put white at exactly 4.6, so there is no margin to
   spend: any translucency at all takes it below.

   That is the same finding as the header labels at .82 and .68, and it hid for
   the same reason plus one more. A sweep reading `color` sees `#fff` and
   reports 4.62, because `opacity` is a property on the element rather than a
   channel in the colour — a third way to lie about contrast, after alpha and
   gradients. Hierarchy on a strong colour comes from weight, which is what
   the 700 above and the 400 here are doing. */
.board .cell s {
  display: block; text-decoration: none; font-weight: 400;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}

/* Scoped to .board, because the board is the only thing with a foot. It was
   unscoped for one commit, while the hero shot wore the same three classes —
   the hero shows the name and the club only now, so leaving these global would
   be an invitation rather than a shared definition.

   margin-top: auto pins the foot to the bottom of the card, so a wrapped or
   ellipsised name never pushes the face out of line with its neighbours. */
.board .cell-foot {
  display: flex; align-items: center; gap: 4px;
  margin-top: auto; padding-top: 3px;
}

.board .cell-dir  { font-weight: 700; line-height: 1; }
.board .cell-pick { font-variant-numeric: tabular-nums; }

/* Pushed to the right edge by the margin rather than by a spacer, so a card
   whose photo failed to load closes up instead of leaving a hole — the
   error handler removes the element entirely. */
.board .cell-face {
  width: 20px; height: 20px; border-radius: 50%;
  margin-left: auto; object-fit: cover; flex: none;
  background: rgba(255, 255, 255, .15);
}

/* At phone width the cell is 74px and the foot wants about 58 of it, which
   fits until a name does not. The face is the first thing to give: it is the
   one item on the card that is decoration rather than fact. */
@media (max-width: 480px) {
  .board .cell-face { width: 16px; height: 16px; }
  .board .cell-foot { gap: 3px; }
}

.legend {
  display: flex; gap: 14px; flex-wrap: wrap;
  margin-top: 12px; font-size: var(--fs-xs); color: var(--ink-mid);
}

.sw { width: 11px; height: 11px; display: inline-block; border-radius: var(--r-xs); margin-right: 5px; vertical-align: -1px; }
.sw.QB { background: var(--qb); } .sw.RB { background: var(--rb); }
.sw.WR { background: var(--wr); } .sw.TE { background: var(--te); }
.sw.K  { background: var(--k); }  .sw.DST { background: var(--dst); }

/* ---------- roster ---------- */

.section-label {
  margin: 4px 0 8px; font-size: var(--fs-2xs); letter-spacing: .1em;
  text-transform: uppercase; color: var(--ink-light); font-weight: 700;
}

.roster {
  list-style: none; margin: 0 0 22px; padding: 0;
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r-md); overflow: hidden;
}

.roster li {
  display: flex; align-items: center; gap: 12px;
  padding: 11px 12px; border-bottom: 1px solid var(--hairline);
}

.roster li:last-child { border-bottom: 0; }

.slot {
  flex: none; width: 46px; text-align: center;
  font-size: var(--fs-2xs); font-weight: 700; color: var(--ink-mid);
  background: var(--well); border-radius: var(--r-xs); padding: 5px 0;
}

.slot.bn { color: var(--ink-light); }

.skeleton { flex: 1; height: 9px; max-width: 190px; border-radius: var(--r-xs); background: var(--track); }

.rname { font-weight: 600; font-size: var(--fs-sm); line-height: 1.25; }
.rmeta { font-size: var(--fs-xs); color: var(--ink-mid); margin-top: 2px; }
.rpick { margin-left: auto; font-size: var(--fs-xs); color: var(--ink-light); font-variant-numeric: tabular-nums; }

/* ---------- picks feed ---------- */

.round-divider {
  display: flex; align-items: center; gap: 10px;
  margin: 16px 0 10px; font-size: var(--fs-2xs); font-weight: 700;
  letter-spacing: .1em; text-transform: uppercase; color: var(--ink-light);
}

.round-divider:before, .round-divider:after {
  content: ""; flex: 1; height: 1px; background: var(--line);
}

.pick-card {
  display: flex; align-items: center; gap: 12px;
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r-md); padding: 10px 12px; margin-bottom: 8px;
}

.pick-card.mine { border-color: var(--mine); background: var(--row-mine); }
.pick-no { flex: none; width: 38px; font-family: Poppins, sans-serif; font-weight: 700; font-size: var(--fs-sm); color: var(--ink-mid); }
.pick-team { font-size: var(--fs-xs); color: var(--ink-light); margin-bottom: 2px; }
.pick-name { font-weight: 600; font-size: var(--fs-sm); line-height: 1.2; }
.pick-meta { display: flex; align-items: center; gap: 6px; margin-top: 3px; font-size: var(--fs-xs); color: var(--ink-mid); }

/* ---------- draft analysis ---------- */

.grade-hero {
  display: flex; align-items: center; gap: 16px;
  background: linear-gradient(120deg, var(--navy-deep), var(--navy) 60%, var(--navy-lift));
  color: #fff; border-radius: var(--r-md); padding: 18px 20px; margin-bottom: 16px;
}

.grade-letter {
  flex: none; width: 78px; height: 78px; border-radius: var(--r-lg);
  background: rgba(255,255,255,.13);
  display: flex; align-items: center; justify-content: center;
  font-family: Poppins, sans-serif; font-weight: 700; font-size: var(--fs-2xl);
  letter-spacing: -0.03em;
}

.grade-hero h3 { margin: 0 0 3px; font-family: Poppins, sans-serif; font-size: var(--fs-lg); font-weight: 700; }
.grade-hero p  { margin: 0; font-size: var(--fs-sm); color: rgba(255,255,255,.78); line-height: 1.5; }

.bars { background: var(--card); border: 1px solid var(--line); border-radius: var(--r-md); padding: 14px 16px; margin-bottom: 16px; }
.bar-row { margin-bottom: 12px; }
.bar-row:last-child { margin-bottom: 0; }

.bar-head {
  display: flex; justify-content: space-between; align-items: baseline;
  font-size: var(--fs-sm); margin-bottom: 5px;
}
.bar-head b { font-weight: 600; }
.bar-head span { color: var(--ink-light); font-size: var(--fs-xs); font-variant-numeric: tabular-nums; }

.bar-track { height: 8px; background: var(--track); border-radius: var(--r-xs); overflow: hidden; }

/* No radius on the fill. The track already rounds it by clipping, and giving
   the fill its own curve as well means it gets anti-aliased against the grey
   track and then clipped again — which leaves a full bar looking washed out
   at the right end instead of solid. */
.bar-fill  { height: 100%; background: var(--blue); }
.bar-fill.good { background: var(--good); }
.bar-fill.warn { background: var(--te); }
.bar-fill.bad  { background: var(--danger); }

.callouts { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 16px; }
@media (max-width: 620px) { .callouts { grid-template-columns: 1fr; } }

.callout {
  background: var(--card); border: 1px solid var(--line);
  border-left: 3px solid var(--blue); border-radius: var(--r-sm); padding: 11px 13px;
}
.callout.good { border-left-color: var(--good); }
.callout.bad  { border-left-color: var(--danger); }
.callout .lbl { font-size: var(--fs-2xs); letter-spacing: .1em; text-transform: uppercase; color: var(--ink-light); font-weight: 700; }
.callout .val { font-weight: 600; font-size: var(--fs-sm); margin-top: 3px; }
.callout .sub { font-size: var(--fs-xs); color: var(--ink-mid); margin-top: 2px; }

.standings { width: 100%; }
.standings td.rk { width: 26px; color: var(--ink-light); font-variant-numeric: tabular-nums; }
/* --pale-mine rather than --blue-pale: --blue-pale is still the hover on a
   ghost button and the selected sheet tab, which are states. This row is an
   identity. */
.standings tr.me { background: var(--pale-mine); }
.standings tr.me td { font-weight: 600; }
.standings td.gr { text-align: right; font-family: Poppins, sans-serif; font-weight: 700; }

.byebar { display: flex; gap: 3px; margin-top: 4px; }
.byebar i {
  flex: 1; height: 22px; border-radius: var(--r-xs); background: var(--track);
  font-style: normal; font-size: var(--fs-2xs); color: var(--ink-light);
  display: flex; align-items: center; justify-content: center;
}
.byebar i.w2 { background: var(--t-info-bg); color: var(--t-info-fg); }
.byebar i.w3 { background: var(--t-bye-bg);  color: var(--t-bye-fg); font-weight: 700; }
.byebar i.w4 { background: var(--t-bad-bg);  color: var(--t-bad-fg); font-weight: 700; }

.method { font-size: var(--fs-xs); color: var(--ink-light); line-height: 1.6; margin-top: 14px; }

/* ---------- player detail sheet ---------- */

.sheet-backdrop {
  position: fixed; inset: 0; z-index: 40;
  background: rgba(6,12,22,.62);
}

.sheet {
  position: fixed; z-index: 41;
  left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: min(680px, calc(100vw - 24px));
  max-height: calc(100vh - 60px);
  display: flex; flex-direction: column;
  background: var(--card); border-radius: var(--r-lg); overflow: hidden;
  box-shadow: var(--elev-4);
}

@media (max-width: 620px) {
  .sheet { width: 100vw; height: 100vh; max-height: none; border-radius: 0;
           left: 0; top: 0; transform: none; }
}

/* The club's colour arrives as --team, set per player in openSheet(). The
   default is here rather than there so a free agent, or a club we have no
   mark for, still gets a ring and a band — only the colour changes.

   position: relative is for the band below, and nothing inside this header is
   absolutely positioned, so it re-anchors nothing. .sheet-close sits on
   margin-left: auto and .avatar img is anchored to .avatar, which has its own
   position. Both checked, because changing position on a shared rule is how
   `top` quietly changes meaning. */
.sheet-head {
  --team: var(--navy-lift);
  position: relative;
  display: flex; align-items: center; gap: 14px;
  padding: 16px 18px;
  background: linear-gradient(120deg, var(--navy-deep), var(--navy) 65%, var(--navy-lift));
  color: #fff;
}

/* A band of the club's colour along the foot of the header, and a ring of it
   around the headshot. Neither carries text, which is the entire reason the
   accent is allowed to be the real brand colour: seven of the thirty-two
   cannot hold white type at all — Pittsburgh's gold measures 1.76:1 — so a
   team-coloured surface with anything written on it is not available. See the
   note above TEAM_ACCENT in app.js for what was measured.

   The band sits inside the header rather than on .sheet-tabs so it travels
   with the navy and cannot separate from it at a rounded corner. */
.sheet-head::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0;
  height: 3px; background: var(--team);
}


/* A ring of the club's colour outside the circle. box-shadow rather than a
   border, so the avatar keeps its 62px and the flex row does not shift. */
.sheet-head .avatar {
  width: 62px; height: 62px; font-size: var(--fs-lg);
  box-shadow: 0 0 0 3px var(--team);
}
.sheet-head h3 { margin: 0; font-family: Poppins, sans-serif; font-size: var(--fs-lg); font-weight: 700; letter-spacing: -.02em; }
.sheet-head .sub { display: flex; align-items: center; gap: 7px; margin-top: 4px; font-size: var(--fs-sm); color: rgba(255,255,255,.80); }
.sheet-head .facts { margin-top: 5px; font-size: var(--fs-xs); color: rgba(255,255,255,.62); font-variant-numeric: tabular-nums; }

/* Where he ranks, in the header rather than buried in the overview. The
   header is navy under both themes, so white at varying opacity is a colour
   this rule is allowed to name. */
.rankrow {
  display: flex; flex-wrap: wrap; gap: 18px;
  margin-top: 11px; padding-top: 10px;
  border-top: 1px solid rgba(255, 255, 255, .14);
}
.rankcell { display: flex; flex-direction: column; gap: 1px; }
.rankcell b {
  font-size: var(--fs-base); font-weight: 700; line-height: 1;
  font-variant-numeric: tabular-nums; color: #fff;
}
.rankcell span {
  font-size: var(--fs-2xs); letter-spacing: .1em; text-transform: uppercase;
  color: rgba(255, 255, 255, .55); font-weight: 700;
}

/* What the Juke score in that strip means, said next to the number instead of
   in a method note under three meters past the fold.

   White at .90 rather than the .55 the labels above use, and that is the
   contrast rule rather than a preference: this is a sentence to read, not a
   micro-label to glance at. The gradient's lightest stop is --navy-lift, and
   every stop has to clear on its own — .90 measures 7.87 there and better on
   the other two, where .62 would be 4.64 and .55 only 4.00. Hierarchy against
   the strip comes from size and weight, which is the only thing that works on
   a saturated surface. */
.jukenote {
  margin: 9px 0 0; padding-top: 8px;
  border-top: 1px solid rgba(255, 255, 255, .14);
  font-size: var(--fs-xs); line-height: 1.5;
  color: rgba(255, 255, 255, .90);
  font-variant-numeric: tabular-nums;
}
.jukenote b { color: #fff; font-weight: 700; }

/* ---------- our read ----------

   The column Sleeper gives to a wire service. Ours is the model talking, so
   it is styled as prose rather than as a stat block — the point is that it
   reads like an explanation. */
.ourread {
  background: var(--sunken); border: 1px solid var(--line);
  border-left: 3px solid var(--link);
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
  padding: 13px 15px; margin-bottom: 16px;
}
.ourread .section-label { margin: 0 0 8px; }
.ourread p { margin: 0 0 8px; font-size: var(--fs-sm); line-height: 1.6; color: var(--ink-mid); }
.ourread p:last-child { margin-bottom: 0; }
.ourread b { color: var(--ink); font-weight: 650; }

.readnote {
  margin-top: 10px !important;
  font-size: var(--fs-xs) !important; font-style: italic;
  color: var(--ink-light) !important;
}

/* A signal we decline to give, in place of the meter. No bar, because there is
   no value to draw; the label and the reason carry it. --ink-light is the
   tertiary tone and clears 4.5:1 on --card in both themes. */
.sig.unrated .sig-head span { color: var(--ink-light); font-weight: 600; }
.sig.unrated .sig-why { margin-top: 4px; }

/* What we forecast against what happened. --good and --bad are the tokens the
   grade already uses for the same idea, so a beat and a miss read the same way
   here as they do there. The games column dims below fifteen because that is
   where the measurement says the error stops being about the player. */
.logtbl.record td.beat   { color: var(--good); font-variant-numeric: tabular-nums; }
.logtbl.record td.missed { color: var(--danger); font-variant-numeric: tabular-nums; }
.logtbl.record td.short  { color: var(--ink-light); }

/* ---------- latest news ----------

   Sits under Our read and is deliberately quieter than it: ours is the model
   talking and has the accent border, this is somebody else's headline and is
   a plain list of links. The order on screen is the order of the argument —
   what we think, then what is being said.

   `:not([hidden])` rather than a bare display, because renderNews() toggles
   the property and an author display beats it. A solo draft once grew a chat
   panel exactly this way. */
.newsbox:not([hidden]) { display: block; margin-bottom: 16px; }
.newsbox .section-label { margin: 0 0 8px; }

.newsitem {
  display: block; text-decoration: none;
  padding: 10px 12px; margin-bottom: 6px;
  background: var(--sunken); border: 1px solid var(--line);
  border-radius: var(--r-sm);
}
.newsitem:hover { border-color: var(--link); }
.newsitem:last-of-type { margin-bottom: 0; }

.newshead {
  display: block; font-size: var(--fs-sm); font-weight: 650;
  line-height: 1.45; color: var(--link);
}
.newssum {
  display: block; margin-top: 3px;
  font-size: var(--fs-xs); line-height: 1.55; color: var(--ink-mid);
}
/* The source's name, which is the part that may not be dropped. --ink-light
   is the tertiary tone and clears 4.5:1 on --sunken in both themes; see the
   contrast note in CLAUDE.md before reaching for anything lighter. */
.newsfoot {
  display: block; margin-top: 5px;
  font-size: var(--fs-2xs); font-weight: 700;
  letter-spacing: .04em; text-transform: uppercase; color: var(--ink-light);
}

/* ---------- the game-log year selector ---------- */

.yearpick { display: flex; gap: 4px; margin-bottom: 10px; }

.yearpick button {
  font-family: Inter, sans-serif; font-size: var(--fs-xs); font-weight: 600;
  color: var(--ink-mid); background: var(--fill);
  border: 1px solid var(--line); border-radius: var(--r-sm);
  padding: 5px 11px; cursor: pointer; font-variant-numeric: tabular-nums;
}
.yearpick button:hover { background: var(--fill-hi); color: var(--ink); }
.yearpick button.on { background: var(--blue); border-color: var(--blue); color: #fff; }

.sheet-close {
  margin-left: auto; align-self: flex-start;
  width: 30px; height: 30px; flex: none;
  border: 0; border-radius: 50%;
  background: rgba(255,255,255,.16); color: #fff;
  font-size: var(--fs-lg); line-height: 1; cursor: pointer;
}

/* 8px below rather than 9, because the border-bottom is inside the box: with
   an even 9 the selected pill measured 9px of space above it and 10 below,
   which is small enough to be invisible as a number and quite visible as a
   lopsided chip. 8 + the 1px border is the 9 the top already had. */
.sheet-tabs {
  display: flex; gap: 4px; padding: 9px 12px 8px;
  border-bottom: 1px solid var(--line); overflow-x: auto;
}

.sheet-tabs button {
  flex: none; font-family: Inter, sans-serif; font-size: var(--fs-sm); font-weight: 600;
  color: var(--ink-mid); background: none; border: 0; border-radius: var(--r-pill);
  padding: 7px 14px; cursor: pointer; white-space: nowrap;
}

.sheet-tabs button.on { background: var(--blue-pale); color: var(--link); }

.sheet-body { overflow-y: auto; padding: 16px 18px 26px; }
.sheet-view { display: none; }
.sheet-view.on { display: block; }

/* signal meters */
.sig { margin-bottom: 16px; }
.sig-head { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: 6px; }
.sig-head b { font-size: var(--fs-sm); font-weight: 600; }
.sig-head span { font-size: var(--fs-xs); font-weight: 700; }
.sig-seg { display: flex; gap: 3px; }
.sig-seg i { flex: 1; height: 7px; border-radius: var(--r-xs); background: var(--track); }
.sig-seg i.on { background: var(--blue); }
.sig.good .sig-seg i.on { background: var(--good); }
.sig.good .sig-head span { color: var(--good); }
.sig.warn .sig-seg i.on { background: var(--te); }
.sig.warn .sig-head span { color: var(--t-warn-fg); }
.sig.bad  .sig-seg i.on { background: var(--danger); }
.sig.bad  .sig-head span { color: var(--danger); }
.sig-why { margin: 6px 0 0; font-size: var(--fs-xs); color: var(--ink-light); line-height: 1.55; }

.statgrid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8px; margin: 4px 0 18px; }
@media (max-width: 520px) { .statgrid { grid-template-columns: repeat(2, 1fr); } }
.statbox { background: var(--sunken); border: 1px solid var(--line); border-radius: var(--r-sm); padding: 9px 10px; text-align: center; }
.statbox .k { font-size: var(--fs-2xs); letter-spacing: .09em; text-transform: uppercase; color: var(--ink-light); font-weight: 700; }
.statbox .v { font-family: Poppins, sans-serif; font-weight: 700; font-size: var(--fs-lg); margin-top: 2px; }

/* Stat tables can outgrow a phone once receiving, rushing and returns
   are all present, so they scroll sideways inside the sheet. */
.tblscroll { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.tblscroll table { min-width: 100%; width: max-content; }

/* ---------- the sortable player grid ----------

   Fifteen columns beside a draft board. The numbers are the point, so they
   get tabular figures and as little padding as stays readable, and the one
   column that is words stays pinned while the rest scrolls under it. */

/* Two inherited properties have to go for the pinned columns to work at all.

   `border-collapse: collapse` hands the borders to the table, and Chrome
   then ignores `position: sticky` on a cell — the rule applies, the computed
   style says sticky, and the cell scrolls away regardless. Collapse also
   overrode the rank's width, so the offset the name sat at was wrong even
   before that.

   `overflow: hidden` — on the table, to clip the corner radius — makes the
   table a scroll container of its own, and a sticky cell sticks to the
   nearest one of those rather than to .tblscroll. The radius goes; the rows
   are already fenced by the panel around them.

   Every cell keeps its own border-bottom, so the horizontal rules survive
   the switch to separate borders. */
#playerTable.stats {
  border-collapse: separate; border-spacing: 0; overflow: visible;
  --rk-w: 44px;
}

#playerTable.stats th, #playerTable.stats td { white-space: nowrap; }

#playerTable.stats .num { text-align: right; font-variant-numeric: tabular-nums; }

/* Projection components sit quieter than ADP and our own score. They are
   supporting evidence, and fifteen columns at equal weight is a wall. */
#playerTable.stats td.stat { color: var(--ink-mid); }

/* The band above a group of columns: Proj, Rushing, Receiving, Passing. */
#playerTable.stats .bandrow th {
  padding: 5px 8px 3px;
  font-size: var(--fs-2xs); letter-spacing: .11em; text-transform: uppercase;
  color: var(--ink-light); font-weight: 700; text-align: center;
  border-bottom: 0;
}
#playerTable.stats .bandrow th.band {
  color: var(--link);
  border-left: 1px solid var(--hairline);
  border-right: 1px solid var(--hairline);
}

#playerTable.stats thead th.sortable { cursor: pointer; user-select: none; }
#playerTable.stats thead th.sortable:hover { color: var(--ink); background: var(--fill); }
#playerTable.stats thead th.sortable:focus-visible {
  outline: 2px solid var(--blue); outline-offset: -2px;
}
#playerTable.stats thead th.sorted { color: var(--link); }

/* Our own column, which is the one thing here that is not a feed. */
#playerTable.stats thead th.ours { color: var(--t-good-fg); }
#playerTable.stats thead th.ours.sorted { color: var(--good); }

/* Rank and name stay put while the stats scroll under them.

   Two cells rather than one, because the rank sits to the left of the name
   and pinning only the name would leave the rank sliding out from behind it.
   The rank gets a fixed width so the name has a known offset to sit at —
   there is no CSS way to say "start where the previous sticky cell ended".

   Each needs a background of its own: a transparent sticky cell shows the
   numbers travelling through it. */
#playerTable.stats .stick {
  position: sticky; z-index: 2;
  background: var(--card);
}
/* The rank's width is written once and the name's offset reads it, so the
   two cannot drift apart into a one-pixel seam that shows the numbers. */
#playerTable.stats .stick.rk {
  left: 0;
  width: var(--rk-w); min-width: var(--rk-w); max-width: var(--rk-w);
  padding-left: 10px; padding-right: 6px;
}
#playerTable.stats .stick.name {
  left: var(--rk-w);
  border-right: 1px solid var(--hairline);
}
#playerTable.stats thead .stick { z-index: 3; background: var(--sunken); }
#playerTable.stats tbody tr:hover .stick { background: var(--fill); }
#playerTable.stats tbody tr.drafted .stick { background: var(--card); }

/* The rank is a quiet number: it is the row's address, not a stat. */
#playerTable.stats td.stick.rk { color: var(--ink-light); font-size: var(--fs-xs); }

@media (max-width: 700px) {
  /* On a phone the pinned pair would eat the screen. Measured at 375px:
     rank plus name came to 336 of the 359 visible, leaving twenty-three
     pixels for the numbers — a grid you could only read one column at a
     time, from behind a name column that never moved.

     So the rank scrolls away with everything else, the name is capped, and
     every cell loses padding. That buys back about 180px of scroll width
     and puts three or four stat columns beside the name instead of none. */
  #playerTable.stats .stick.rk { position: static; }
  #playerTable.stats .stick.name { left: 0; --rk-w: 0px; max-width: 160px; }

  #playerTable.stats th, #playerTable.stats td { padding-left: 7px; padding-right: 7px; }

  /* The name truncates rather than wrapping, so rows stay one height and
     the list stays scannable. The meta line under it is allowed to wrap —
     losing the tier or a Reach chip off the side would be losing the
     reason to look at the row. */
  #playerTable.stats .stick.name .nm {
    display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  /* The line may break between chips, never inside one.

     This has to be block rather than the flex it is on a wide screen. Two
     things were fighting it. A flex row does not wrap unless it is told to,
     so "display: block" on the chip below could never give it a line of its
     own — flex items sit in a line whatever their display says. And the
     items that stayed on the line were squeezed instead, which is what broke
     the text inside them: "ATL · WR5" came apart into three stacked lines
     while a chip that would not shrink pushed out past the column and, being
     in a sticky cell that paints above the scrolling ones, drew itself
     straight over the ADP and OVR numbers.

     Block layout, one width-capped column, and the pieces below wrap between
     themselves and never inside. `.meta > *` is also finally true: the team
     and rank used to be a bare text node, which is an anonymous box that no
     selector can reach, so the nowrap it needed never applied to it. */
  #playerTable.stats .stick.name { overflow: hidden; }
  #playerTable.stats .stick.name .meta { display: block; white-space: normal; }
  #playerTable.stats .stick.name .meta > * { white-space: nowrap; margin-right: 5px; }

  /* The value/reach chip is the widest thing on the line and the only one
     that is a sentence. Given its own row it reads; left to wrap in with the
     badge and the team it drags them into three ragged lines that no longer
     look like a row. */
  #playerTable.stats .stick.name .meta .chip.val,
  #playerTable.stats .stick.name .meta .chip.reach {
    display: block; width: fit-content; margin: 3px 0 0;
  }
}

.logtbl { font-size: var(--fs-xs); }
.logtbl th { padding: 7px 6px; font-size: var(--fs-2xs); }
.logtbl td { padding: 6px; text-align: right; font-variant-numeric: tabular-nums; }
.logtbl td:first-child, .logtbl th:first-child { text-align: left; }
.logtbl tr.bye td { color: var(--ink-light); font-style: italic; }
.logtbl td.hi { background: var(--t-good-bg); font-weight: 600; }
.logtbl td.lo { background: var(--row-bad); }

.depthcol { margin-bottom: 16px; }
.depthcol h5 { margin: 0 0 6px; font-size: var(--fs-2xs); letter-spacing: .09em; text-transform: uppercase; color: var(--ink-light); font-weight: 700; }
.depthrow { display: flex; align-items: center; gap: 9px; padding: 7px 0; border-bottom: 1px solid var(--hairline); font-size: var(--fs-sm); }
.depthrow:last-child { border-bottom: 0; }
.depthrow .ord { width: 20px; font-family: Poppins, sans-serif; font-weight: 700; color: var(--ink-light); font-size: var(--fs-xs); }
.depthrow.self { font-weight: 600; color: var(--link); }

.name-link { cursor: pointer; }
.name-link:hover { color: var(--link); text-decoration: underline; }

.nodata { padding: 30px 10px; text-align: center; font-size: var(--fs-sm); color: var(--ink-light); }

/* ---------- empty states ---------- */

.empty {
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r-md); padding: 48px 24px; text-align: center;
}

.empty-title { margin: 0 0 4px; font-weight: 600; font-size: var(--fs-base); }
.empty-sub { margin: 0 auto; max-width: 34ch; font-size: var(--fs-sm); color: var(--ink-mid); line-height: 1.5; }
.empty .primary { margin-top: 18px; }

/* ==========================================================
   The site shell — everything outside a running draft
   ========================================================== */

.shellbar { background: var(--card); border-bottom: 1px solid var(--line); }

/* Three zones with the brand in the middle. The centre only holds its place
   if the two sides carry similar weight, which is why the left has a second
   link rather than the Rooms button on its own. */
.shell-inner {
  max-width: 1120px; margin: 0 auto;
  display: grid; grid-template-columns: 1fr auto 1fr;
  align-items: center; gap: 14px;
  padding: 10px 18px;
}

.shell-side { display: flex; align-items: center; gap: 4px; min-width: 0; }
.shell-side.end { justify-content: flex-end; gap: 8px; }

.shell-brand { display: flex; align-items: center; gap: 9px; text-decoration: none; }

/* Poppins, not Montserrat. Poppins is already loaded for the rest of the app,
   so the wordmark costs no extra font request. */
/* --mark-ink, not --ink: the word is half the logo, and --ink is a slightly
   different near-black, which made the shield read navy next to a black
   wordmark. One colour, one lockup. */
.wordmark {
  font-family: Poppins, sans-serif; font-weight: 700; font-size: var(--fs-lg);
  letter-spacing: -0.01em; color: var(--mark-ink); line-height: 1;
}

.navbtn, .navlink {
  font-family: Inter, sans-serif; font-size: var(--fs-sm); font-weight: 600;
  border: 0; border-radius: var(--r-sm); padding: 8px 12px;
  cursor: pointer; text-decoration: none; white-space: nowrap;
}

.navbtn { background: var(--fill); color: var(--ink); }
.navbtn:hover { background: var(--fill-hi); }
.navlink { background: none; color: var(--ink-mid); }
.navlink:hover { color: var(--ink); }

/* Brand orange is only 3.34:1 against white, so a filled button that carries
   white text uses the darker --orange-cta. */
.cta {
  font-family: Inter, sans-serif; font-size: var(--fs-sm); font-weight: 700;
  color: #fff; background: var(--orange-cta); border: 0; border-radius: var(--r-sm);
  padding: 9px 16px; cursor: pointer; text-decoration: none; white-space: nowrap;
}
.cta:hover { background: var(--orange-cta-hover); }
.cta.lg { font-size: var(--fs-base); padding: 13px 26px; }
.navlink.lg { font-size: var(--fs-sm); padding: 13px 10px; }

.chev {
  display: inline-block; width: 6px; height: 6px; opacity: .65;
  border-right: 2px solid currentColor; border-bottom: 2px solid currentColor;
  transform: rotate(45deg); margin: -3px 0 0 4px;
}
[aria-expanded="true"] .chev { transform: rotate(-135deg); margin-top: 2px; }

/* ---------- rooms ---------- */

.rooms-panel { border-top: 1px solid var(--line); background: var(--sunken); }

.rooms-inner {
  max-width: 1120px; margin: 0 auto; padding: 16px 18px 20px;
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;
}

.rooms-head {
  margin: 0 0 4px;
  font-family: Poppins, sans-serif; font-size: var(--fs-xl); font-weight: 700;
  letter-spacing: -.02em; color: var(--ink);
}

.rooms-sub { margin: 0 0 18px; font-size: var(--fs-sm); color: var(--ink-mid); }

/* ---------- the season arc ----------

   One column per phase, in the order a season happens. The columns hold
   different numbers of rooms and that is the point: they read as a timeline
   rather than as a grid with gaps in it. align-items: start keeps each
   column its own height instead of stretching the short ones. */
/* ---------- the doorway, and the rooms turning through it ----------

   Rebuilt after somebody who had never seen the page called it "the books
   with the room names". That is the correct read of what was there: a 4:5
   panel, hinged on its left edge, swinging open, with two embossed rectangles
   on its face. Every one of those is a hardback cover. None of them is a door.

   What makes a door read as a door is construction, not detail, and there are
   five parts to it. Grain, brass and a knob are the last of the five and the
   old version had only those.

   **Proportion.** A door is 36 by 80 inches — 1:2.2. The old opening was
   1:0.8, which is a book, a tablet or a picture frame. The opening is 1:2.15
   now, and it is the single change that does most of the work.

   **A wall with thickness.** A door is a hole cut in something. The old jamb
   was a 12px flat border, which is a mount rather than a wall. The reveal —
   the sides of the hole, seen through the opening — is *painted* rather than
   built from real 3D faces, and that is measured rather than lazy: the
   perspective has to stay long or the swinging panel magnifies until it
   spills over the section above it, and at that focal length a 40px reveal on
   a 280px doorway resolves to about three pixels of visible offset. Real
   geometry that renders as three pixels costs the same as a gradient and
   reads as nothing.

   **Thickness on the door itself.** This one *is* real 3D and has to be. The
   panel turns 64 degrees, so its leading edge swings round to face the reader,
   and a painted edge does not survive that. `.door-edge` is a face at 90
   degrees to the panel. After the proportion it is the strongest cue in here,
   because it is the one a book cover cannot fake.

   **A floor.** Books float; doors stand on something. The sill, the contact
   shadow under the leading edge and the light the open room throws onto the
   floor are one object between them, and without them the whole assembly is
   a rectangle on a background again.

   **A handle at human height.** The old knob was 13px on a 456px panel —
   2.9% of the width, dead centre vertically. A real handle sits at 1.05m on a
   2.03m door, which is 48% down, and is about 7% of the width. Small and
   centred reads as a clasp, which is one more thing books have and doors
   do not.

   Two constraints inherited from the version this replaces, both still live:

   Nothing between .doorway-outer and .door may set an overflow. Any value but
   visible establishes a flattening context that `transform-style: preserve-3d`
   cannot cross, and the door stops swinging and starts squashing. The
   interior clips; the frame does not.

   `margin: auto` is never used to centre the doorway. An auto margin on a
   grid item defeats the default stretch and makes the item shrink-wrap its
   content — and every child of .doorway is absolutely positioned, so its
   intrinsic width is zero. The two together collapse the whole thing while it
   still reports a perfectly healthy max-width. `justify-self` instead.

   The door swings *towards* the reader. Away is what it really does seen from
   inside the room, and at that angle it is edge-on, unlit and invisible — the
   physics is right and there is no picture.

   The whole assembly is dark under both themes, the same licence .hero-band
   takes, which is what lets everything in here name white and the navies
   directly. It is a lit object rather than a page surface, and it is the one
   place --elev-* does not apply: those model a single overhead light on a
   flat page, and this has a light of its own coming out of it. */

/* The list is capped at 560 so the block's right edge lands on the pillars'
   below it. At 1fr it ran to 624px for rows whose longest line is 124px, so
   the door and the list read as two objects with a gap rather than as one
   composition. */
.roomstage {
  display: grid;
  grid-template-columns: minmax(0, 480px) minmax(0, 1fr);
  gap: 40px; align-items: center; margin-bottom: 46px;
}

/* The door and its card. Relative, because the card overlaps the doorway. */
.doorstage { position: relative; min-height: 560px; display: flex; align-items: center; }

/* 2000px is a long lens and it is not a taste decision. A door swung towards
   the reader brings its leading edge closer, and closer means bigger: the
   magnification is P / (P - w·sin θ), which at 228px of door and 64 degrees is
   1.11 here and would be 1.42 at 700px. The shorter lens gives a far better
   reveal and renders the panel half again as tall as its own frame, over
   whatever sits above it.

   The angle is set by how much lit room is left to see, and that is a
   three-way trade rather than a preference: the panel covers cos θ of the
   opening, the card covers a fixed strip on the right, and what remains
   between them is the whole point of opening it. At 58° and a 264px card the
   room showed through a 59px slot — a door ajar rather than a door open.
   64° and 250px give 104px of the 228, which is 46% of the opening. Past
   about 73° the placard starts to squeeze and the panel stops reading as a
   door at all. */
.doorway-outer {
  position: relative;
  perspective: 2000px;
  width: 280px; padding: 44px 0;
}

.doorway {
  position: relative; width: 100%; aspect-ratio: 280 / 560;
  transform-style: preserve-3d;
}

/* ---- the wall ---- */

/* Lit from above, like everything else on the page. The grain is two
   near-invisible gradients rather than an image: at 4% alpha it does nothing
   you could point at and it stops 300 by 560 pixels of flat navy reading as
   a swatch. */
.wall {
  position: absolute; inset: 0;
  border-radius: var(--r-sm);
  background:
    linear-gradient(180deg, #2B4A6C 0%, #1B3350 42%, #0E1E36 100%),
    repeating-linear-gradient(90deg, rgba(255,255,255,.04) 0 1px, transparent 1px 7px);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, .13),
    inset 0 -40px 60px -40px rgba(0, 0, 0, .55);
}

/* Light from the open room falling on the wall around the opening. It is the
   cheapest thing in here and it is most of why the room reads as lit rather
   than as a dark rectangle with a picture in it. */
.wall-spill {
  position: absolute; inset: 0; border-radius: var(--r-sm);
  background: radial-gradient(58% 42% at 50% 62%, rgba(237, 96, 17, .20), transparent 70%);
  opacity: 0; transition: opacity .7s ease .12s;
}
.roomstage.open .wall-spill { opacity: 1; }

/* ---- the opening ----

   1:2.15, against a real door's 1:2.2. The inner shadow is the reveal: a hard
   dark line at the head, softer down the sides, and the two thin gradient
   edges below are the lit and unlit faces of the wall's thickness. */
.opening {
  position: absolute; left: 26px; right: 26px; top: 26px; bottom: 0;
  transform-style: preserve-3d;
  box-shadow:
    inset 0 8px 14px -6px rgba(0, 0, 0, .85),
    inset 8px 0 14px -8px rgba(0, 0, 0, .70),
    inset -8px 0 14px -8px rgba(0, 0, 0, .55),
    0 0 0 1px rgba(0, 0, 0, .45);
}
/* The reveal's two faces. The left one takes the light and the right one is
   in shade, which is the whole reason a hole looks like a hole. */
.opening::before, .opening::after {
  content: ""; position: absolute; top: 0; bottom: 0; width: 5px;
  pointer-events: none; z-index: 3;
}
.opening::before { left: 0;  background: linear-gradient(90deg, rgba(255,255,255,.16), transparent); }
.opening::after  { right: 0; background: linear-gradient(270deg, rgba(0,0,0,.55), transparent); }

/* Seen only while the room turns. Without it the swap rotates to edge-on and
   you look straight through the wall. */
.shaft {
  position: absolute; inset: 0;
  background: linear-gradient(180deg, #060C16, #02060C);
}

/* Room and door together, so a change of room reads as a drum revolving
   inside a fixed frame rather than as the wall itself spinning. */
.swap {
  position: absolute; inset: 0;
  transform-style: preserve-3d;
  transition: transform .46s cubic-bezier(.6, 0, .3, 1);
}
.roomstage.turning .swap { transform: rotateY(90deg); }

/* ---- the room beyond ---- */

.door-room {
  position: absolute; inset: 0; overflow: hidden;
  background: linear-gradient(180deg, #14263F 0%, #0A1526 62%, #060D18 100%);
}
/* The far wall, lit warm, and low enough that the light reads as coming from
   inside the room rather than from a lamp pointed at the reader. */
.room-far {
  position: absolute; inset: 0;
  background:
    radial-gradient(72% 44% at 50% 30%, rgba(237, 96, 17, .30), transparent 68%),
    radial-gradient(120% 60% at 50% 100%, rgba(237, 96, 17, .18), transparent 70%);
}
/* A floor inside the room, which is what gives the interior a bottom. The
   opening runs to the ground, so without it the room is a lit fog. */
.room-floor {
  position: absolute; left: 0; right: 0; bottom: 0; height: 26%;
  background: linear-gradient(0deg, rgba(237, 96, 17, .26), rgba(6, 13, 24, .10));
  box-shadow: inset 0 1px 0 rgba(255, 200, 150, .18);
}

/* ---- the door ---- */

.door {
  position: absolute; inset: 0;
  transform-origin: left center; transform: rotateY(0deg) translateZ(-3px);
  transition: transform .78s cubic-bezier(.22, .9, .24, 1.06);
  transform-style: preserve-3d;
  display: flex; align-items: center; justify-content: center;
}
.roomstage.open .door { transform: rotateY(-64deg) translateZ(-3px); }

/* Stiles, rails and two panels, at the proportions a real six-panel door
   uses: a tall upper panel, a lock rail at handle height, a shorter lower
   one. The old face carried two equal 30% rectangles, which is a book cover's
   embossing rather than joinery. */
.door-face {
  position: absolute; inset: 0; overflow: hidden;
  background:
    linear-gradient(102deg, #2A4972 0%, #1C3459 46%, #142743 100%);
  border: 1px solid rgba(255, 255, 255, .12);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .10);
}
.door-face::before, .door-face::after {
  content: ""; position: absolute; left: 11%; right: 11%;
  border-radius: var(--r-xs);
  background: linear-gradient(160deg, rgba(0,0,0,.22), rgba(255,255,255,.05));
  box-shadow:
    inset 0 2px 3px rgba(0, 0, 0, .55),
    inset 0 -1px 0 rgba(255, 255, 255, .10);
}
.door-face::before { top: 8%;  height: 35%; }
.door-face::after  { top: 57%; height: 32%; }

/* The panel's thickness, and the only genuinely 3D piece besides the swing.
   rotateY(-90deg) about the right edge lays this face straight back into the
   opening; at 58 degrees open it turns towards the reader and catches the
   light. Take this out and the door is a sheet of paper again. */
.door-edge {
  position: absolute; top: 0; bottom: 0; right: 0; width: 9px;
  transform-origin: right center; transform: rotateY(-90deg);
  background: linear-gradient(180deg, #4B6E99, #22395C);
}

/* A swung door turns away from the light. Without this the panel is as bright
   open as shut, which is the tell that it is a rectangle rather than a door. */
.door-shade {
  position: absolute; inset: 0;
  background: linear-gradient(270deg, rgba(0, 0, 0, .52), rgba(0, 0, 0, .08));
  opacity: 0; transition: opacity .78s ease;
}
.roomstage.open .door-shade { opacity: 1; }

/* 1.05m on a 2.03m door is 48% down, and a lever is about 7% of the width.
   A backplate and a lever, rather than a 13px stud in the vertical centre. */
.handle {
  position: absolute; right: 6%; top: 48%;
  width: 7px; height: 34px; margin-top: -17px; border-radius: var(--r-pill);
  background: linear-gradient(140deg, #E8D294, #C9A227 55%, #8E7015);
  box-shadow: 0 1px 3px rgba(0, 0, 0, .6), inset 0 1px 0 rgba(255, 255, 255, .5);
}
.handle::before {
  content: ""; position: absolute; right: 4px; top: 9px;
  width: 24px; height: 6px; border-radius: var(--r-pill);
  background: linear-gradient(180deg, #EBD79F, #B8901D);
  box-shadow: 0 1px 3px rgba(0, 0, 0, .55), inset 0 1px 0 rgba(255, 255, 255, .45);
}

/* The placard. Brass rather than a brand colour on purpose — it is furniture,
   and it must not read as a position solid or a club mark. */
.placard {
  position: relative; width: 74%; padding: 9px 10px; text-align: center;
  border-radius: var(--r-xs);
  background: linear-gradient(158deg, #E6CE86, #C9A227 52%, #9A7B18);
  border: 1px solid rgba(0, 0, 0, .35);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .45), 0 3px 8px rgba(0, 0, 0, .5);
}
.placard-name {
  display: block; font-size: var(--fs-xs); font-weight: 700;
  letter-spacing: .1em; text-transform: uppercase;
  color: #23180A; text-shadow: 0 1px 0 rgba(255, 255, 255, .35);
  line-height: 1.25;
}
.placard::before, .placard::after {
  content: ""; position: absolute; top: 50%; margin-top: -2px;
  width: 4px; height: 4px; border-radius: 50%;
  background: radial-gradient(circle at 30% 30%, #6B5310, #3A2C06);
}
.placard::before { left: 6px; }
.placard::after { right: 6px; }

/* ---- the ground ----

   Three elements that are one object: the sill the door closes onto, the
   light the open room throws forward, and the shadow the assembly casts.
   Together they are what stops the whole thing floating. */
.sill {
  position: absolute; left: 18px; right: 18px; bottom: -6px; height: 8px;
  border-radius: 0 0 var(--r-xs) var(--r-xs);
  background: linear-gradient(180deg, #35547A, #16263D);
  box-shadow: 0 2px 4px rgba(0, 0, 0, .6);
}
.floor-spill {
  position: absolute; left: -14%; right: -14%; bottom: 4px; height: 76px;
  border-radius: 50%;
  background: radial-gradient(50% 60% at 50% 0%, rgba(237, 96, 17, .34), transparent 72%);
  filter: blur(9px);
  opacity: 0; transition: opacity .7s ease .18s;
  pointer-events: none;
}
.roomstage.open .floor-spill { opacity: 1; }
.floor-shadow {
  position: absolute; left: 4%; right: 4%; bottom: 12px; height: 46px;
  border-radius: 50%;
  background: radial-gradient(50% 58% at 50% 50%, rgba(3, 8, 16, .60), transparent 72%);
  filter: blur(11px);
  pointer-events: none;
}

/* ---- the room's own words ----

   Outside the opening, overlapping it, which is the arrangement that lets the
   door be door-shaped at all. It carries its own dark ground under both
   themes, so white and the band tones are legal on it. */
.room-card {
  position: absolute; right: 0; top: 50%; transform: translateY(-50%);
  width: 250px; z-index: 2;
  padding: 18px 20px 20px;
  border-radius: var(--r-md);
  border: 1px solid rgba(255, 255, 255, .13);
  background: linear-gradient(158deg, rgba(26, 48, 78, .96), rgba(9, 19, 34, .97));
  box-shadow: var(--elev-4);
  backdrop-filter: blur(6px);
}
/* The card arrives once the door is out of the way, rather than sitting there
   waiting to be uncovered. */
.room-card > * {
  opacity: 0; transform: translateX(-8px);
  transition: opacity .42s ease, transform .42s ease;
}
.roomstage.open .room-card > * { opacity: 1; transform: none; }
.roomstage.open .door-room-status { transition-delay: .30s; }
.roomstage.open .door-room-name   { transition-delay: .36s; }
.roomstage.open .door-room-blurb  { transition-delay: .42s; }
.roomstage.open .door-room-season { transition-delay: .48s; }
.roomstage.open .room-enter       { transition-delay: .54s; }

.door-room-status {
  display: inline-block; margin-bottom: 9px;
  padding: 3px 9px; border-radius: var(--r-pill);
  font-size: var(--fs-2xs); font-weight: 700;
  letter-spacing: .12em; text-transform: uppercase;
  color: #FFC49B; background: rgba(237, 96, 17, .18);
  border: 1px solid rgba(237, 96, 17, .38);
}
.door-room-status.soon {
  color: #A9BED6; background: rgba(255, 255, 255, .07);
  border-color: rgba(255, 255, 255, .16);
}
/* Literal white, not a token. --band-ink is declared on .hero-band rather
   than on :root, so out here it resolves to nothing, the property falls back
   to inherit, and the room's name picks up the page's dark ink and vanishes
   against a dark card in the light theme. The card carries its own ground in
   both themes, which is exactly the case where naming white is allowed. */
.door-room-name {
  margin: 0 0 8px; font-size: var(--fs-lg); font-weight: 700;
  color: #fff; line-height: 1.2;
}
.door-room-blurb {
  margin: 0; font-size: var(--fs-sm); line-height: 1.5; color: #D3DFEC;
}
.door-room-season {
  display: block; margin-top: 12px;
  font-size: var(--fs-2xs); font-weight: 700;
  letter-spacing: .12em; text-transform: uppercase; color: #8DA2BB;
}
/* Deliberately not orange. One primary action per screen and the hero above
   already has it; this is the same destination reached from further down the
   page, so it states rather than shouts. */
.room-enter {
  display: inline-block; margin-top: 14px;
  padding: 8px 14px; border-radius: var(--r-sm);
  font-size: var(--fs-sm); font-weight: 700; text-decoration: none;
  color: #fff; background: rgba(255, 255, 255, .10);
  border: 1px solid rgba(255, 255, 255, .22);
  transition: background .15s ease, border-color .15s ease;
}
.room-enter:hover { background: rgba(255, 255, 255, .17); border-color: rgba(255, 255, 255, .34); }
.room-enter:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
.room-enter::after { content: " \2192"; }
.room-enter[hidden] { display: none; }


/* ---------- the list beside the door ---------- */
.roomlist { display: flex; flex-direction: column; gap: 18px; max-width: 560px; }
.rl-group { display: flex; flex-direction: column; gap: 4px; }
/* The rule above each phase is what makes the three read as one timeline
   rather than three unrelated lists — carried over from the season columns
   this replaced, where it did the same job across rather than down. */
.rl-phase {
  margin: 0 0 4px; padding-bottom: 7px;
  border-bottom: 2px solid var(--blue-line);
  font-size: var(--fs-2xs); font-weight: 700;
  letter-spacing: .12em; text-transform: uppercase; color: var(--link);
}

.rl {
  display: block; width: 100%; text-align: left;
  background: transparent; border: 1px solid transparent;
  border-radius: var(--r-sm); padding: 9px 12px;
  font: inherit; color: var(--ink-mid); cursor: pointer;
  text-decoration: none;
  transition: background .15s ease, border-color .15s ease, color .15s ease;
}
.rl:hover { background: var(--fill); color: var(--ink); }
.rl:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
.rl.on { background: var(--fill); border-color: var(--line); color: var(--ink); }
.rl-name { display: block; font-size: var(--fs-sm); font-weight: 700; color: var(--ink); }
.rl-meta {
  display: block; margin-top: 2px;
  font-size: var(--fs-2xs); font-weight: 700;
  letter-spacing: .1em; text-transform: uppercase; color: var(--ink-light);
}
/* The open room keeps the orange edge it had as a card. */
.rl.live { border-left: 3px solid var(--orange); padding-left: 10px; }
.rl.live .rl-meta { color: var(--t-orange-fg); }

@media (max-width: 760px) {
  .roomstage { grid-template-columns: 1fr; gap: 22px; }

  /* The card can no longer overlap the doorway — at this width there is
     nothing to overlap into — so the stage stops being a canvas and becomes
     a column, and the card goes under the door.

     justify-self, never `margin: 0 auto`: an auto margin on a grid item
     defeats stretch and makes it shrink-wrap, and every child of .doorway is
     absolutely positioned — so its intrinsic width is zero and the whole
     doorway collapses while still reporting a healthy max-width. */
  .doorstage {
    flex-direction: column; align-items: center;
    min-height: 0; justify-self: center; width: 100%;
  }
  .doorway-outer { width: min(74vw, 250px); padding: 24px 0 12px; }
  .room-card {
    position: static; transform: none;
    width: 100%; max-width: 340px; margin-top: 6px;
  }
  .roomstage.open .room-card > * { transform: none; }

  /* The door opens further on a phone.

     A swung door covers cos(θ) of the opening, so opening it *further* is
     what shows more of the room behind it — the intuition runs the other way
     and it is wrong. It mattered more when the copy sat inside the opening
     and the panel ran over the words it was supposed to be revealing; now
     that the card is outside, this is simply about how much lit room a
     250px-wide doorway can show. Past about 73° the placard starts to squeeze
     and the door stops reading as a door. */
  .roomstage.open .door { transform: rotateY(-66deg) translateZ(-3px); }
}

/* The header panel is a flat list and can reflow to any column count. The
   landing arc cannot: at two columns the third phase drops to a second row
   and stops reading left to right, which is the only thing the layout was
   for. So it holds three columns as long as they are usable and then goes
   to one, where the phases stack in season order and the arc still reads —
   just vertically. */
@media (max-width: 860px) { .rooms-inner { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 560px) { .rooms-inner { grid-template-columns: 1fr; } }

@media (max-width: 700px) {
  .roomgrid { grid-template-columns: 1fr; gap: 22px; }
}

.room {
  display: block; text-decoration: none;
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r-md); padding: 13px 15px;
  transition: border-color .15s ease;
}

a.room:hover { border-color: var(--blue-line); }
.room.soon { opacity: .66; }
.room b { display: block; font-size: var(--fs-sm); font-weight: 700; color: var(--ink); }
.room .blurb { display: block; font-size: var(--fs-sm); color: var(--ink-mid); margin-top: 4px; line-height: 1.5; }

.pill {
  display: inline-block; margin-top: 9px;
  font-size: var(--fs-2xs); font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  padding: 3px 7px; border-radius: var(--r-pill);
  background: var(--t-orange-bg); color: var(--t-orange-fg);
}
.pill.grey { background: var(--t-neutral-bg); color: var(--t-neutral-fg); }

/* ==========================================================
   OPTION 2 — the open room takes the colour
   Self-contained. Deleting this block returns all six cards
   to identical grey; the "live" class in roomCard() is
   harmless on its own.
   ==========================================================

   The room grid is the largest region on the landing page — 427px tall,
   full width — and it was six identical grey rectangles.

   Colouring all six was the obvious version and is worse. Grey on the
   five that do not open yet is not decoration withheld: it is the
   difference between a door and a plan, and it is the fastest way to
   read this grid. So the colour marks the one room you can walk into,
   which makes it information rather than paint, and the grid needs no
   second pass the day the Waiver Room ships — it just moves.

   Brand orange, not the blue the phase rules use, because the one thing
   on this page already wearing it is the button that does the same job.
   --orange is the same value under both themes, so it can be named here
   directly. */

.room.live {
  /* 3px border plus 13px padding is the 16px the other cards get from
     15px plus their 1px border, so every card's text starts on the same
     line down the column. */
  border-left: 3px solid var(--orange);
  padding-left: 13px;
  background: var(--card);
}

/* Longhands, and border-left-color restated rather than left to cascade.
   a.room:hover sets all four to --blue-line at (0,2,1), which outranks the
   (0,2,0) of .room.live — so without this line the orange bar turns blue
   under the pointer, on the one card the colour is there to mark. */
a.room.live:hover {
  border-top-color: var(--line);
  border-right-color: var(--line);
  border-bottom-color: var(--line);
  border-left-color: var(--orange);
  background: var(--fill);
}

/* Solid rather than the tint the other pills carry, because this is the
   only badge on the page that means "you can press this". White on
   --orange-cta is 5.18:1; on the brand --orange it would be 3.34:1,
   which is the whole reason the darker token exists. */
.room.live .pill {
  background: var(--orange-cta); color: #fff;
}

/* ---------- landing ---------- */

/* Not ".home" — that is already the header's home button, and the collision
   collapsed this section to zero width. */
/* The 1120px column used to live here. It moved down to .landing-inner and
   .band-inner so the navy band can run edge to edge between them. */
.landing { padding: 0 0 64px; }

.landing-inner { max-width: 1120px; margin: 0 auto; padding: 0 18px; }

.hero { max-width: 720px; margin: 0 auto; text-align: center; padding: 66px 0 42px; }

/* The slogan. Orange because it is the brand's one warm colour and this is
   the brand's one line. Through --orange-ink rather than --orange: at 11px
   bold this is small text, needs 4.5:1, and the brand value only manages
   3.11:1 on the light page. Small and widely spaced, so it introduces the
   headline rather than competing with it. */
.hero-slogan {
  margin: 0 0 10px;
  font-family: Inter, sans-serif;
  font-size: var(--fs-xs); font-weight: 700;
  letter-spacing: .18em; text-transform: uppercase;
  color: var(--orange-ink);
}

.hero h1 {
  margin: 0 0 16px; font-family: Poppins, sans-serif;
  font-size: var(--fs-3xl); font-weight: 700; letter-spacing: -0.03em; line-height: 1.12;
}

.hero-sub { margin: 0 0 26px; font-size: var(--fs-lg); line-height: 1.65; color: var(--ink-mid); }
.hero-cta { display: flex; gap: 12px; justify-content: center; align-items: center; flex-wrap: wrap; }

@media (max-width: 620px) {
  .hero { padding: 42px 0 32px; }
  .hero h1 { font-size: var(--fs-2xl); }
  .hero-sub { font-size: var(--fs-base); }
}

.landing-resume {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  background: var(--card); border: 1px solid var(--blue-line);
  border-radius: var(--r-md); padding: 14px 16px; margin: 0 0 36px;
}
.landing-resume p { margin: 0; font-size: var(--fs-sm); }
.landing-resume .sub { font-size: var(--fs-xs); color: var(--ink-mid); margin-top: 3px; }
.landing-resume .btnrow { margin-left: auto; display: flex; gap: 8px; }

/* ---------- claim and proof ----------

   Three claims down the left, and the thing each one claims running on the
   right. The list is the control as well as the index, the same shape the
   rooms door uses, and for the same reason: a visitor who wants to look at
   one of them should not have to wait for the timer to come round.

   The stage is a fixed minimum height. Three stages of different lengths in a
   box that sizes to its contents means the section grows and shrinks under
   the reader every seven seconds, and anything below it walks up and down the
   page — which is the failure a carousel usually ships with. */
.proof {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 44px; align-items: center;
  border-top: 1px solid var(--line); padding-top: 34px;
}

.proof-eyebrow {
  margin: 0 0 6px; font-size: var(--fs-2xs); font-weight: 700;
  letter-spacing: .12em; text-transform: uppercase; color: var(--link);
}
.proof-head { margin: 0 0 8px; font-size: var(--fs-2xl); line-height: 1.1; }
.proof-sub {
  margin: 0 0 20px; font-size: var(--fs-sm); line-height: 1.6; color: var(--ink-mid);
  max-width: 44ch;
}

.proof-list { display: flex; flex-direction: column; gap: 4px; }

/* A button, not a div with a colour on it. This section is a control and the
   landing page has already shipped one thing that looked clickable and was
   not — see the rail's "My Team" row, which rendered, contrasted and did
   nothing for as long as it existed. */
.pf {
  display: block; width: 100%; text-align: left; font: inherit;
  background: transparent; border: 1px solid transparent;
  border-left: 3px solid transparent;
  border-radius: var(--r-sm); padding: 11px 14px;
  color: var(--ink-mid); cursor: pointer;
  transition: background .15s ease, border-color .15s ease;
}
.pf:hover { background: var(--fill); }
.pf:focus-visible { outline: 2px solid var(--blue); outline-offset: 2px; }
.pf.on {
  background: var(--fill); border-color: var(--line);
  border-left-color: var(--orange); color: var(--ink);
}
.pf b { display: block; font-size: var(--fs-sm); font-weight: 700; color: var(--ink); margin-bottom: 3px; }
.pf span { display: block; font-size: var(--fs-xs); line-height: 1.55; }

.proof-stage {
  min-height: 306px;
  display: flex; align-items: center;
}
.proof-stage.empty { display: none; }

.pstage {
  width: 100%;
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r-md); padding: 16px 18px 18px;
  box-shadow: var(--elev-2);
}
.pstage-head {
  display: flex; align-items: baseline; justify-content: space-between; gap: 10px;
  margin-bottom: 10px;
}
.pstage-label {
  font-size: var(--fs-2xs); font-weight: 700;
  letter-spacing: .1em; text-transform: uppercase; color: var(--ink-light);
}
.pstage-stamp { font-size: var(--fs-2xs); color: var(--ink-light); font-variant-numeric: tabular-nums; }

/* The dial is the rule being changed, and the point of the claim, so it is
   the one thing in the stage drawn at full strength. */
.pstage-dial { display: flex; gap: 4px; }
.pd {
  min-width: 30px; text-align: center;
  padding: 2px 7px; border-radius: var(--r-pill);
  font-size: var(--fs-xs); font-weight: 700;
  background: var(--fill); color: var(--ink-light);
  transition: background .3s ease, color .3s ease;
}
.pd.on { background: var(--orange-cta); color: #fff; }

.pstage-note { margin: 0 0 10px; font-size: var(--fs-xs); color: var(--ink-mid); }
.pstage-foot { margin: 11px 0 0; font-size: var(--fs-2xs); line-height: 1.5; color: var(--ink-light); }

.pstage-rows { list-style: none; margin: 0; padding: 0; }
.prow {
  display: flex; align-items: center; gap: 9px;
  padding: 7px 0; border-bottom: 1px solid var(--hairline);
  font-size: var(--fs-sm);
}
.prow:last-child { border-bottom: 0; }
.prow-rank {
  width: 14px; flex: none; text-align: right;
  font-size: var(--fs-xs); color: var(--ink-light); font-variant-numeric: tabular-nums;
}
.prow-name {
  flex: 1 1 auto; min-width: 0; font-weight: 600; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.prow-num { flex: none; font-weight: 700; font-variant-numeric: tabular-nums; color: var(--ink); }
.prow-score { color: var(--orange-ink); }

/* The subtraction, printed rather than asserted. Tabular figures so the three
   numbers line up down the column as the players change. */
.prow-sums {
  flex: none; display: flex; align-items: baseline; gap: 3px;
  font-size: var(--fs-xs); color: var(--ink-mid); font-variant-numeric: tabular-nums;
}
.prow-sums b { font-weight: 700; color: var(--ink); }
.prow-sums i { font-style: normal; color: var(--ink-light); padding: 0 1px; }
.prow-gap { color: var(--ink) !important; }

/* Every chip carries its own ground, the same contract the board head's
   roster strip runs under: white on a position solid, so the surface behind
   it is never part of the sum. */
.pos-chip {
  flex: none; min-width: 30px; text-align: center;
  padding: 2px 5px; border-radius: var(--r-xs);
  font-size: var(--fs-2xs); font-weight: 700; color: #fff;
}
.pos-chip.QB  { background: var(--qb); }
.pos-chip.RB  { background: var(--rb); }
.pos-chip.WR  { background: var(--wr); }
.pos-chip.TE  { background: var(--te); }
.pos-chip.K   { background: var(--k); }
.pos-chip.DST { background: var(--dst); }

@media (max-width: 860px) {
  .proof { grid-template-columns: 1fr; gap: 26px; }
  .proof-stage { min-height: 0; }
  .prow-sums { display: none; }   /* the name and the score survive; the sum is the first thing a phone gives up */
}

.landing-foot { margin: 38px 0 0; text-align: center; font-size: var(--fs-xs); color: var(--ink-light); }
.landing-foot p { margin: 0 0 4px; }
.landing-foot p:last-child { margin-bottom: 0; }

/* ==========================================================
   OPTION 1 — the navy hero band
   Self-contained. Deleting this block and the two wrapper
   divs in index.html returns the landing page to flat.
   ==========================================================

   The homepage was two tones ten points apart — --page #0E151E under
   --card #18212D — across a thousand square pixels, with the one
   saturated thing on it being the CTA at 0.8% of the area. This is the
   second surface.

   It is brand navy specifically because --navy-deep, --navy and
   --navy-glow are byte-identical in both themes. That is what lets
   everything inside name white and the --band-* values below directly
   without a light override: the rule at the top of this file forbids a
   colour that moves between themes, and this band never moves. It is
   the same licence .appbar.my-turn already uses.

   It stops at the header rather than continuing up through it. Turning
   .shellbar navy would have to turn .appbar navy with it — CLAUDE.md
   requires the two to share a surface so a CPU on the clock looks like
   the homepage — and that is a change to the draft room, not to the
   landing page. */

.hero-band {
  /* Light-on-navy, blue-tinted rather than neutral grey, which is the one
     trick DESIGN-DIRECTION.md singles out as making a dark surface read as
     designed. Contrast on --navy: 8.7:1 mid, 5.5:1 dim, both over 4.5. */
  --band-ink:     #FFFFFF;
  --band-ink-mid: #B9CBE0;
  --band-ink-dim: #8AA2BE;

  /* The linear part holds --navy-deep flat for 112px before it moves. The
     score strip is 102px tall and sits in that flat region, so the fades at
     its two ends can match the background exactly with a single colour —
     over a moving gradient they could not.

     The radial is --navy-glow at 45%, centred below the band so it lifts the
     area behind the button and never reaches the strip. Written as rgba
     rather than through the token because a colour cannot be interpolated
     inside rgba(); color-mix() could, and would take the whole declaration
     down with it on any browser that did not know the function. */
  background:
    /* The fade out, painted over everything below it. The band used to end at
       --navy against a --page of --navy's own 1.27:1 — and the radial below
       put the band at its brightest exactly where it was cut off, which is
       what made that edge read as a mistake rather than as a boundary.

       --page is the one value in this block that does move between themes,
       and it has to: the band's job at this edge is to arrive at whatever
       page it is sitting on. In light that is navy melting into #F5F7FA,
       which is a far bigger step than dark's and needs the ramp more.

       64px, and .hero carries at least 78px of bottom padding under both
       breakpoints, so no text ever sits inside the fade. That clearance is
       the whole constraint here — --band-ink-mid over a light-theme fade
       would lose its contrast on the way down. */
    linear-gradient(to top, var(--page), transparent 64px),
    radial-gradient(900px 380px at 50% 118%, rgba(14,90,147,.45), transparent 72%),
    linear-gradient(180deg, var(--navy-deep) 0, var(--navy-deep) 112px, var(--navy) 100%);

  /* The header is --card, which is lighter than this band's --navy-deep top
     by 1.08:1 and a different hue besides, so the join read as two unrelated
     strips. It cannot be closed from this side: matching the band to the
     header would grey out the navy exactly where the score strip sits, and
     turning .shellbar navy means turning .appbar navy with it, because
     CLAUDE.md requires a CPU on the clock to look like the homepage.

     So the seam becomes depth instead. The header casts onto the band, which
     is a thing a reader already knows how to interpret. */
  box-shadow: inset 0 9px 18px -10px rgba(0,0,0,.75);

  margin-bottom: 46px;
}

.band-inner { max-width: 1120px; margin: 0 auto; padding: 0 18px; }

/* The bottom padding was the fade's clearance back when the hero was the
   last thing in the band. The shot sits under it now and reaches the band's
   bottom edge, so it is the shot that meets the fade — and it fades itself,
   on purpose, with its own mask. What is left here is just the gap between
   the button and the picture. */
.hero-band .hero { padding: 66px 0 34px; }

.hero-band .hero h1     { color: var(--band-ink); }
.hero-band .hero-sub    { color: var(--band-ink-mid); }
/* --orange rather than --orange-ink: the light theme swaps --orange-ink for a
   darker value that only works on a light page, and this page is navy under
   both themes. The brand value is 5.17:1 here, so small text still passes. */
.hero-band .hero-slogan { color: var(--orange); }

.hero-band .navlink     { color: var(--band-ink-mid); }
.hero-band .navlink:hover { color: var(--band-ink); }

/* The score strip on navy. Every surface in here is translucent white rather
   than --card, because --card is #FFFFFF in the light theme and a white card
   on navy carrying near-black --ink text is the one combination this band
   would otherwise produce. */
.hero-band .scores-wrap { border-bottom: 0; margin-bottom: 0; }
.hero-band .scores-wrap::before { background: linear-gradient(to right, var(--navy-deep), transparent); }
.hero-band .scores-wrap::after  { background: linear-gradient(to left,  var(--navy-deep), transparent); }

.hero-band .game {
  background: rgba(255,255,255,.06);
  border-color: rgba(255,255,255,.14);
}
.hero-band .game-row      { color: var(--band-ink-mid); }
.hero-band .game-row.lead { color: var(--band-ink); }
.hero-band .game-st       { color: var(--band-ink-dim); }
.hero-band .game-st.live  { color: var(--orange); }

.hero-band .scores-arrow {
  background: rgba(255,255,255,.10);
  border-color: rgba(255,255,255,.20);
  color: var(--band-ink-mid);
}
.hero-band .scores-arrow:hover { background: rgba(255,255,255,.20); color: var(--band-ink); }

/* --blue is the focus ring everywhere else and it disappears into navy. */
.hero-band :focus-visible { outline-color: #fff; }

/* ---------- the product shot ----------

   Filled by renderHeroShot(). Decoration: no pointer events, no selection,
   no tab stop, aria-hidden in the markup.

   It is cropped rather than scaled on a narrow screen. Ten columns wide
   enough to hold a surname cannot fit a phone — at 375px each would get
   31px — and a board shrunk until it is unreadable stops being a picture
   of the product. So the columns keep a floor of 88px and the container
   clips, which is how a screenshot is normally cropped on a phone anyway.

   That crop is the one place in the app where something overflows and can
   neither scroll nor ellipsise, which is the shape CLAUDE.md calls a leak.
   It is deliberate here and it is the exception: the clipped part is
   decoration with a duplicate elsewhere, not content with no way to reach
   it. Anything else that trips that check is still a bug. */
.hero-shot {
  display: grid;
  grid-template-columns: 26px repeat(10, minmax(88px, 1fr));
  /* The header row sizes to itself; every round row after it is one card
     tall. Stated here rather than as a min-height on the cell, for the reason
     written beside the board's own grid.

     34px, which is what it was before the card grew a third line and what a
     two-line card measures at (32) plus room. It went to 54 to fit an arrow
     and a pick number; those are gone. */
  grid-template-rows: auto;
  grid-auto-rows: 34px;
  gap: 3px;
  overflow: hidden;
  pointer-events: none;
  user-select: none; -webkit-user-select: none;
  /* Dissolves into the band rather than stopping at an edge, so it reads as
     a glimpse of something larger — which it is. The band's own fade is a
     background layer and cannot touch foreground content, so the shot has
     to carry its own. No mask support means no fade and a hard bottom edge,
     which is a worse picture and not a broken one. */
  -webkit-mask-image: linear-gradient(to bottom, #000 46%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 46%, transparent 100%);
}

.shot-hd, .shot-rd {
  font-size: var(--fs-2xs); font-weight: 700; color: var(--band-ink-dim);
  text-align: center; padding-bottom: 3px;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.shot-hd.me { color: var(--band-ink); }
.shot-rd { display: flex; align-items: center; justify-content: center; padding-bottom: 0; }

/* A column, so the foot sits at the bottom of the card the way the board's
   does. The height is on the row rather than here — same reason as the board:
   a floor on the cell gives a uniform grid only while it happens to exceed
   what the content actually needs. */
.shot-cell {
  border-radius: var(--r-xs); padding: 5px 6px;
  color: #fff; overflow: hidden;
  display: flex; flex-direction: column;
}
.shot-cell b {
  display: block; font-size: var(--fs-2xs); font-weight: 700;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* `opacity: .78` here, measuring 3.38 to 3.66 across the six solids — the same
   bug as the board's sub-line and worse, on the first screen a visitor sees.
   Both were invisible to every sweep for the same reason: opacity is a
   property on the element, not a channel in the colour, so a walker reading
   `color` reports 4.62 and moves on. Weight carries the hierarchy instead. */
.shot-cell s {
  display: block; text-decoration: none; font-size: var(--fs-2xs);
  font-weight: 400; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}
/* The same six solids the board uses, so the shot cannot drift from it. */
.shot-cell.QB  { background: var(--qb); }
.shot-cell.RB  { background: var(--rb); }
.shot-cell.WR  { background: var(--wr); }
.shot-cell.TE  { background: var(--te); }
.shot-cell.K   { background: var(--k); }
.shot-cell.DST { background: var(--dst); }
.shot-cell.empty { background: rgba(255,255,255,.06); }
/* The shot is an excerpt of the real board, so it marks a seat the way the
   board does. Translucent white was picked before there was a rule to follow. */
.shot-cell.mine { box-shadow: inset 0 0 0 2px var(--mine), inset 0 0 0 3px var(--ring-edge); }

@media (max-width: 620px) {
  /* The flat region of the gradient tracks the strip, and the strip is the
     same height at every width — one row that scrolls rather than wraps —
     so only the spacing moves here, never the 112px.

     78px still clears the 64px fade. It is lopsided against the 42px above
     it and that is the fade's price, not a judgement about the hero. */
  .hero-band { margin-bottom: 34px; }
  .hero-band .hero { padding: 42px 0 26px; }

  /* At the desktop floor of 88px a phone shows 3.4 of the ten columns, and
     three and a bit columns reads as a picture that failed to load rather
     than one that is cropped. So the floor drops here, and where it drops to
     is set by how much of a name survives.

     It was 58px, chosen when a cell held a surname alone — 38 of the 50 names
     on the shot fitted. The card carries an initial now, which is about 14px
     more, and at 58px that collapses to **11 of 50**: a board of "G. Pic…"
     and "A. Bro…", which is not truncation, it is a different graphic.

     70px puts it back to 40 of 50 and still shows 4.8 of the ten columns —
     near enough the half-room that made 58 the answer the first time.

     This stays at 70 now the arrow and pick number have gone, because the
     foot was never what decided it: it wanted 41px at its widest and the
     names want more. The initial is what costs the width, and the initial is
     the part that was worth keeping. */
  .hero-shot { grid-template-columns: 20px repeat(10, minmax(70px, 1fr)); }
}

/* ---------- the "not yet" dialog ----------
   A native <dialog>, so Escape, focus trapping and the backdrop are free. */

.dlg {
  border: 0; border-radius: var(--r-lg); padding: 24px 26px;
  width: min(420px, calc(100vw - 32px));
  background: var(--card); color: var(--ink);
  box-shadow: var(--elev-4);
}

.dlg::backdrop { background: rgba(6,12,22,.55); }
.dlg h3 { margin: 0 0 8px; font-family: Poppins, sans-serif; font-size: var(--fs-lg); font-weight: 700; }
.dlg p { margin: 0 0 18px; font-size: var(--fs-sm); line-height: 1.6; color: var(--ink-mid); }

/* ---------- the score strip ----------
   One row, horizontally scrolling, never taller than it needs to be. It is
   the only part of the site that depends on a third party being up, so it
   fails by disappearing rather than by showing an error. */

.scores-wrap { position: relative; border-bottom: 1px solid var(--line); margin-bottom: 2px; }

/* No scrollbar. A native horizontal bar under a score row is the loudest
   thing on the page and it is pure chrome, so the affordance moves to a fade
   at each end plus arrows for anyone on a mouse. Touch still swipes. */
.scores {
  display: flex; gap: 8px; overflow-x: auto;
  padding: 12px 0 14px;
  scroll-behavior: smooth;
  scrollbar-width: none; -ms-overflow-style: none;
  -webkit-overflow-scrolling: touch;
}

.scores::-webkit-scrollbar { display: none; }

@media (prefers-reduced-motion: reduce) { .scores { scroll-behavior: auto; } }

/* The fades only appear on the side that has more to show, so at either end
   the row reads as finished rather than clipped. */
.scores-wrap::before, .scores-wrap::after {
  content: ""; position: absolute; top: 0; bottom: 1px; width: 44px; z-index: 1;
  pointer-events: none; opacity: 0; transition: opacity .18s ease;
}
.scores-wrap::before { left: 0;  background: linear-gradient(to right, var(--page), transparent); }
.scores-wrap::after  { right: 0; background: linear-gradient(to left,  var(--page), transparent); }
.scores-wrap.more-left::before  { opacity: 1; }
.scores-wrap.more-right::after { opacity: 1; }

.scores-arrow {
  position: absolute; top: calc(50% - 1px); transform: translateY(-50%); z-index: 2;
  display: flex; align-items: center; justify-content: center;
  width: 28px; height: 28px; border-radius: 50%;
  border: 1px solid var(--line); background: var(--card); color: var(--ink-mid);
  font-size: var(--fs-base); line-height: 1; cursor: pointer;
  opacity: 0; pointer-events: none; transition: opacity .18s ease;
}

.scores-arrow.left  { left: -4px; }
.scores-arrow.right { right: -4px; }
.scores-arrow:hover { background: var(--fill-hi); color: var(--ink); }
.scores-wrap.more-left  .scores-arrow.left  { opacity: 1; pointer-events: auto; }
.scores-wrap.more-right .scores-arrow.right { opacity: 1; pointer-events: auto; }

/* A finger scrolls the row directly and never hovers, so the arrows are for
   pointers only. */
@media (hover: none) { .scores-arrow { display: none; } }

.game {
  flex: none; min-width: 132px;
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r-sm); padding: 8px 11px;
}

.game-row {
  display: flex; justify-content: space-between; align-items: baseline; gap: 12px;
  font-size: var(--fs-sm); line-height: 1.55; color: var(--ink-mid);
}

.game-row .tm { font-weight: 600; }
.game-row .sc { font-variant-numeric: tabular-nums; }
.game-row.lead { color: var(--ink); }
.game-row.lead .tm, .game-row.lead .sc { font-weight: 700; }

.game-st { margin-top: 5px; font-size: var(--fs-2xs); color: var(--ink-light); white-space: nowrap; }
.game-st.live { color: var(--orange); font-weight: 700; }

/* ---------- the header on a narrow screen ----------
   The three columns are 1fr auto 1fr, so each side gets the same width and
   the brand sits dead centre. That stops working around 540px: the sides
   need about 191px of links each and only get 110px, and because the links
   are nowrap they do not shrink, they spill straight over the wordmark.

   So below the breakpoint the header keeps only what it must — a way into
   the rooms, the brand, and one action — and everything else moves into the
   panel that the burger opens. */

.burger { display: none; flex-direction: column; gap: 4px; width: 18px; }
.burger i { display: block; height: 2px; background: currentColor; border-radius: var(--r-xs); }

.rooms-extra { display: none; }

@media (max-width: 700px) {
  /* .shell-inner, not #shellbar: the rooms panel is inside the header too,
     and scoping to the header hid the panel's own toggle along with it. */
  .shell-inner .navlink,
  .shell-inner .js-install,
  .shell-inner .theme-toggle { display: none; }

  .rooms-label { display: none; }
  .burger { display: flex; }
  #roomsBtn { padding: 9px 12px; }

  .shell-inner { gap: 10px; padding: 10px 14px; }

  .rooms-extra {
    display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
    padding: 14px 18px 18px; margin-top: 2px;
    border-top: 1px solid var(--line);
  }

  .rooms-extra .navlink { padding-left: 0; }
  .rooms-extra .theme-toggle { margin-left: auto; }
}

/* ---------- back to top ---------- */

/* One control, two homes: over the viewport for a page that scrolls, and
   inside the player sheet for the one container that scrolls on its own.
   The button is created by back-to-top.js, so nothing here assumes markup.

   Colours: --navy is dark under both themes and white sits on it, so this
   needs no light-theme override. See the note at the top of this file. */
.to-top {
  position: fixed; z-index: 35;
  right: calc(18px + env(safe-area-inset-right));
  /* The iOS home indicator sits in the bottom inset. Without this the button
     lands under it in the installed app, where there is no browser chrome
     holding it clear. */
  bottom: calc(18px + env(safe-area-inset-bottom));
  width: 44px; height: 44px;      /* a full touch target, not a decoration */
  font-size: var(--fs-sm);        /* icon only — see the note on .theme-toggle */
  display: flex; align-items: center; justify-content: center;
  border: 0; border-radius: 50%;
  background: var(--navy); color: #fff;
  box-shadow: var(--elev-3);
  cursor: pointer;
  /* Out of the way when it is not wanted: invisible, unclickable, and
     untabbable — the tabIndex is handled in JavaScript alongside this. */
  opacity: 0; pointer-events: none; transform: translateY(8px);
  transition: opacity .18s ease, transform .18s ease, background .15s ease;
}

.to-top.on { opacity: 1; pointer-events: auto; transform: none; }
.to-top:hover { background: var(--navy-lift); }

/* Focus has to be visible on a round button floating over arbitrary content,
   so it gets a ring with a gap rather than relying on the border. */
.to-top:focus-visible { outline: 2px solid var(--cyan); outline-offset: 3px; }

/* The arrow is a rotated corner, the same shape the rooms chevron uses. */
.to-top-arrow {
  width: 9px; height: 9px; margin-top: 3px;
  border-top: 2px solid currentColor; border-left: 2px solid currentColor;
  transform: rotate(45deg);
}

@media (prefers-reduced-motion: reduce) {
  /* The fade stays — it is an opacity change, not motion — but the slide up
     goes, and so does any easing that would make the button feel like it
     travelled. The scroll itself is switched to "auto" in JavaScript, since
     a rule here cannot reach a programmatic smooth scroll. */
  .to-top { transform: none; transition: opacity .01s linear; }
}

/* With the sheet open the page behind it is not the thing being read, and a
   button showing through the backdrop is just noise. The sibling combinator
   works because back-to-top.js appends to <body>, after the sheet markup;
   the sheet's own copy is a child of #sheet, so it is untouched. */
#sheet:not([hidden]) ~ .to-top { opacity: 0; pointer-events: none; }

/* Inside the player sheet the button belongs to the sheet, not the viewport:
   .sheet is already position:fixed, so absolute anchors to it. It also has to
   clear the sheet's own rounded corner, hence the tighter offsets. */
.to-top.in-sheet {
  position: absolute; z-index: 1;
  right: 14px; bottom: calc(14px + env(safe-area-inset-bottom));
  width: 40px; height: 40px;
}

/* Full-screen sheet on a phone: the sheet is the viewport, so the offsets
   go back to matching the page-level button. */
@media (max-width: 620px) {
  .to-top.in-sheet {
    right: calc(18px + env(safe-area-inset-right));
    bottom: calc(18px + env(safe-area-inset-bottom));
  }
}

/* ---------- the queue ----------

   Your own order, as distinct from the model's. It reuses the suggestion
   row's shape rather than inventing a third kind of player row, so the
   three lists on this site read as one family. */

.qrow {
  display: flex; align-items: center; gap: 10px;
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r-md); padding: 9px 12px; margin-bottom: 7px;
}

/* The one at the front is what the clock takes if it runs out while you are
   away, so it is worth marking as different from the rest of the list. */
.qrow.next { border-color: var(--orange); }
.qrow.next .qn { color: var(--orange-cta); }

.qn {
  font-family: Inter, sans-serif; font-size: var(--fs-xs); font-weight: 700;
  color: var(--ink-light); width: 16px; flex: none;
  font-variant-numeric: tabular-nums;
}

.qbody { flex: 1; min-width: 0; }
.qname { font-size: var(--fs-sm); font-weight: 600; }
.qmeta { display: flex; align-items: center; gap: 6px; margin-top: 2px; font-size: var(--fs-xs); color: var(--ink-mid); }

.qmoves { display: flex; flex-direction: column; gap: 2px; flex: none; }

.qmove {
  width: 22px; height: 17px; padding: 0; cursor: pointer;
  font-size: var(--fs-2xs); line-height: 1;
  color: var(--ink-mid); background: var(--fill);
  border: 1px solid var(--line); border-radius: var(--r-xs);
}
.qmove:hover:not(:disabled) { background: var(--fill-hi); color: var(--ink); }
.qmove:disabled { opacity: .35; cursor: default; }

/* The star that puts a player in the queue. Off is an outline in muted ink,
   on is the brand orange filled — both legible on either theme, since
   --orange is a brand colour rather than a themed one. */
/* This is a <td>, so it does not get to be a flex container.

   `display: flex` takes a cell out of table layout: it stops stretching to
   the height of its row and sizes to its own content instead. Every other
   cell in the row was 43px tall and this one was 35px, so its bottom border
   was drawn seven pixels higher than the rest — a visible step in the
   divider, starting exactly where the last column does. Laid out with
   inline-block and white-space instead, which a table cell can do without
   giving up being one. */
.rowacts { text-align: right; white-space: nowrap; }
.rowacts > * { vertical-align: middle; }
.rowacts .draft-btn { margin-left: 6px; }

.star {
  flex: none; width: 26px; height: 26px; padding: 0; cursor: pointer;
  font-size: var(--fs-sm); line-height: 1;
  color: var(--ink-light); background: none;
  border: 1px solid var(--line); border-radius: var(--r-sm);
}
.star:hover { color: var(--orange); border-color: var(--orange); }
.star.on { color: #fff; background: var(--orange-cta); border-color: var(--orange-cta); }

#playerTable.compact .star { width: 22px; height: 22px; font-size: var(--fs-xs); }

/* A positional run, in the strip that already says what just happened. */
.run {
  margin-left: auto; flex: none;
  font-size: var(--fs-xs); color: var(--t-warn-fg);
  background: var(--t-warn-bg); border-radius: var(--r-pill); padding: 4px 11px;
}
.run b { color: inherit; font-weight: 700; }

/* The on-the-clock cell counts down in place. It is already dashed blue and
   bold from .cell.now; this only stops the digits jittering as they change. */
.board .cell.now { font-variant-numeric: tabular-nums; }

/* ==========================================================
   The persistent draft shell

   The board never leaves the screen, because the board is what
   tells you whether to reach: a receiver run three picks away
   is a shape you can see, and only if you are looking at it.
   The rail carries the two things checked between picks.

   Only the middle column changes. That is the whole idea.
   ========================================================== */

/* The board and the work under it are one column; the chat, when there is
   one, is the other. Grid rather than a float or a flex row because the chat
   has to be able to run the full height of both without either of them
   knowing it is there — and because in a solo draft the second column simply
   does not exist and the first takes the whole width with no rule needed. */
.draftshell {
  padding: 10px 12px 48px;
  display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 10px;
  align-items: start;
}

.draftmain { min-width: 0; }

/* The slot is nothing until the dock is moved into it. Empty in a solo
   draft, and `auto` above means an empty column takes no width at all. */
.chatslot { min-width: 0; }
.draftshell > .chatslot:not(:empty) { width: 330px; }

/* ---------- the board pane ---------- */

/* Fourteen rounds do not fit above a working area, so the pane is capped and
   scrolls, and renderBoard() keeps the live round in the middle of it. The
   cap is in vh rather than pixels because the useful trade is against the
   height of the screen, not against a number. */
.boardpane {
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r-md); padding: 10px 12px; margin-bottom: 10px;
}

.boardpane .board-scroll {
  /* 43px taller than it was, which is exactly what merging the tab and action
     bars gave back — spent here rather than pocketed, so nothing below the
     board moves and it gains a whole round. It showed 5.9 of fourteen. */
  max-height: min(43vh, 383px);
  overflow: auto;
  overscroll-behavior: contain;   /* the page does not move when this ends */
}

/* The header row has to stay put, or a scrolled board is a grid of names
   with no way to tell whose column is whose. */
.boardpane .board .hd { position: sticky; top: 0; z-index: 2; }
.boardpane .board .rd { position: sticky; left: 0; z-index: 1; }

.boardpane .legend { margin-top: 9px; }

/* ---------- the split ---------- */

.worksplit { display: grid; grid-template-columns: minmax(0, 1fr) 268px; gap: 10px; align-items: start; }

.workmain { min-width: 0; }

/* Panels no longer own the page, so they lose the page's padding. */
.workmain .panel { padding: 0; }

/* ---------- the rail ---------- */

.rail { display: flex; flex-direction: column; gap: 10px; position: sticky; top: 8px; }

.railcard {
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r-md); padding: 11px 13px;
}

.railhead {
  margin: 0 0 8px; font-family: Inter, sans-serif;
  font-size: var(--fs-2xs); letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink-light); font-weight: 700;
}

.railslots { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 3px; }

.railslots li {
  display: flex; align-items: center; gap: 8px;
  background: var(--sunken); border-radius: var(--r-sm); padding: 5px 8px;
  font-size: var(--fs-xs); min-width: 0;
}

.rslot {
  flex: none; width: 34px;
  font-size: var(--fs-2xs); font-weight: 700; letter-spacing: .04em;
  color: var(--ink-light);
}

/* The slot label takes its position colour, so an empty starting slot is
   readable as "no quarterback" without reading the word. */
.rslot.QB { color: var(--qb-fg); }  .rslot.RB { color: var(--rb-fg); }
.rslot.WR { color: var(--wr-fg); }  .rslot.TE { color: var(--te-fg); }
.rslot.K  { color: var(--k-fg); }   .rslot.DST { color: var(--dst-fg); }

.rfill {
  flex: 1; min-width: 0; font-weight: 600; color: var(--ink);
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.rfill.none { font-weight: 400; font-style: italic; color: var(--ink-light); }
/* Two things wear this class and only one of them is a control: the trailing
   "RB · SF" on every roster row, and the bench row's button through to My
   Team. So the shared half is the typography, and everything that makes a
   control a control is scoped to the element that is one.

   Getting that wrong is not theoretical — it shipped. A cursor, a hover
   underline and 10px of padding went onto the bare `.rtm` and landed on every
   roster row in the rail, which is the sheet's own rule about checking a class
   name against the existing stylesheet, ignored by the person who wrote it
   down. `grep` for the class, not just for the element you are looking at. */
.rtm { flex: none; font-size: var(--fs-2xs); color: var(--ink-light); letter-spacing: .02em; }

/* A <button>, because it was always drawn as a link and is now wired like one.
   Everything here except the padding is undoing a browser default so it still
   looks exactly like the span it replaced; the padding is there because
   `--fs-2xs` is a very small thing to ask a thumb to hit, and the negative
   vertical margin keeps the taller tap target from growing the row. */
button.rtm {
  font-family: inherit; font-weight: inherit;
  background: none; border: 0; padding: 6px 0 6px 10px; margin: -6px 0;
  cursor: pointer; -webkit-appearance: none; appearance: none;
}
button.rtm:hover { text-decoration: underline; }
button.rtm:focus-visible {
  outline: 2px solid var(--link); outline-offset: 2px; border-radius: 4px;
}

/* An unfilled starting slot is the actionable state, so it is the one that
   takes a tint rather than every filled row taking one. */
.railslots li.empty { background: var(--well); }

/* The bench summary, so the nine rows above it add up to the number in the
   heading. Quieter than a real roster row, because it is a count rather than
   somebody you drafted. */
.railslots li.benchsum { background: var(--well); color: var(--ink-light); }
.railslots li.benchsum .rtm { color: var(--link); }

/* The queue in the rail is narrower than it was as a panel, so the avatar
   goes — it is decoration next to a name. The move buttons stay, because
   with the Queue tab gone this is the only place a queue can be reordered,
   and a list you cannot reorder is not a queue. The row wraps rather than
   squeezing the name to nothing. */
.rail .qrow { padding: 6px 8px; gap: 6px; margin-bottom: 5px; flex-wrap: wrap; }
.rail .qrow .avatar { display: none; }
.rail .qbody { flex: 1 1 130px; }
.rail .qname { font-size: var(--fs-sm); }
.rail .qmeta { font-size: var(--fs-2xs); gap: 5px; }
.rail .qrow .draft-btn { flex: none; font-size: var(--fs-xs); padding: 4px 9px; }
.rail .empty { padding: 6px 2px; }
.rail .empty-title { font-size: var(--fs-sm); }
.rail .empty-sub { font-size: var(--fs-xs); }

/* ---------- narrow ---------- */

/* The launcher and the sheet's way out are for the phone layout only. On a
   desktop the rail is a column and neither has anything to do, so they are
   off here and turned on inside the media query. */
.railfab { display: none; }
.raildismiss { display: none; }

/* Between a tablet and a desktop there is room for the cards side by side
   under the list, which is where they used to live at every width. */
@media (max-width: 900px) {
  .worksplit { grid-template-columns: minmax(0, 1fr); }
  .rail { position: static; flex-direction: row; flex-wrap: wrap; }
  .rail .railcard { flex: 1 1 260px; }
}

/* ---------- the rail on a phone ----------

   Two cards under the panel is fine while the panel is short. It is not fine
   under the player table: measured at 375px the rail began 11,841px down the
   Players tab, and the queue lives nowhere else, so the screen carrying 177
   ways to add to the queue was the screen you could not read it from. The
   rail was "never more than a glance away" everywhere except where it was
   needed.

   So it becomes a sheet with a launcher, exactly as the chat does one
   breakpoint further in — the same problem deserves the same answer rather
   than a second idea to learn. */
@media (max-width: 700px) {
  .railfab:not([hidden]) {
    display: flex; align-items: center; gap: 7px;
    position: fixed; left: 14px; z-index: 60;
    /* Level with the chat launcher, which sits at the right on this rail. */
    bottom: calc(74px + env(safe-area-inset-bottom, 0px));
    font-family: Inter, sans-serif; font-size: var(--fs-sm); font-weight: 600;
    color: #fff; background: var(--navy);
    border: 1px solid var(--navy-soft); border-radius: var(--r-pill);
    padding: 10px 16px; cursor: pointer;
    box-shadow: var(--elev-3);
  }

  /* Out of the layout until asked for, so it leaves no gap under the panel. */
  .rail { display: none; }

  body.rail-open .rail {
    display: flex; flex-direction: column; flex-wrap: nowrap;
    /* top: auto for the reason the chat sheet gives: the docked rule sets
       top: 8px, and top plus a height is a complete answer the browser will
       take in preference to bottom. */
    position: fixed; top: auto; left: 0; right: 0; bottom: 0; z-index: 70;
    height: 78vh; max-height: 78vh; width: auto;
    gap: 10px; padding: 14px 12px calc(14px + env(safe-area-inset-bottom, 0px));
    overflow-y: auto;
    background: var(--sunken);
    border-top: 1px solid var(--line);
    border-radius: var(--r-lg) var(--r-lg) 0 0;
    box-shadow: var(--elev-up);
  }

  body.rail-open .rail .railcard { flex: none; }

  /* The sheet covers the board, so it needs its own way out. */
  body.rail-open .raildismiss {
    display: block; align-self: flex-end;
    font-size: var(--fs-2xl); line-height: 1; padding: 0 6px; margin-bottom: -4px;
    color: var(--ink-light); background: none; border: 0; cursor: pointer;
  }

  body.rail-open .railfab { display: none; }
}

@media (max-width: 560px) {
  .draftshell { padding: 8px 8px 40px; }
  .boardpane { padding: 8px; }
  .boardpane .board-scroll { max-height: 30vh; }
  .rail { flex-direction: column; }
}

/* ---------- draft with friends ---------- */

.invite { border-top: 1px solid var(--line); padding-top: 18px; }

.linkrow { display: flex; gap: 6px; margin-bottom: 8px; }

.linkrow input {
  flex: 1; min-width: 0;
  font-family: Inter, sans-serif; font-size: var(--fs-sm);
  color: var(--ink-mid); background: var(--sunken);
  border: 1px solid var(--line); border-radius: var(--r-sm); padding: 8px 10px;
}

/* Every seat in the room at a glance, so it is obvious how many friends have
   actually arrived before anyone starts. */
.seatlist { display: flex; flex-wrap: wrap; gap: 4px; margin: 10px 0 12px; }

.seat {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: var(--fs-xs); color: var(--ink-light);
  background: var(--fill); border-radius: var(--r-sm); padding: 3px 8px;
}
.seat b { font-weight: 700; color: var(--ink-light); font-variant-numeric: tabular-nums; }

/* A chair with a person in it, and the one that is yours. Both use tint
   pairs that carry legible text under either theme. */
.seat.human { background: var(--t-info-bg); color: var(--t-info-fg); }
.seat.human b { color: var(--t-info-fg); }
.seat.you { background: var(--t-good-bg); color: var(--t-good-fg); }
.seat.you b { color: var(--t-good-fg); }

/* The host's copy of a chair is a <button>, so that setting the draft order
   is reachable by keyboard and announces itself, and so a phone gets a tap
   target rather than a drag it cannot perform. Everything here is undoing
   what a browser does to a button by default — the chair should look exactly
   like the span it replaces until it is picked up. */
.seat.movable {
  font: inherit; font-size: var(--fs-xs); border: 0; cursor: grab;
  -webkit-appearance: none; appearance: none;
}
.seat.movable:hover { outline: 1px solid var(--line); }
.seat.movable:focus-visible { outline: 2px solid var(--link); outline-offset: 1px; }

/* Picked up. Named rather than dimmed, because on a phone this state can
   last as long as it takes to decide where it is going. */
.seat.held { cursor: grabbing; outline: 2px dashed var(--link); outline-offset: 1px; }

/* ---------- room chat ----------

   The only text on this page written by somebody other than us. It is
   escaped in renderChat() before it ever reaches innerHTML — see the note
   there — and everything below is only about making it readable.

   It used to be a card in the rail with a 220px log, under the roster and
   the queue, and it read like an afterthought because it was one: four lines
   visible, no names, nothing to tell you anything had been said. This is the
   same content given the room to be a conversation. */

.chatdock {
  display: flex; flex-direction: column;
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r-md); overflow: hidden;
  /* Sticky and tall, so reading the chat is not a thing you scroll away from
     the board to do. The viewport minus the header and a margin. */
  position: sticky; top: 8px;
  height: calc(100vh - 90px);
  min-height: 320px;
}

/* An author `display` beats the browser's own rule for [hidden], so every
   element below that is both hidden from JavaScript and given a display here
   has to say this as well. Without it a solo draft grows a chat dock for a
   room it is not in — the attribute is set correctly and simply loses. */
.chatdock[hidden], .chatbadge[hidden] { display: none; }

/* In the lobby it is under the setup form rather than beside a board, so it
   sizes to its content instead of to the screen and stops being sticky —
   there is nothing to stay level with. */
/* Relative rather than static: the jump-to-latest pill is absolutely
   positioned inside the dock, and with no positioned ancestor it would go
   looking for the page and land somewhere else entirely. */
/* `top: auto` is doing real work here, and this is the second place in this
   file to learn it. The docked rule is `position: sticky; top: 8px`; this one
   only changes `position`, and `top` applies to a relatively positioned box
   just as happily — as an 8px shove downwards, with the layout box left
   where it was. So the dock hung 8px below its own slot and sat on top of
   the Start button underneath it, which is a button, so it was 8px of dead
   pixels on the thing the whole screen is for. The mobile sheet has the same
   line for the same reason. */
#lobbyChatSlot .chatdock {
  position: relative; top: auto; height: 420px; min-height: 0; margin-top: 14px;
}

.chathead {
  display: flex; align-items: baseline; gap: 8px;
  padding: 10px 13px; border-bottom: 1px solid var(--hairline);
  background: var(--sunken); flex: none;
}

.chathead h3 {
  margin: 0; font-family: Inter, sans-serif;
  font-size: var(--fs-2xs); letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink-light); font-weight: 700;
}

.chatpresence { font-size: var(--fs-xs); color: var(--ink-light); margin-left: auto; }

/* Only ever in the mobile sheet. Hidden here rather than not rendered, so
   the same dock is the same dock in both places. */
.chatdismiss { display: none; }

/* The log takes whatever height is left, which is what makes the dock a
   column rather than a card with a scrollbar in it. */
.chatscroll {
  flex: 1; min-height: 0;
  overflow-y: auto; overscroll-behavior: contain;
  display: flex; flex-direction: column; gap: 2px;
  padding: 10px 12px;
}

/* ---- a message ---- */

.msg {
  display: grid; grid-template-columns: 26px minmax(0, 1fr) auto;
  gap: 8px; align-items: start;
  padding: 3px 0; position: relative;
}

/* A follow-up from the same person inside two minutes. The avatar column is
   kept but left empty, so the text stays on the same left edge as the line
   above it rather than stepping in and out as people take turns. */
.msg.grouped { padding-top: 0; }
.msg.grouped .msgwhen {
  grid-column: 1; text-align: right; font-size: var(--fs-2xs);
  /* Only on hover, because a timestamp against every line of a fast
     conversation is nine columns of noise to find one useful number. */
  opacity: 0; transition: opacity .12s;
}
.msg.grouped:hover .msgwhen { opacity: 1; }

.chatav {
  grid-column: 1; width: 26px; height: 26px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: var(--fs-2xs); font-weight: 700; letter-spacing: .02em;
  color: #fff;                     /* white on a solid, the same in both themes */
  flex: none; user-select: none;
}

.av0 { background: var(--seat-0); } .av1 { background: var(--seat-1); }
.av2 { background: var(--seat-2); } .av3 { background: var(--seat-3); }
.av4 { background: var(--seat-4); } .av5 { background: var(--seat-5); }
.av6 { background: var(--seat-6); } .av7 { background: var(--seat-7); }

.msgbody { grid-column: 2; min-width: 0; }

.msgwho {
  margin: 0 0 1px; font-size: var(--fs-xs); font-weight: 650; color: var(--ink);
  display: flex; align-items: baseline; gap: 6px;
}

.msgwhen { font-size: var(--fs-2xs); font-weight: 400; color: var(--ink-light); }

.msgtext {
  font-size: var(--fs-sm); line-height: 1.45; color: var(--ink-mid);
  /* A long unbroken string typed by someone else must not widen the dock. */
  overflow-wrap: anywhere; white-space: pre-wrap;
}

/* Your own lines. A tint on the name only — a whole bubble in a second
   colour would fight the pick rows for the eye, and the pick rows are the
   ones carrying information. */
.msg.mine .msgwho { color: var(--link); }

/* ---- the room talking about itself ---- */

.chatline { margin: 0; overflow-wrap: anywhere; }

.chatline.system {
  font-size: var(--fs-xs); color: var(--ink-light); font-style: italic;
  padding: 3px 0 3px 34px;
}

/* ---- a pick, merged into the conversation ----

   Not stored as a message — see chatStream(). It reads as a different kind
   of thing from somebody talking, because it is one, and a draft chat where
   the picks are invisible is a chat you have to look away from to follow the
   draft. */
.pickline {
  display: flex; align-items: baseline; gap: 8px;
  margin: 3px 0; padding: 5px 9px;
  background: var(--sunken); border-radius: var(--r-sm);
  border-left: 2px solid var(--line);
  font-size: var(--fs-xs); color: var(--ink-mid);
}

.pickline.mine { border-left-color: var(--mine); background: var(--row-mine); }

.pickno {
  flex: none; font-size: var(--fs-2xs); font-weight: 700;
  color: var(--ink-light); font-variant-numeric: tabular-nums;
}

.picktext { flex: 1; min-width: 0; overflow-wrap: anywhere; }
.picktext b { color: var(--ink); font-weight: 650; }

.chatempty { margin: 0; font-size: var(--fs-xs); color: var(--ink-light); font-style: italic; }

/* ---- reactions ---- */

.reactrow { display: flex; flex-wrap: wrap; gap: 3px; margin-top: 3px; }

.reactchip {
  display: inline-flex; align-items: center; gap: 3px;
  font-family: Inter, sans-serif; font-size: var(--fs-xs); line-height: 1;
  color: var(--ink-mid); background: var(--fill);
  border: 1px solid var(--line); border-radius: var(--r-md);
  padding: 2px 6px; cursor: pointer;
}
.reactchip b { font-size: var(--fs-2xs); font-weight: 700; font-variant-numeric: tabular-nums; }
.reactchip:hover { background: var(--fill-hi); }

/* Yours, so a count of four is readable as "including me" without counting. */
.reactchip.on {
  background: var(--t-info-bg); color: var(--t-info-fg); border-color: var(--blue-line);
}

/* The way to add one. Held back until the message is hovered, because a
   button on every line is a column of buttons down the whole log. It stays
   put on a touch screen, where there is no hover to reveal it. */
.reactadd {
  grid-column: 3; align-self: start;
  width: 20px; height: 20px; padding: 0;
  font-size: var(--fs-sm); line-height: 1;
  color: var(--ink-light); background: var(--fill);
  border: 1px solid var(--line); border-radius: 50%;
  cursor: pointer; opacity: 0; transition: opacity .12s;
}
.msg:hover .reactadd, .reactadd:focus-visible { opacity: 1; }
.reactadd:hover { color: var(--ink); background: var(--fill-hi); }

@media (hover: none) {
  .reactadd { opacity: .5; }
}

.reactpicker {
  position: absolute; right: 0; top: 22px; z-index: 5;
  display: flex; gap: 2px; padding: 4px;
  background: var(--card); border: 1px solid var(--line);
  border-radius: var(--r-sm); box-shadow: var(--elev-2);
}

.reactpicker button {
  font-size: var(--fs-base); line-height: 1; padding: 3px 4px;
  background: none; border: 0; border-radius: var(--r-sm); cursor: pointer;
}
.reactpicker button:hover { background: var(--fill-hi); }

/* ---- what you missed ---- */

/* Sits over the foot of the log rather than pushing it, so arriving at a
   message does not move the thing you were reading. */
.chatjump {
  position: absolute; left: 50%; transform: translateX(-50%);
  bottom: 96px; z-index: 4;
  font-family: Inter, sans-serif; font-size: var(--fs-xs); font-weight: 600;
  color: #fff; background: var(--blue);
  border: 0; border-radius: var(--r-pill); padding: 5px 12px;
  cursor: pointer; box-shadow: var(--elev-2);
}

.chattyping {
  flex: none; margin: 0; padding: 0 13px 4px;
  font-size: var(--fs-2xs); font-style: italic; color: var(--ink-light);
  /* Reserved whether or not anybody is typing, so the composer does not
     jump up and down every time somebody starts a sentence. */
  min-height: 15px;
}

/* Why the composer below it has gone dead. Sits between the log and the box
   so it is read on the way to the thing that is not working.

   `[hidden]` is spelled out because the display below would otherwise beat
   it — the same rule the chat dock itself needs, for the same reason. */
.chatoffline {
  flex: none; display: block; margin: 0 13px 6px; padding: 5px 9px;
  font-size: var(--fs-xs); border-radius: var(--r-sm);
  /* --ink-mid, not the --ink-light the typing line uses. This is the note
     that explains why nothing below it works, so it has to be read rather
     than noticed: light comes out at 2.5:1 on this background, mid at 5.4:1
     in light and 7.8:1 in dark. */
  color: var(--ink-mid);
  background: var(--sunken); border: 1px solid var(--line);
}
.chatoffline[hidden] { display: none; }

/* ---- the composer ---- */

.chatfoot {
  flex: none; padding: 8px 12px 10px;
  border-top: 1px solid var(--hairline); background: var(--sunken);
}

.quicksay { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 6px; }

.quicksay button {
  font-family: Inter, sans-serif; font-size: var(--fs-xs); line-height: 1;
  color: var(--ink-mid); background: var(--fill);
  border: 1px solid var(--line); border-radius: var(--r-md);
  padding: 5px 9px; cursor: pointer;
}
.quicksay button:hover { background: var(--fill-hi); color: var(--ink); }

.chatform { display: flex; gap: 5px; }

.chatform input {
  flex: 1; min-width: 0;
  font-family: Inter, sans-serif; font-size: var(--fs-sm);
  color: var(--ink); background: var(--card);
  border: 1px solid var(--line); border-radius: var(--r-sm); padding: 8px 10px;
}
.chatform input:focus { outline: 2px solid var(--blue); outline-offset: 1px; border-color: var(--blue); }
.chatform button { flex: none; font-size: var(--fs-xs); padding: 8px 11px; }
.chatform .sendbtn { font-size: var(--fs-xs); }

/* ---- your name ---- */

.namerow { margin-bottom: 8px; }

.namerow input {
  width: 100%; box-sizing: border-box;
  font-family: Inter, sans-serif; font-size: var(--fs-sm);
  color: var(--ink); background: var(--sunken);
  border: 1px solid var(--line); border-radius: var(--r-sm); padding: 9px 11px;
}
.namerow input:focus { outline: 2px solid var(--blue); outline-offset: 1px; border-color: var(--blue); }

/* ---- the launcher, and the sheet it opens ----

   Both are for the narrow layout only. On a desktop the dock is a column and
   neither of these has anything to do, so they are hidden here and turned on
   inside the media query rather than the other way round. */
.chatfab { display: none; }
.chatbadge {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 17px; height: 17px; padding: 0 4px;
  font-size: var(--fs-2xs); font-weight: 700; color: #fff;
  background: var(--orange-cta); border-radius: var(--r-sm);
}

/* ---------- chat on a narrow screen ----------

   Below this the board and a 330px column cannot both be readable, and the
   honest answer is not a narrower column — it is that on a phone the chat is
   somewhere you go, not something you watch out of the corner of your eye.
   So it becomes a sheet over the board with a launcher, and the launcher
   carries the unread count, because a closed sheet and a quiet room look
   exactly the same otherwise. */
@media (max-width: 1000px) {
  .draftshell { grid-template-columns: minmax(0, 1fr); }
  .draftshell > .chatslot:not(:empty) { width: auto; }

  /* :not([hidden]) rather than trusting the attribute, for the reason given
     beside .chatdock[hidden] above. */
  .chatfab:not([hidden]) {
    display: flex; align-items: center; gap: 7px;
    position: fixed; right: 14px; z-index: 60;
    /* Above the action bar, which is itself pinned to the bottom, and clear
       of the home indicator on a phone with no bezel. */
    bottom: calc(74px + env(safe-area-inset-bottom, 0px));
    font-family: Inter, sans-serif; font-size: var(--fs-sm); font-weight: 600;
    color: #fff; background: var(--navy);
    border: 1px solid var(--navy-soft); border-radius: var(--r-pill);
    padding: 10px 16px; cursor: pointer;
    box-shadow: var(--elev-3);
  }

  /* Out of the layout entirely until it is asked for, so the dock does not
     leave a hole under the board where it used to be. */
  .chatdock { display: none; }

  body.chat-open .chatdock:not([hidden]) {
    display: flex;
    /* top: auto matters. The docked rule sets top: 8px for the sticky
       column, and top + an explicit height is a complete answer — the
       browser takes it and ignores bottom, which put the sheet at the top of
       the screen with a gap under it. */
    position: fixed; top: auto; left: 0; right: 0; bottom: 0; z-index: 70;
    height: 78vh; max-height: 78vh; width: auto;
    border-radius: var(--r-lg) var(--r-lg) 0 0;
    border-left: 0; border-right: 0; border-bottom: 0;
    box-shadow: var(--elev-up);
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }

  /* The sheet covers the board, so it needs its own way out. */
  body.chat-open .chatdismiss {
    display: block; margin-left: 6px;
    font-size: var(--fs-lg); line-height: 1; padding: 0 4px;
    color: var(--ink-light); background: none; border: 0; cursor: pointer;
  }

  body.chat-open .chatfab { display: none; }

  /* The lobby copy is a plain block in the form either way — there is no
     board for it to cover and nothing to launch it from. */
  #lobbyChatSlot .chatdock:not([hidden]) {
    display: flex; position: relative; height: 360px;
  }
}

/* ---------- GIFs in chat ----------
   Every address here has been through safeGif() twice — once in the room,
   once in renderChat() — so nothing below is doing any of the safety work. */

.chatgif {
  display: block; margin-top: 5px;
  max-width: 100%; max-height: 130px;
  border-radius: var(--r-sm);
}

.gifbox { margin-bottom: 8px; }

.gifresults {
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px;
  max-height: 170px; overflow-y: auto; overscroll-behavior: contain;
  margin-top: 6px;
}

.gifpick {
  padding: 0; border: 1px solid var(--line); border-radius: var(--r-sm);
  background: var(--sunken); cursor: pointer; overflow: hidden;
  aspect-ratio: 1 / 1;
}
.gifpick:hover { border-color: var(--blue); }
.gifpick img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* The grid is the only child when a message replaces it, so the empty and
   error lines have to span it rather than sit in the first cell. */
.gifresults .chatempty { grid-column: 1 / -1; }
