/* §6's visual system, settled in PR #7. Every colour here is a token from
   §6.1's table and no screen hard-codes a raw value outside :root. The type
   scale is §6.2's, given outright because the handoff makes no framework
   choice. §6.3: one committed light theme, phone-first, 360px is the design
   width, no motion system. */

:root {
  --chrome:      #16261f;
  --chrome-line: #24382f;
  --brand:       #35594b;
  --field:       #e9e3d4;
  --card:        #ffffff;
  --ink:         #16261f;
  --ink-muted:   #5f5b4e;
  --line:        #ddd6c6;
  --cream:       #f5f0e3;
  --cream-muted: #c6d2c9;
  --gold:        #d9a441;   /* fill only */
  --gold-ink:    #7a5410;   /* text on light surfaces */
  /* §8.2's CTA hover: gold lightened 20% toward cream. A declared
     token because §6.1 forbids a screen hard-coding a raw colour, and
     a hover computed inline is a raw colour nobody audits. Ink on it
     is 8.08:1 — lightening the fill can only raise it. */
  --gold-lift:   #dfb361;   /* fill only, hover of --gold */
  --rust:        #9c4526;
  /* D46: a divider *inside* a brand-green block — §8.2's setup list. Brand
     lightened toward cream, declared here because §6.1 forbids a screen rule
     hard-coding a raw colour. It is a line and never a text colour: text on
     brand green is cream or cream-muted and nothing about this changes that. */
  --brand-line:  #47695c;

  --radius: 0.625rem;
  --unit: 0.25rem;

  /* §6.2's scale */
  --label: 0.625rem;
  --sm:    0.8125rem;
  --base:  1rem;
  --lg:    1.125rem;
  --xl:    1.25rem;
  --xxl:   1.5rem;
  --xxxl:  1.875rem;
}

/* §6.2's typeface, self-hosted. The stack below named Archivo from the start
   and nothing ever defined it, so every screen rendered in Helvetica -- the
   intent was in the CSS and only the file was missing.

   TWO FILES, NOT SIX. §6.2 asks for a width axis and a *real drawn* italic,
   and both are axis-level requirements a set of static weights cannot meet:
   `font-stretch: 92%` has nothing to interpolate without `wdth`, and a
   synthesized oblique is the thing §6.2 rules out by name. So these are the
   variable faces, roman and italic, carrying `wght` and `wdth`.

   THE RANGES ARE THE ONES THE SCREENS ASK FOR. Upstream ships wght 100-900 and
   wdth 62-125; the CSS uses five weights (400, 500, 600, 700, 800) and exactly
   two widths (92% for .wordmark, 100% everywhere else). The design space is
   clipped to that before subsetting, and that clip is the bigger of the two
   levers: the charset takes the pair 348KB -> 273KB, the axes take it 273KB ->
   159KB. Both faces together are now 156KiB. The descriptors below declare the
   clipped ranges honestly, so a weight outside 400-800 clamps rather than
   rendering a lie. Widen the descriptor *and* rebuild the file if §8 ever
   needs a 900.

   Latin-1 + Latin Extended-A, plus the punctuation the screens actually emit
   (§ · -- ... -> <- x and the minus). `tnum` is kept through the subset because
   §6.2's tabular figures are a feature lookup, not a glyph. Rebuild:
   `tools/build-fonts.py`, which carries the full reasoning.

   ARCHIVO IS NOT A COMPLETE FALLBACK. It has no glyph for the rank arrows
   (U+25B2/U+25BC) or the warning sign (U+26A0); those three keep resolving
   down the stack to Helvetica exactly as they did before, which is why the
   stack stays declared in full on `body`. */
@font-face {
  font-family: Archivo;
  src: url("/static/archivo-var.woff2") format("woff2");
  font-weight: 400 800;
  font-stretch: 92% 100%;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: Archivo;
  src: url("/static/archivo-italic-var.woff2") format("woff2");
  font-weight: 400 800;
  font-stretch: 92% 100%;
  font-style: italic;
  font-display: swap;
}

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  background: var(--field);
  color: var(--ink);
  font-family: Archivo, "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-size: var(--base);
  line-height: 1.45;
}

/* §6.2: a .wordmark treatment -- weight 800, italic, width 92%,
   letter-spacing -0.02em -- for the site wordmark, headline numbers, and the
   primary number on any feature block. */
.wordmark {
  font-weight: 800;
  font-style: italic;
  font-stretch: 92%;
  letter-spacing: -0.02em;
}

/* §6.2: every numeric value in a column uses tabular figures. */
.tabular { font-variant-numeric: tabular-nums; }

.sm    { font-size: var(--sm); line-height: 1.35; }
.lg    { font-size: var(--lg); line-height: 1.3; }
.xl    { font-size: var(--xl); line-height: 1.25; }
.xxxl  { font-size: var(--xxxl); line-height: 1.1; }
.muted { color: var(--ink-muted); }

/* §6.3: centred at a max width of roughly 48rem, 1.5rem horizontal padding. */
.wrap {
  max-width: 48rem;
  margin: 0 auto;
  padding: 0 1.5rem;
}

/* ------------------------------------------------------------- site header */

.chrome {
  background: var(--chrome);
  border-bottom: 1px solid var(--chrome-line);
}
/* ── D46: TWO ROWS ON A PHONE, AND THE IDENTITY IS NOT ONE OF THEM.
   All three of these — wordmark, nav, identity — were flex siblings that
   wrapped in source order, so a phone got three stacked rows: the wordmark on
   its own, the nav over two lines, and the name and Sign out under that.
   Measured at 360px and again at 430px, that is 205px of header before the page
   starts — a quarter of the viewport, on the screen whose whole job is the
   block underneath it.
   The nav is the only one of the three that needs a full row, so it takes one
   (`order` puts it last and `flex-basis: 100%` breaks the line before it) and
   the wordmark and the identity share the row above it. Two rows, 134px, and
   the header reads as a header rather than as a stack of leftovers. */
.chrome-inner {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0 1rem;
  flex-wrap: wrap;
}
.chrome a.wordmark {
  color: var(--cream);
  text-decoration: none;
  font-size: var(--lg);
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  order: 0;
}
.who-out { order: 1; }
/* §7: "Links wrap rather than scroll horizontally on a phone." The wrap was on
   `.chrome-inner` and not on the nav itself, so five links happened to fit at
   360px and a sixth did not — adding **Admin** for the commissioner (D20) put
   14px of the page off the right edge. `min-width: 0` is the half that makes
   the wrap reachable: a flex item will not shrink below its content's intrinsic
   width without it, so the nav kept its one-line width and pushed the row out
   instead of wrapping inside it. */
.chrome nav {
  display: flex;
  flex-wrap: wrap;
  gap: 0 1.125rem;
  min-width: 0;
  order: 2;
  flex: 1 0 100%;
  border-top: 1px solid var(--chrome-line);
}
.chrome nav a {
  color: var(--cream-muted);
  text-decoration: none;
  font-size: var(--sm);
  /* §6.3: minimum touch target 44px on every interactive element. */
  min-height: 44px;
  display: inline-flex;
  align-items: center;
}
.chrome nav a.current { color: var(--cream); font-weight: 600; }
/* Wide enough for all seven links beside the wordmark: one row, the order §7
   states — wordmark, nav, identity — and no rule above it. 52rem is where the
   commissioner's seven stop wrapping, measured rather than guessed. */
@media (min-width: 52rem) {
  .chrome-inner { min-height: 56px; }
  .chrome nav { order: 1; flex: 0 1 auto; border-top: 0; }
  .who-out { order: 2; }
}

/* §6.3: a visible focus ring on every interactive element, using
   focus-visible so it appears for keyboard and not for touch. */
:focus-visible {
  outline: 2px solid var(--chrome);
  outline-offset: 2px;
}
.chrome :focus-visible { outline-color: var(--cream); }

/* ------------------------------------------------------------- typography */

/* §6.2's vertical rhythm: 1.25rem between a page heading and the first block,
   1rem between stacked blocks and cards, 0.5rem between a section label and
   the card it introduces. */
h1 {
  font-size: var(--xxxl);
  line-height: 1.1;
  font-weight: 700;
  margin: 1.5rem 0 0;
}
h1 + .detail { margin: calc(var(--unit) * 2) 0 1.25rem; }
.detail { color: var(--ink-muted); font-size: var(--sm); }

.label {
  font-size: var(--label);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  margin: 0;
}
.section-label {
  font-size: var(--label);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--ink-muted);
  margin: 1rem 0 0.5rem;
}

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

.card {
  background: var(--card);
  border-radius: var(--radius);
  padding: 1.125rem;
  margin: 1rem 0;
}
/* §6.3: cards come in two forms -- padded (default) and flush, for lists
   whose rows own their own inset. */
.card.flush { padding: 0; overflow: hidden; }

/* §6.4's stat block: the brand-green block for the one number a screen is
   really about. §6.1: nothing gold can live on brand green -- text on brand
   is cream (6.88:1) or cream-muted (5.02:1), and nothing else. */
.stat-block {
  background: var(--brand);
  border-radius: var(--radius);
  padding: 1.125rem 1.25rem;
  margin: 1rem 0;
  color: var(--cream);
}
.stat-block .label { color: var(--cream-muted); }
.stat-value { margin: calc(var(--unit) * 2) 0 0; color: var(--cream); }
.stat-detail {
  margin: calc(var(--unit) * 1) 0 0;
  color: var(--cream-muted);
  font-size: var(--sm);
}

/* §6.4's notice, in three weights. */
.notice {
  background: var(--card);
  border-radius: var(--radius);
  padding: 0.875rem 1.125rem;
  margin: 1rem 0;
  font-size: var(--sm);
}
.notice-warning {
  background: var(--field);
  border-left: 3px solid var(--ink);
  box-shadow: inset 0 0 0 1px var(--line);
}
.notice-error { border-left: 3px solid var(--rust); }

/* §6.4's button, primary and quiet, usable as an action and as a link. */
.button {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 1rem;
  border-radius: var(--radius);
  font-size: var(--sm);
  font-weight: 600;
  text-decoration: none;
  cursor: pointer;
}
.button.quiet {
  border: 1px solid var(--line);
  background: var(--card);
  color: var(--ink);
}
.button.quiet:hover { background: var(--field); }
/* A quiet button whose action is destructive takes §6.1's rust on its **text**,
   never as a fill: rust ink is 6.38:1 on `card` and 4.98:1 on the `field` the
   quiet button hovers to, both past AA, while rust as a background would need a
   second text colour nobody has audited. Needs `.button` in the selector
   because `.button.quiet` sets `color` at the same specificity `.rust-ink`
   has — the class alone renders ink and looks like it worked. */
.button.rust-ink { color: var(--rust); }

/* ------------------------------------------------------- §8.6 the board */

.board { list-style: none; margin: 1rem 0; }
.board-row {
  display: grid;
  /* rank ~1.25rem, movement ~1.75rem, name flexible, gap ~4rem, points ~4rem */
  grid-template-columns: 1.5rem 1.75rem 1fr 3.25rem 4rem;
  align-items: center;
  gap: calc(var(--unit) * 2);
  padding: 0.625rem 1rem;
  min-height: 44px;
  border-bottom: 1px solid var(--line);
  border-left: 3px solid transparent;
  position: relative;
}
.board-row:last-child { border-bottom: 0; }

/* §8.6: your own row is tinted to the field colour and carries a 3px gold
   inset rail on its left edge (§6.4's inset rail, gold for "you"). */
.board-row.mine {
  background: var(--field);
  border-left-color: var(--gold);
}
.board-row .rank { font-weight: 700; color: var(--ink-muted); }
.board-row.mine .rank { color: var(--gold-ink); }

.board-row .who { min-width: 0; }
.board-row .who a {
  color: var(--ink);
  font-weight: 600;
  text-decoration: none;
  display: block;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* §6.3: minimum touch target 44px, and "a row that is a button counts as one
   target; it does not gain a second." §8.6 puts the link on the *name*, so the
   link is stretched to cover its whole row rather than adding a second
   tappable element beside it -- one target, full row, 44px by construction. */
.board-row .who a::after {
  content: "";
  position: absolute;
  inset: 0;
}
.board-row .who a:focus-visible::after {
  outline: 2px solid var(--chrome);
  outline-offset: -2px;
}
.board-row.mine .who a { font-weight: 700; }
.board-row .subline {
  display: block;
  font-size: var(--sm);
  color: var(--ink-muted);
}
.board-row .gap {
  text-align: right;
  font-size: var(--sm);
  color: var(--ink-muted);
}
.board-row .points {
  text-align: right;
  font-size: var(--lg);
  font-weight: 800;
  font-style: italic;
  font-stretch: 92%;
  letter-spacing: -0.02em;
}
/* §6.1: gold-ink for gold-meaning text on light surfaces; rust means falling. */
.move.up   { color: var(--gold-ink); font-weight: 700; }
.move.down { color: var(--rust); font-weight: 700; }
.move.none { color: var(--ink-muted); }
.movement  { font-size: var(--sm); }

/* --------------------------------------------------- §8.5 / §8.3 lineups */

.manager-card { padding: 1rem 1.125rem; }
/* T-11, measured 2026-08-07 by /design-review rather than argued.
 *
 * At 360x640 the reveal is 16.9 screens collapsed and 31.8 with every
 * breakdown open, and on **52% of the expanded page no manager name is on
 * screen at all** -- an expanded card runs 1120-1308px against a 640px
 * viewport, so the name is guaranteed to scroll off while you are still
 * inside that manager's lineup. That is the failure: not finding a manager,
 * but losing track of which one you are already reading.
 *
 * Sticky is the fix that matches it. The header carries the name AND the
 * total, so what pins is the answer to both "whose is this" and "what did
 * they score". `background` is required -- a sticky element with a
 * transparent background scrolls the rows underneath straight through it. */
.manager-head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: 1rem;
  padding-bottom: 0.5rem;
  border-bottom: 1px solid var(--line);
  position: sticky;
  top: 0;
  background: var(--card);
  padding-top: 0.5rem;
  margin-top: -0.5rem;
  z-index: 1;
}
/* T-11's index. A wrapping row of name chips, not a column: fourteen names
 * stacked would be its own screen of scrolling, which is the problem rather
 * than the fix. Chips are 44px tall so a thumb hits them (§6.3). */
.manager-index ul {
  list-style: none;
  margin: 0 0 1rem;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.375rem;
}
.manager-index a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 0.75rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--card);
  font-size: var(--sm);
  text-decoration: none;
  /* Explicit, because a link with no `color` renders in the user agent's
   * default blue -- which is not in §6's palette and was exactly what the
   * first draft of this index shipped. `.button.quiet` sets `--ink` at this
   * same specificity for the same reason. */
  color: var(--ink);
}
/* §8.5's cards are `scroll-margin-top`ed so a jump does not land the name
 * underneath the sticky header it shares a page with. */
.manager-card { scroll-margin-top: 0.5rem; }

.manager-head h2 { margin: 0; font-size: var(--lg); }
/* §8.5's `auto-filled` tag. Non-italic and muted, inside the wordmark heading
   — so it inherits the heading's size and has to be pulled back to `sm`, which
   is the whole of what this rule is for. */
.card-tag { font-style: normal; font-size: var(--sm); font-weight: 400; }
.manager-head p { margin: 0; }
.manager-total {
  font-size: var(--lg);
  font-weight: 800;
  font-style: italic;
  font-stretch: 92%;
  color: var(--ink-muted);
  margin: 0;
  white-space: nowrap;
}
/* §6.1: gold moves to points once any score exists for the week. */
.manager-total.has-score { color: var(--gold-ink); }

.slots { list-style: none; margin: 0; padding: 0; }
/* §6.3: a list of repeated items MAY reflow into multiple columns at a wider
   viewport -- the same composition, widened. §8.5's nine slots are the
   intended case. No region and no data column appears or disappears. */
.manager-card .slots {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0;
}
/* §8.5: "one column on a phone, three on wider viewports", and §6.3 permits a
   repeated list reflowing -- that is the same composition widened, not a
   different one. Two columns is the intermediate step of the same reflow: at
   48rem three columns leaves ~7rem for a name, which truncates most of them.
   See T-11, which is exactly this question and is deliberately not solved here.
   Measured for the PR rather than redesigned. */
@media (min-width: 34rem) {
  .manager-card .slots { grid-template-columns: repeat(2, 1fr); column-gap: 1.25rem; }
}
@media (min-width: 52rem) {
  .manager-card .slots { grid-template-columns: repeat(3, 1fr); column-gap: 1rem; }
}

.slot-row { border-bottom: 1px solid var(--line); }
.manager-card .slot-row:last-child,
.manager-card .slot-row:nth-last-child(-n+1) { border-bottom: 0; }

.slot-main {
  display: grid;
  grid-template-columns: 2.25rem 1fr 2.75rem 0.75rem;
  align-items: center;
  gap: calc(var(--unit) * 2);
  width: 100%;
  min-height: 44px;
  padding: 0.5rem 0;
  background: none;
  border: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
}
.own-lineup .slot-main { padding: 0.5rem 1rem; }
.slot-main[disabled] { cursor: default; }

.slot-name {
  font-size: var(--sm);
  font-weight: 700;
  color: var(--ink-muted);
}
.slot-main .player {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: var(--sm);
}
.slot-main .team { color: var(--ink-muted); font-size: var(--label); }
.slot-main .pts { text-align: right; font-size: var(--sm); font-weight: 600; }
.has-score-week .slot-main .pts { color: var(--gold-ink); }

/* §8.3: the row shows that it expands -- a small ink-muted chevron at the
   row's trailing edge, pointing down when collapsed and up when open. There is
   no hover on a phone and §6.3 forbids motion, so the affordance has to be a
   mark on the row. */
.chevron {
  justify-self: end;
  width: 0.5rem;
  height: 0.5rem;
  border-right: 2px solid var(--ink-muted);
  border-bottom: 2px solid var(--ink-muted);
  transform: rotate(45deg);
  margin-bottom: 0.25rem;
}
.slot-main[aria-expanded="true"] .chevron {
  transform: rotate(-135deg);
  margin-bottom: -0.25rem;
}

.breakdown {
  list-style: none;
  margin: 0;
  padding: 0 0 0.625rem 2.75rem;
  font-size: var(--sm);
  color: var(--ink-muted);
}
.own-lineup .breakdown { padding-left: 3.75rem; padding-right: 1rem; }
.breakdown li { display: flex; justify-content: space-between; gap: 1rem; }

.owns { list-style: none; }
.owns li {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.625rem 1rem;
  min-height: 44px;
  align-items: center;
  border-bottom: 1px solid var(--line);
  font-size: var(--sm);
}
.owns li:last-child { border-bottom: 0; }
.owns .player { font-weight: 600; }
.owns .owners { color: var(--ink-muted); text-align: right; }

/* ------------------------------------------------------------ §8.0 footer */

.footer-line {
  font-size: var(--sm);
  color: var(--ink-muted);
  margin: 1rem 0 0;
}
/* §8.0: on two consecutive failed refreshes the footer goes rust. The
   connection failure outranks a stale ingestion, because it is the one the
   manager can act on. */
.footer-line.failing { color: var(--rust); font-weight: 600; }

/* §6.1: no opacity on text -- dimming is applied to a row's surface, never to
   its content. rust at 45% over card is 2.09:1 and ink at 45% is 2.77:1. */

/* --------------------------------------------------- §8.7 standings */

.note-early { margin-top: calc(var(--unit) * 2); }

.pot-small { margin: calc(var(--unit) * 2) 0 0; }

/* §8.7: when a week has voided, the money it moved is stated, not implied. */
.pot-moved { margin: calc(var(--unit) * 2) 0 0; font-size: var(--sm); }
/* §6.1: rust carries "this will not happen", and §8.7 asks for the
   never-awarded line specifically in rust. */
.pot-never {
  margin: calc(var(--unit) * 2) 0 0;
  font-size: var(--sm);
  color: var(--rust);
  font-weight: 600;
}

.standings { list-style: none; margin: 1rem 0; }
/* §8.7: FOUR COLUMNS AT EVERY WIDTH. The five-column form was measured at
   360px and failed §14's own acceptance test, hiding 48px of Best behind a
   horizontal scroll; the permission to scroll horizontally was explicitly
   withdrawn, because a permitted scroll is how the checkbox gets marked green
   while failing. Best lives in the subline under the manager's name instead --
   the shape §8.6 already uses for `4 left`. Do not add a fifth column. */
.standings-row,
.standings-head {
  display: grid;
  grid-template-columns: 1.5rem 1fr 4.25rem 4rem;
  align-items: center;
  gap: calc(var(--unit) * 2);
  padding: 0.625rem 1rem;
  min-height: 44px;
  border-bottom: 1px solid var(--line);
}
.standings-row:last-child { border-bottom: 0; }
.standings-head {
  min-height: 0;
  padding-top: 0.5rem;
  padding-bottom: 0.5rem;
  background: var(--field);
  font-size: var(--label);
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--ink-muted);
  font-weight: 700;
}
.standings-head .third,
.standings-head .adjusted { text-align: right; }

.standings-row .rank { font-weight: 700; color: var(--ink-muted); }
.standings-row .who { min-width: 0; }
.standings-row .who .name {
  display: block;
  font-weight: 600;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.standings-row .subline {
  display: block;
  font-size: var(--sm);
  color: var(--ink-muted);
}
.standings-row .third {
  text-align: right;
  font-size: var(--sm);
  color: var(--ink-muted);
}
.standings-row .adjusted {
  text-align: right;
  font-size: var(--base);
  font-weight: 800;
  font-style: italic;
  font-stretch: 92%;
  letter-spacing: -0.02em;
}

/* §8.7's receipts: the week and the amount hold a fixed width on the first
   row and the names wrap beneath them, so a three-line two-name tie at 360px
   cannot push the amounts out of column. */
.receipts { list-style: none; }
.receipt {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 0 calc(var(--unit) * 2);
  padding: 0.625rem 1rem;
  border-bottom: 1px solid var(--line);
}
.receipt:last-child { border-bottom: 0; }
.rc-week { font-weight: 600; }
.rc-amount { text-align: right; font-weight: 600; }
.rc-who {
  grid-column: 1 / -1;
  font-size: var(--sm);
  color: var(--ink-muted);
}

/* §8.8's playoffs block stood here — the champion banner, the seeds card, the
   bracket rounds and the TBD / em-dash distinction. D48 removed §9.9, so every
   selector in it addressed markup no renderer emits. Deleted rather than kept
   "in case": dead CSS is the kind of thing a later reader treats as a
   constraint. `.rank`, `.who` and `.name` survive on §8.7's own rows. */

/* ------------------------------------------------------ §8.9 action log */

/* §8.9: "{Actor} {what they did} {(Week N)} {— detail}, with the timestamp
   right-aligned in small muted type". At 360px the timestamp carries a date, a
   time and a zone, so it wraps to its own line beneath the sentence rather than
   squeezing the sentence into a column two words wide. §6.3 permits a repeated
   list to reflow; it does not permit the row to stop being one row of meaning,
   so the stamp stays right-aligned at both widths. */
.log { list-style: none; }
.log-row {
  display: grid;
  grid-template-columns: 1fr;
  gap: 0.125rem calc(var(--unit) * 2);
  padding: 0.625rem 1rem;
  border-bottom: 1px solid var(--line);
}
.log-row:last-child { border-bottom: 0; }
.log-what { line-height: 1.45; }
.log-when {
  font-size: var(--sm);
  color: var(--ink-muted);
  text-align: right;
  white-space: nowrap;
}
@media (min-width: 34rem) {
  .log-row { grid-template-columns: 1fr auto; }
  .log-when { text-align: right; }
}

/* --------------------------------------- §8.11 salary upload / unmatched */

/* §6.4's button, primary variant: chrome fill, cream text. "Hover lightens the
   *fill*; text colour never changes on hover." */
.button.primary {
  background: var(--chrome);
  color: var(--cream);
  border: 0;
}
.button.primary:hover { background: var(--brand); }
.button.wide { width: 100%; justify-content: center; }
/* §8.11: when the list empties, [ Import the file again to open the week ] is
   "the largest thing on it". Size is how that is said. */
.button.huge { min-height: 60px; font-size: var(--lg); }

.upload-head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: calc(var(--unit) * 2);
}
.upload-head h1 { margin-bottom: 0; }
/* §8.11: "the time to lock beside it [...] going rust inside one hour." */
.lock-readout { color: var(--ink-muted); font-size: var(--sm); margin: 0; }
.lock-urgent { color: var(--rust); font-weight: 600; }
.back { margin: 0.75rem 0 0; }

/* §8.11: "Result message in brand green on success, rust on failure." The rust
   half is §6.4's error notice, already defined; this is the green half. */
.notice-good {
  border-left: 3px solid var(--brand);
  color: var(--brand);
}
.fail-list { list-style: none; margin: 0.5rem 0 0; padding: 0; }
.fail-list li {
  display: grid;
  grid-template-columns: 4.5rem 1fr;
  gap: calc(var(--unit) * 2);
  padding: 0.125rem 0;
  color: var(--ink);
}
.fail-line { color: var(--rust); font-weight: 600; }
.rust-ink { color: var(--rust); }

/* §8.11's running count sits above the list and is the thing that turns forty
   names from an hour into ten minutes. */
.running-count {
  font-weight: 600;
  margin: 0.5rem 0 0.75rem;
}

.unresolved-list { list-style: none; }
.unresolved {
  padding: 0.875rem 1rem;
  border-bottom: 1px solid var(--line);
}
.unresolved:last-child { border-bottom: 0; }
.un-name { font-weight: 600; margin: 0; }
.unresolved .detail { margin: 0.125rem 0 0; }
.un-form { margin-top: 0.625rem; }
.un-form .label { display: block; margin-bottom: 0.25rem; }
.or-add { margin: 0.5rem 0 0.25rem; }
.un-new { display: flex; flex-wrap: wrap; gap: calc(var(--unit) * 2); }
.un-new input { flex: 1 1 6rem; min-width: 0; }

/* §6: form controls inherit the type scale rather than the browser's. 44px is
   §6.3's minimum target and is not negotiable on the one screen with a
   deadline. */
.un-form select, .un-form input, .import-form input {
  width: 100%;
  min-height: 44px;
  padding: 0 0.625rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--card);
  color: var(--ink);
  font: inherit;
  font-size: var(--sm);
}
.un-form button { margin-top: 0.625rem; }

/* §8.11: "a bulk action in its own tinted block". The page background is
   already `field`, so a bare field fill is not a block -- it is invisible. The
   1px inset line is §6.4's warning-notice treatment, reused rather than
   invented, which is what makes the tint read as a boundary. */
.add-all {
  background: var(--field);
  box-shadow: inset 0 0 0 1px var(--line);
  border-radius: var(--radius);
  padding: 1rem 1.125rem;
  margin: 1rem 0;
}
.add-all .detail { margin: 0.625rem 0 0; }

.all-resolved { margin: 1rem 0; }
.all-resolved .stat-value { margin: 0 0 0.75rem; }

.import-form .label { display: block; margin: 0.75rem 0 0.25rem; }
.import-form .label:first-child { margin-top: 0; }
.import-form button { margin-top: 0.875rem; }
.import-form .stage { margin: 0.5rem 0 0; }
.replace-file summary {
  cursor: pointer;
  font-size: var(--sm);
  color: var(--ink-muted);
  min-height: 44px;
  display: flex;
  align-items: center;
}

/* ═══════════════════════════════════════════════════ §8.4 the lineup builder
 *
 * "The centrepiece screen. A manager makes nine consecutive priced decisions
 * under a cap, usually on a phone." 360px is §6.3's design width and every
 * measurement below was taken there.
 *
 * Two rules from §8.4 are enforced here and are easy to break by accident:
 *
 *   1. **The row's surface dims, never its content.** A whole-row fade to 45%
 *      puts the name at 2.77:1, the salary at 2.09:1 and the subline at 2.05:1
 *      against AA's 4.5:1, "and no opacity value dims perceptibly while still
 *      clearing the bar." So `.unaffordable` sets a `field` background and
 *      touches no colour inside the row. There is no `opacity` in this block
 *      and there must never be one.
 *   2. **§6.3 permits exactly one sticky element in the app and it is the cap
 *      bar.** It pins to the viewport, not beneath §7's header — the header is
 *      not sticky, cannot become sticky, and is not a fixed height (it wraps to
 *      two or three rows at 360px).
 */

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  clip-path: inset(50%);
  overflow: hidden;
  white-space: nowrap;
}

/* --------------------------------------------------- block 1: cap readout */

.cap-readout {
  display: flex;
  gap: calc(var(--unit) * 2);
  margin: 1rem 0;
}
.cap-left {
  flex: 1 1 auto;
  background: var(--brand);
  color: var(--cream);
  border-radius: var(--radius);
  padding: 0.875rem 1rem;
}
/* "When the remaining cap goes negative, the whole left block inverts — a rust
   fill with cream text (5.60:1), label and value together." */
.cap-left.over { background: var(--rust); }
.cap-right {
  flex: 0 0 6rem;
  background: var(--chrome);
  color: var(--cream);
  border-radius: var(--radius);
  padding: 0.875rem 1rem;
}
.cap-label {
  margin: 0;
  font-size: var(--label);
  text-transform: uppercase;
  letter-spacing: 0.14em;
  color: var(--cream-muted);
}
.cap-value { margin: calc(var(--unit) * 2) 0 0; color: var(--cream); }

/* ---------------------------------------------------- block 1a: the bar */

.sticky-bar {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 20;
  min-height: 44px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: calc(var(--unit) * 3);
  padding: 0 1rem;
  background: var(--chrome);
  color: var(--cream);
  font-size: var(--sm);
  font-weight: 600;
}
.sticky-bar[hidden] { display: none; }
.sticky-bar.over { background: var(--rust); }
.sticky-bar.urgent .sticky-lock { color: var(--gold); }
.sticky-lock { color: var(--cream-muted); font-weight: 500; }
.sticky-bar.over .sticky-lock { color: var(--cream); }

/* "Every focusable row reserves the sticky bar's height as scroll clearance, so
   a row scrolled into view by a focus change lands below the bar rather than
   underneath it. [...] It is invisible with a mouse, which means it is
   invisible in ordinary testing and only fails for the users these rules exist
   to serve." */
.roster-row, .pool-row { scroll-margin-top: 52px; }

/* ------------------------------------------------------ block 2: roster */

.roster-card, .pool-card { padding: 0; overflow: hidden; }
.roster, .pool { list-style: none; margin: 0; padding: 0; }

.roster-row {
  width: 100%;
  display: grid;
  /* D57: the right column is 5.25rem rather than 4.5rem because it now carries
     `20.0 FPPG` beneath the price. The 0.75rem comes out of the name column,
     which ellipsises; the price column is tabular and must not wrap. */
  grid-template-columns: 3.25rem 1fr 5.25rem;
  align-items: center;
  gap: calc(var(--unit) * 2);
  min-height: 48px;
  padding: 0.5rem 0.875rem;
  border: 0;
  border-bottom: 1px solid var(--line);
  border-left: 3px solid transparent;
  background: var(--card);
  color: var(--ink);
  text-align: left;
  font-size: var(--base);
  cursor: pointer;
}
.roster li:last-child .roster-row { border-bottom: 0; }
/* "The active row is tinted to the field colour and carries a 3px brand-green
   inset rail on its left edge." */
.roster-row.active {
  background: var(--field);
  border-left-color: var(--brand);
}
.rr-slot { font-weight: 700; font-size: var(--sm); color: var(--ink-muted); }
.roster-row.active .rr-slot { color: var(--ink); }
.rr-who { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0.375rem; }
.rr-name { font-weight: 600; }
.rr-team { font-size: var(--sm); }
.rr-empty { color: var(--ink-muted); font-style: italic; }
/* D57: price over provider average, right-aligned as one cell. Column, not two
   grid cells, so the pair stays together at every width and the row keeps its
   three-column shape. */
.rr-right, .pr-right {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  justify-content: center;
  gap: 0.0625rem;
}
.rr-salary {
  text-align: right;
  color: var(--gold-ink);
  font-variant-numeric: tabular-nums;
  font-weight: 600;
}
.badge-out {
  color: var(--rust);
  font-size: var(--label);
  font-weight: 800;
  letter-spacing: 0.08em;
}
.badge-q { color: var(--ink-muted); font-size: var(--label); font-weight: 700; }
/* "A row holding a ruled-out player says what to do about it." */
.rr-ruled-out {
  flex-basis: 100%;
  color: var(--rust);
  font-size: var(--sm);
  font-weight: 600;
}
.unsaved { margin: 0.5rem 0 0; }

/* --------------------------------------- blocks 3 and 4: header, toggles */

.pool-head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: calc(var(--unit) * 2);
  margin: 1.5rem 0 0.5rem;
}
.pool-head h2 { margin: 0; }
#pool-search {
  flex: 1 1 8rem;
  min-height: 44px;
  padding: 0 0.75rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--card);
  color: var(--ink);
  font-size: var(--base);
}

.toggles {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: calc(var(--unit) * 2);
  margin-bottom: 0.5rem;
}
/* "a two-button segmented control inside a bordered rounded container; the
   selected option is chrome-filled with cream text, the other is white with
   muted text. Each button carries its own pressed state for assistive
   technology." */
.segmented {
  display: inline-flex;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  background: var(--card);
}
/* §8.4: "**Two toggles** on one line beneath — mode on the left, sort on the
   right." Measured at 360px: the four labels plus §6.3's 1.5rem gutters leave
   312px, so the horizontal padding is 0.625rem and not a rem — at 0.875rem the
   pair wraps to two rows and the sentence stops being true on the one width
   §6.3 designs for. The 44px target height is untouched. */
.segmented button {
  min-height: 44px;
  padding: 0 0.625rem;
  border: 0;
  background: var(--card);
  color: var(--ink-muted);
  font-size: var(--sm);
  font-weight: 600;
  white-space: nowrap;
  cursor: pointer;
}
.segmented button[aria-pressed="true"] {
  background: var(--chrome);
  color: var(--cream);
}

/* -------------------------------------------------------- block 5: pool */

/* "a flush white card, scrollable, capped at ~24rem tall with contained
   overscroll." */
.pool-card { max-height: 24rem; overflow-y: auto; overscroll-behavior: contain; }
.pool-row {
  width: 100%;
  display: grid;
  /* D57: 5.25rem, for `20.0 FPPG` beneath the price. See `.roster-row`. */
  grid-template-columns: 1fr 5.25rem;
  align-items: center;
  gap: calc(var(--unit) * 2);
  min-height: 48px;
  padding: 0.5rem 0.875rem;
  border: 0;
  border-bottom: 1px solid var(--line);
  background: var(--card);
  color: var(--ink);
  text-align: left;
  font-size: var(--base);
  cursor: pointer;
}
.pool li:last-child .pool-row { border-bottom: 0; }
/* THE DIM IS ON THE SURFACE. No opacity, no colour change inside the row —
   every foreground here stays at 6.38:1 or better against the field wash. */
.pool-row.unaffordable { background: var(--field); }
/* "A player already assigned to another slot renders at full strength on an
   undimmed surface, and the `In {SLOT}` label carries the information the dim
   used to." */
.pool-row.assigned { background: var(--card); }
/* "The row is tinted to the field colour when it holds the player currently in
   the active slot." */
.pool-row.in-active-slot { background: var(--field); }
.pr-main { display: flex; flex-wrap: wrap; align-items: baseline; }
.pr-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; }
.pr-sub { flex-basis: 100%; }
.pr-in { color: var(--brand); font-weight: 600; }
.pr-salary {
  text-align: right;
  font-weight: 700;
  color: var(--gold-ink);
  font-variant-numeric: tabular-nums;
}
/* "gold-ink when affordable, rust when not" */
.pr-salary.over { color: var(--rust); }
/* D57: FanDuel's average, and it must not read as a second price. Muted ink,
   `sm`, no gold and no rust — gold means salary on this screen (§6.1) and rust
   means something has gone wrong. It stays at full opacity inside an
   unaffordable row like every other foreground here: the surface dims, the
   content never does.

   One class for both rows: a roster row and a pool row render the same span
   through the same helper (`html_render._fppg`), so a second class name would
   be two things to keep in step for no visual difference. */
.pr-fppg {
  line-height: 1.1;
  white-space: nowrap;
}
.pool-empty { padding: 1rem 0.875rem; }

/* ------------------------------------- blocks 6, 7 and 8: say, note, save */

.live-region { margin: 0.5rem 0 0; min-height: 1.2em; color: var(--ink-muted); }
.live-region.problem { color: var(--rust); font-weight: 700; }
.hidden-note { margin: 0.375rem 0 0; }

.save {
  width: 100%;
  justify-content: center;
  min-height: 52px;
  margin-top: 1rem;
  border: 0;
  border-radius: var(--radius);
  background: var(--chrome);
  color: var(--cream);
  font-size: var(--lg);
  font-weight: 600;
  cursor: pointer;
}
.save:hover { background: var(--brand); }
.save[disabled] { cursor: progress; }
.save-status { margin: 0.5rem 0 0; }
.save-status.saved { color: var(--brand); font-weight: 600; }
/* A rejection and a dropped connection are both rust and must still be
   distinguishable: one is a heading with a list, the other is one line. */
.save-status.illegal, .save-status.transit { color: var(--rust); }
.problems { margin: 0.375rem 0 0 1.125rem; padding: 0; }

/* §8.4's lock refusal. Not a bullet among §9.2's problems — a different block
   entirely, which is the structural version of that rule. */
.notice-locked {
  border-left: 3px solid var(--rust);
  background: var(--card);
}
.locked-head { margin: 0; }
.notice-locked strong { color: var(--rust); }
.locked-lineup { padding: 0; overflow: hidden; }
.locked-row {
  display: grid;
  grid-template-columns: 3.25rem 1fr 4.5rem;
  align-items: center;
  gap: calc(var(--unit) * 2);
  min-height: 40px;
  padding: 0.25rem 0.875rem;
  border-bottom: 1px solid var(--line);
}
.locked-lineup li:last-child { border-bottom: 0; }
.locked-total { padding: 0.625rem 0.875rem; margin: 0; }
.quiet-link { font-size: var(--sm); }
.quiet-link a { color: var(--ink-muted); }

.page-title { margin: 1.25rem 0 0.25rem; }
.page-detail { margin: 0 0 0.5rem; }

/* ------------------------------------------ §8.1 and §8.12 — the way in */

/* §7: "Right (signed in only): the manager's display name in `cream-muted`,
   and beneath or beside it a `quiet` Sign out control." Beside it, because the
   header already wraps on a phone and a second stacked row costs the 56px
   chrome another 44. */
.who-out {
  display: flex;
  align-items: center;
  gap: calc(var(--unit) * 2);
  min-width: 0;
}
/* D46: the identity shares a row with the wordmark now, so a long chosen name
   has to give way rather than push Sign out off the edge — the control that
   ends a session on a borrowed phone is the half of this pair that must always
   be reachable. §8.14's 24-character ceiling means the pair takes a row of its
   own before it ever has to clip (measured: 170px of header at 360px, against
   134px for an ordinary name); the ellipsis is what stops a name deciding
   where Sign out lands in between. */
.whoami {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* D44: the name in the header is a link to §8.14. It must not read as one —
   §7 specifies "the manager's display name in `cream-muted`" and a blue
   underlined name in the chrome would be the loudest thing on every screen —
   so it takes the colour it always had and shows its hand on hover and focus. */
.whoami { color: var(--cream-muted); text-decoration: none; }
.whoami:hover, .whoami:focus-visible { text-decoration: underline; }
.signout { margin: 0; }
/* The control sits on the green chrome, so §6.4's `quiet` treatment — a card
   fill with an ink border — would be a white block in the header. Same shape,
   chrome-side colours. */
.chrome .button.quiet {
  background: transparent;
  border-color: var(--chrome-line);
  color: var(--cream-muted);
  min-height: 36px;
  padding: 0 0.625rem;
}
.chrome .button.quiet:hover { background: var(--brand); color: var(--cream); }

/* §8.1 and §8.12: "Narrow, vertically centred, ~24rem wide." Centred against
   the viewport rather than against the content, so the card does not drift as
   a state replaces the form with a shorter card. */
.door {
  max-width: 24rem;
  margin: 0 auto;
  min-height: calc(100vh - 56px - 4rem);
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.door h1 { margin: 0 0 0.25rem; }
.door .detail { margin: 0 0 1.25rem; }
.door .card { margin: 0; }
.door-form { margin: 0; }
.door-form label { display: block; margin-bottom: 0.25rem; }
/* §8.14's field is `type="text"` and shares §8.1's, because they are the same
   control on the same card at the same width — a second rule would be two
   things to keep in step for no visible difference. */
.door-form input[type="email"],
.door-form input[type="text"] {
  width: 100%;
  min-height: 44px;
  padding: 0 0.625rem;
  margin-bottom: 1rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--card);
  color: var(--ink);
  font: inherit;
  font-size: var(--sm);
}
/* §8.1's code field (D50). The one input in the product that is **read off a
   second screen and copied**, which is what every departure from the rule above
   is for: the characters are set apart so a 7 cannot be handed back as a 1, and
   they are large enough to check against the email without zooming. Uppercase
   is a transform rather than a constraint — `access.normalize_code()` folds
   case anyway, so this only means what the member sees matches what they are
   copying.

   `text-transform` needs the placeholder rule beside it: it does not apply to
   placeholder text in every engine, and a lowercase ghost in a field that
   uppercases what you type reads as a bug. */
.door-form input.code-field {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--xl);
  font-weight: 600;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}
.door-form input.code-field::placeholder {
  color: var(--ink-muted);
  font-weight: 400;
  letter-spacing: 0.18em;
  text-transform: uppercase;
}
.door .code-failed { margin: 1rem 0 0; }
/* §8.1's way back, on the card (D51). A hairline and a gap, because it is a
   second thing to do on a screen that already has a primary one — the code box
   keeps the button, and this keeps the member from being stuck when the email
   never came. */
.door-again {
  margin-top: 1.5rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--line);
}
.door-again .detail { margin: 0 0 0.75rem; }

.door-action { margin: 1rem 0 0; }
/* §8.1: the arrival notice sits **above** the heading — "someone arriving from
   a dead link needs to be told why before they are asked to do anything." */
.door .arrival { margin: 0 0 1.25rem; }
.door .send-failed { margin: 1rem 0 0; }
.door .sent + .door-form { margin-top: 1rem; }
.door .failed p { margin: 0; font-weight: 600; }

/* The demo's mailbox (server._demo_outbox) — declared demo furniture, not a
   product screen, so it borrows §6.4's card and button and invents nothing. */
.outbox, .outbox-letters { list-style: none; margin: 1rem 0; padding: 0; }
.outbox-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  flex-wrap: wrap;
  padding: 0.5rem 0;
  border-bottom: 1px solid var(--line);
}
.outbox-row form { margin: 0; }
.outbox-row .detail { font-size: var(--sm); }
.outbox-letters li { margin-bottom: 0.75rem; }
/* D50: the code, in the demo's mailbox. Set like §8.1's field rather than like
   body text — the whole job of this line is to be read off one screen and typed
   into another, which is the same job the field has. */
.letter-code {
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: var(--xl);
  font-weight: 600;
  letter-spacing: 0.18em;
}
.outbox-letters p { margin: 0 0 0.375rem; }

/* ─────────────────────────────────────── §8.10 the commissioner dashboard
   "Inherits the token layer and gets no bespoke layout work." Everything below
   is layout: the banners are §6.4's notice in its three weights, unchanged, and
   the forms borrow §8.11's field styling rather than growing their own.

   Built one-column first. §8.10's banner stack is the part most likely to be
   unusable at 360px — four notices with an action apiece — so the action sits
   on its own line under the text at phone width and moves inline at 34rem. */
.banner { display: grid; gap: calc(var(--unit) * 2); }
.banner-text { margin: 0; }
.banner-do { margin: 0; }

.admin-block { margin: 0 0 1.5rem; }
.admin-empty { margin: 1rem 1.125rem; color: var(--ink-muted); }
.admin-footer { margin: 1.5rem 0 0; }

.admin-weeks { list-style: none; margin: 0; padding: 0; }
.admin-week {
  display: grid;
  grid-template-columns: auto auto;
  align-items: baseline;
  gap: 0.125rem calc(var(--unit) * 2);
  padding: 0.75rem 1.125rem;
  border-bottom: 1px solid var(--line);
}
.admin-week:last-child { border-bottom: 0; }
.aw-name { font-weight: 600; }
.aw-status { text-transform: uppercase; font-size: var(--label); letter-spacing: 0.06em; }
.aw-lock, .aw-pool { grid-column: 1 / -1; }
.aw-do { grid-column: 1 / -1; display: flex; flex-wrap: wrap; gap: calc(var(--unit) * 2); margin-top: calc(var(--unit) * 2); }
.aw-do form { margin: 0; }
/* Who has not saved a lineup, on an `open` week. Full width in both layouts
   for the reason `.aw-edit` is: it carries names, and there is no column
   width that holds "still out: Dana, Kev, Marcus" and also holds one name. */
.aw-lineups { grid-column: 1 / -1; margin: calc(var(--unit) * 2) 0 0; }

/* D34's Reschedule and Delete. Full width in both layouts — it holds a date
   field, and §8.11's `.replace-file` already set the rule that a disclosure's
   summary is a 44px target in muted small type. Closed, it costs the row one
   line; open, it pushes nothing sideways at 360px. */
.aw-edit { grid-column: 1 / -1; margin-top: calc(var(--unit) * 2); }
.aw-edit summary {
  cursor: pointer;
  font-size: var(--sm);
  color: var(--ink-muted);
  min-height: 44px;
  display: flex;
  align-items: center;
}
.aw-edit .admin-form { margin-top: 0; }
.week-drop { margin: calc(var(--unit) * 3) 0 calc(var(--unit) * 2); }
.week-drop .detail { margin: 0 0 0.5rem; }

.admin-form { margin: 0.75rem 0 0; display: grid; gap: calc(var(--unit) * 1); }
.admin-form .label { margin-top: calc(var(--unit) * 2); }
.admin-form input {
  width: 100%;
  min-height: 44px;
  padding: 0 0.625rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--card);
  color: var(--ink);
  font: inherit;
  font-size: var(--sm);
}
.admin-form button { margin-top: 0.75rem; justify-self: start; }

.invite-list { list-style: none; margin: 1rem 0 0; padding: 0; }
.invite-row {
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 0.125rem calc(var(--unit) * 2);
  padding: 0.625rem 0;
  border-bottom: 1px solid var(--line);
}
.invite-row:last-child { border-bottom: 0; }
/* An address is long and a phone is 360px: it wraps rather than widening the
   card, which is the same rule §14 states for a long manager name. */
.iv-who { font-weight: 600; overflow-wrap: anywhere; }
.iv-status { text-transform: uppercase; font-size: var(--label); letter-spacing: 0.06em; }
.iv-why { grid-column: 1 / -1; }
/* D38 put a second control on the row, so this is a flex row rather than a
   slot holding one form. On a phone the two sit side by side and wrap to a
   second line before either narrows; `Invite again` reads first because the
   stranded row is the one that has both, and re-inviting is what that banner
   sends the commissioner here to do. */
.iv-do {
  grid-column: 1 / -1;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: calc(var(--unit) * 2);
}
.iv-do form { margin: calc(var(--unit) * 2) 0 0; }
.ceiling-list { margin: 0.5rem 0 0.5rem 1rem; font-size: var(--sm); }
.share-url { margin: 0.5rem 0 0; overflow-wrap: anywhere; }

@media (min-width: 34rem) {
  .banner {
    grid-template-columns: 1fr auto;
    align-items: center;
    gap: 1rem;
  }
  /* §8.10's row, as one row: `Week N`, its status, the lock and cap, the pool
     state, then the actions. Eighteen of these stack, so an action wrapping to
     a line of its own doubles the table's height for nothing. */
  .admin-week {
    grid-template-columns: 5rem 4.5rem 1fr auto auto;
    align-items: center;
  }
  .aw-lock, .aw-pool { grid-column: auto; }
  .aw-pool { text-align: right; }
  .aw-do { grid-column: auto; margin-top: 0; justify-content: flex-end; }
  /* Still full width: the row above is five columns of one line each, and a
     date field does not belong in any of them. */
  .aw-edit { margin-top: 0; }
  .aw-edit .admin-form { max-width: 22rem; }
  .invite-row { grid-template-columns: 1fr 6rem auto; align-items: center; }
  .iv-why { grid-column: 1 / -1; }
  .iv-do { grid-column: auto; }
  .iv-do form { margin: 0; }
}

/* ─────────────────────────────────────────────────────────── §8.2 home */

/* §8.2's call to action: "a gold fill with ink text (7.01:1). On hover the
   fill lightens toward cream and the text stays ink." §6.1 adds the failure
   this is written against: "cream text on gold is 1.98:1 and must never ship",
   and §14 says the failures all lived in *derived* colours rather than in the
   token table.

   Two things make that structural rather than careful. `color` is set once and
   the hover rule does not restate it, so there is no second place for the text
   colour to drift to. And `--gold-lift` is a declared token rather than an
   inline lighten — §6.1's rule is that no screen hard-codes a raw colour, and a
   hover computed in the ruleset is a raw colour nobody audits. Both pairings
   are computed from these two lines by `app/tests/test_home.py`:
   ink on gold 7.01:1, ink on the lift 8.08:1. */
.button.gold {
  background: var(--gold);
  color: var(--ink);
  border: 0;
}
.button.gold:hover { background: var(--gold-lift); }

/* §6.1: the focus card is a **feature block**, so it is brand green, and text
   on brand green is cream or cream-muted and nothing else. §8.2 gives gold one
   meaning domain on this screen — "the one action the page wants" — which is
   the button and nothing else here. */
.focus {
  background: var(--brand);
  border-radius: var(--radius);
  padding: 1.25rem;
  margin: 1rem 0 1.5rem;
}
.focus-head {
  margin: 0;
  color: var(--cream);
  font-size: var(--xxl);
}
.focus-body {
  margin: 0.5rem 0 0;
  color: var(--cream);
  max-width: 34rem;
}
.focus-line {
  margin: 0.375rem 0 0;
  color: var(--cream-muted);
  font-size: var(--sm);
}
.focus-do { margin: 1rem 0 0; }

/* ── D46's no-weeks state. It is `.focus` — same block, same brand green, same
   gold CTA — so the page has its anchor before it has a season. Everything
   below is the setup list inside it: text on brand green is cream or
   cream-muted and nothing else (§6.1), and the marker column is sized off the
   text so a `✓` and a `3` occupy the same width and the titles line up. */
.setup {
  list-style: none;
  margin: 1rem 0 0;
  padding: 0;
  border-top: 1px solid var(--brand-line);
}
.setup li {
  display: grid;
  grid-template-columns: 1.5rem 1fr;
  gap: calc(var(--unit) * 3);
  padding: 0.6875rem 0;
  border-bottom: 1px solid var(--brand-line);
}
.su-mark {
  font-size: var(--sm);
  font-weight: 700;
  line-height: 1.5;
  text-align: center;
  color: var(--cream-muted);
}
.setup .done .su-mark { color: var(--cream); }
.su-what { min-width: 0; }
.su-title { display: block; color: var(--cream); font-weight: 600; }
.su-detail {
  display: block;
  color: var(--cream-muted);
  font-size: var(--sm);
  margin-top: calc(var(--unit) * 0.5);
}

/* D46's four sentences, shown only while there is no week to point at. */
.hiw { list-style: none; margin: 0; padding: 0; }
.hiw-row {
  padding: 0.75rem 1.125rem;
  border-bottom: 1px solid var(--line);
}
.hiw-row:last-child { border-bottom: 0; }
.hiw-what { display: block; font-weight: 600; }
.hiw-detail {
  display: block;
  color: var(--ink-muted);
  font-size: var(--sm);
  margin-top: calc(var(--unit) * 0.5);
  max-width: 40rem;
}

/* The add-to-home-screen hint, shown by `app.js` to iOS Safari and nobody else.
   `[hidden]` needs the explicit rule because `display: flex` below would
   otherwise beat the attribute's own `display: none` — the same reason
   `.sticky-bar[hidden]` carries one, and a lesson this file has already had. */
.hs-hint {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: calc(var(--unit) * 3);
  margin: 1.5rem 0 0.5rem;
  padding: 0.875rem 1.125rem;
  border: 1px dashed var(--line);
  border-radius: var(--radius);
  color: var(--ink-muted);
  font-size: var(--sm);
}
.hs-hint[hidden] { display: none; }
/* Dashed and unfilled on purpose: §6.4's `.notice` is how this app says
   something about the league — a lock, a refusal, a score. A tip about a
   bookmark borrowing that surface would read as league news at a glance. */
/* The sentence takes the row and the button takes what is left; at 360px there
   is nothing left, the button wraps, and `margin-left: auto` sends it to the
   right edge rather than leaving it stranded under the first word. */
.hs-text { flex: 1 1 14rem; margin: 0; min-width: 0; }
.hs-text b { color: var(--ink); font-weight: 600; }
.hs-dismiss { margin-left: auto; }
/* The glyph sits on the text baseline rather than centred on the line box; at
   `--sm` a centred icon rides visibly high between two words. */
.hs-share {
  width: 1em;
  height: 1em;
  vertical-align: -0.125em;
}

/* §8.2's slate card — the week's own games, under the focus card.

   **The mark is a fixed box and the row is laid out without it.** The logos
   come from the scoreboard host (see `html_render.TEAM_LOGO`) and any one of
   them can fail to arrive; `width`/`height` on the tag and `flex: none` here
   mean the row occupies the same space either way, so a card that finishes
   loading does not push itself down the screen under a thumb. `object-fit`
   keeps a mark that is not square from being stretched into one.

   Two columns and not three (the All-weeks card's shape) because the middle
   column there is a status word every row has, and here it is a word that
   *replaces* the time on the two rows out of thirteen that have one. */
.slate { list-style: none; margin: 0; padding: 0; }
.slate-when { margin: calc(var(--unit) * 2) 0 0; }
.slate-game {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: calc(var(--unit) * 3);
  padding: 0.5rem 1.125rem;
  border-bottom: 1px solid var(--line);
}
.slate-game:last-child { border-bottom: 0; }
.sg-teams {
  display: flex;
  align-items: center;
  gap: calc(var(--unit) * 2);
  min-width: 0;
}
/* **The `@` is a column and not a space between two words.** Abbreviations run
   two and three characters, so laid out as flowed text the `@` and the home
   team start at a different x on every row and thirteen of them wobble down the
   card — the failure the All-weeks card's own comment records being caught in a
   browser at 1280px. A minimum on the away block is the whole fix: it is wide
   enough for the longest mark-plus-abbreviation, so nothing is clipped and
   every row lines up. */
.sg-team {
  display: inline-flex;
  align-items: center;
  gap: calc(var(--unit) * 1.5);
}
.sg-teams .sg-team:first-child { min-width: 4rem; }
.sg-logo { width: 22px; height: 22px; flex: none; object-fit: contain; }
.sg-abbr { font-weight: 600; }
.sg-at { color: var(--ink-muted); font-size: var(--sm); }
.sg-note { text-align: right; white-space: nowrap; }
/* The word a game says about itself, in the same uppercase `--label` treatment
   the All-weeks card gives a status — one screen, one way of saying `final`. */
.sg-word {
  text-transform: uppercase;
  font-size: var(--label);
  font-weight: 600;
  letter-spacing: 0.06em;
  color: var(--ink-muted);
}
/* A game being played is the one row on a Sunday worth finding at a glance, so
   it takes full ink where a finished one stays muted. Nothing gold: §8.2 gives
   gold one meaning on this screen and it is the button in the block above. */
.slate-game[data-status="in_progress"] .sg-word { color: var(--ink); }

/* §8.2's All weeks card. Three columns at every width — at 360px the status
   word is uppercase `--label` type and the third column is `nowrap`, so the
   week number never wraps under it (T-11's failure one screen over).

   **The status rides with the week number, and the third column takes the
   `1fr`.** §8.2 asks for the third column right-aligned and says nothing about
   the second, and the obvious `1fr auto auto` is wrong once T-12 lands: the two
   `auto` columns pack right, so the status word's position is decided by how
   wide that row's *score* is and the `FINAL`s wobble down the card. Measured in
   a browser at 1280px before it was noticed. */
.home-weeks { list-style: none; margin: 0; padding: 0; }
.home-week { border-bottom: 1px solid var(--line); }
.home-week:last-child { border-bottom: 0; }
.hw-link {
  display: grid;
  grid-template-columns: auto auto 1fr;
  align-items: baseline;
  gap: calc(var(--unit) * 3);
  padding: 0.75rem 1.125rem;
  min-height: 44px;
  text-decoration: none;
  color: var(--ink);
}
/* §8.2: "Rows highlight to the field colour on hover." */
.hw-link:hover { background: var(--field); }
/* `Week 18` and its status never wrap; the note in the `1fr` does, if it must.
   At 360px the one row with a long note is the `pending` week, whose lock time
   is a full Eastern stamp with its zone (§7) — and `Week` over `18` is a worse
   row than a stamp on two lines. */
.hw-name { font-weight: 600; white-space: nowrap; }
.hw-status {
  text-transform: uppercase;
  font-size: var(--label);
  letter-spacing: 0.06em;
  color: var(--ink-muted);
  white-space: nowrap;
}
.hw-note { text-align: right; }

/* ────────────────────────────────────────── §8.13 how scoring works */

.sc-half { margin: 1rem 0 1.5rem; font-weight: 600; }
.sc-card { list-style: none; margin: 0 0 1.25rem; padding: 0; }
.sc-row {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: baseline;
  gap: calc(var(--unit) * 3);
  padding: 0.5rem 1.125rem;
  border-bottom: 1px solid var(--line);
}
.sc-row:last-child { border-bottom: 0; }
.sc-value { text-align: right; white-space: nowrap; font-weight: 600; }
.sc-unit { font-weight: 400; }
/* §8.13: an unlabelled constant "renders loudly, never vanishes". Loud is the
   monospaced stat id — it reads as a defect report on the page, which is what
   it is, rather than as a rule somebody named badly. */
.sc-raw .sc-what { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; }
.sc-note { margin: 0 0 0.75rem; max-width: 40rem; }

/* §8.13's first half — the rules of the game. Two-line rows rather than the
   constants' two columns: a rule is a sentence and a sentence does not fit in
   a right-aligned column at 360px. `sc-half` keeps its margin above this, so
   the most-argued constant in the league is still the first thing under the
   heading and the jump link is directly beneath it. */
.sc-jump { margin: 0 0 1.25rem; }
/* The page's two section labels sit directly above a card that carries a label
   of its own, at the same size — measured at 390px, `HOW A WEEK WORKS` over
   `WHAT THIS GAME IS` reads as one heading that wrapped. The hairline is what
   makes them two halves of a page rather than two lines of a title. */
.sc-section {
  margin: 2rem 0 1rem;
  padding-top: 1.25rem;
  border-top: 1px solid var(--line);
  color: var(--ink);
  /* A size step above the card labels beneath it, because at `--label` the two
     are the same tiny uppercase line and the rule alone was not enough. */
  font-size: var(--sm);
  letter-spacing: 0.08em;
}
.rl-card { list-style: none; margin: 0 0 1.25rem; padding: 0; }
.rl-row {
  padding: 0.75rem 1.125rem;
  border-bottom: 1px solid var(--line);
}
.rl-row:last-child { border-bottom: 0; }
.rl-what { display: block; font-weight: 600; }
.rl-detail {
  display: block;
  color: var(--ink-muted);
  font-size: var(--sm);
  margin-top: calc(var(--unit) * 0.5);
  /* §6.3's measure. These are the longest sentences in the product and an
     unbounded line on a 1280px desktop is the one place this page reads as a
     document rather than as a screen. */
  max-width: 40rem;
}
