:root {
  color-scheme: light dark;
  --bg: #faf8f5; --fg: #1c1a17; --dim: #6b6560; --line: #e0dad2;
  --card: #fff; --mount: #ece7df; --accent: #8a5a2b; --accent-fg: #fff;
  --good: #2f7d52; --bad: #a8362f; --panel: #fff;
  /* Right household, wrong person: a third state, and neither of the other
     two. The prototype's own `--warm`. */
  --near: #a76c25;
}
@media (prefers-color-scheme: dark) {
  :root {
    --bg: #16151a; --fg: #ece8e3; --dim: #9a938c; --line: #312d36;
    --card: #201e26; --mount: #191820; --accent: #d9a066; --accent-fg: #16151a;
    --good: #6fc08d; --bad: #e2796f; --panel: #201e26;
    --near: #dca55c;
  }
}
* { box-sizing: border-box; }
body {
  margin: 0; background: var(--bg); color: var(--fg);
  font: 15px/1.45 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  padding: 0 0 40px;
}
h1, h2 { font-weight: 650; letter-spacing: -0.02em; }
h2 { margin: 28px 16px 8px; }
a { color: var(--accent); }
button { font: inherit; cursor: pointer; }

.bar {
  position: sticky; top: 0; z-index: 5; background: var(--bg);
  border-bottom: 1px solid var(--line); padding: 10px 16px;
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
}
.bar .who { color: var(--dim); font-size: 13px; margin-left: 6px; }
.tally { font-variant-numeric: tabular-nums; font-weight: 650; }
.back { font-size: 14px; text-decoration: none; }
.bar .nav { display: flex; gap: 14px; align-items: baseline; font-size: 14px; }
/* "Forget this device": the only way off a cookie that renews itself, and
   deliberately quiet — it is an exit, not an action the game is played with. */
.forget { margin: 26px 0 8px; text-align: center; }
.forget button {
  background: none; border: 0; padding: 4px; font: inherit; font-size: 13px;
  color: var(--dim); text-decoration: underline; cursor: pointer;
}
.forget .why { display: block; color: var(--dim); font-size: 12px; margin-top: 2px; }
.lede { color: var(--dim); margin: 14px 16px 18px; max-width: 62ch; }
.note { color: var(--dim); max-width: 48ch; margin: 10px auto; }
.muted { color: var(--dim); }
.center { max-width: 520px; margin: 12vh auto; padding: 0 20px; text-align: center; }

/* card grid — same column counts as the printed sheet, at most four across */
.grid {
  display: grid; gap: 26px 22px; padding: 0 16px;
  grid-template-columns: 1fr; max-width: 1120px; margin: 0 auto;
}
/* `span 2` belongs to the multi-column layouts and nowhere else. A second copy
   of it lived in the phone block at the bottom of this file, where the grid is
   one column, and a spanning item in a one-column grid invents an implicit
   second column: the tracks resolved to `2px 346px` on a 402px phone, and grid
   auto-placement put every other Card in the 2px one. Nineteen of the 45 Cards
   were a hairline at the left edge — the tally still said 45, because it counts
   rows from the database and not boxes on the screen. Two Residents reported it
   on the first day of play, 2026-09-14; four more had quietly guessed only the
   26 Cards they could see. If a wide Card ever needs phone-specific treatment,
   it is the `aspect-ratio` below that takes it, never `grid-column`. */
@media (min-width: 560px) {
  .grid { grid-template-columns: repeat(2, 1fr); }
  .card.wide { grid-column: span 2; }
}
@media (min-width: 900px) { .grid { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1120px) { .grid { grid-template-columns: repeat(4, 1fr); } }

.card {
  margin: 0; background: var(--card); border: 1px solid var(--line);
  border-radius: 12px; overflow: hidden; position: relative; cursor: pointer;
}
.card:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* A mount, like a photo in a mat: the frame is a fixed shape, the photo sits
   inside it at its own. Landscape pictures are never cropped into a portrait
   slot — they letterbox against the mount instead. */
.card .photo {
  aspect-ratio: 3 / 4; background: var(--mount);
  display: flex; align-items: center; justify-content: center; overflow: hidden;
}
.card.wide .photo { aspect-ratio: 5 / 3; }
@media (max-width: 559px) { .card.wide .photo { aspect-ratio: 4 / 3; } }
.card .photo img { max-width: 100%; max-height: 100%; width: auto; height: auto; display: block; }
.num {
  position: absolute; top: 8px; left: 8px; background: #0009; color: #fff;
  border-radius: 999px; padding: 1px 9px; font-size: 12px;
  font-variant-numeric: tabular-nums;
}
.card figcaption {
  padding: 8px 10px; font-size: 13px; border-top: 1px solid var(--line);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.card figcaption em { color: var(--dim); }
.card.done { border-color: var(--accent); }
.card.done figcaption { color: var(--accent); font-weight: 600; }

.foot { display: flex; gap: 18px; padding: 22px 16px; font-size: 14px; }

/* picker ---------------------------------------------------------------------
   The photo is the sheet, and the names are drawn over it. Typing comes first:
   the field and the list of eligible names hold their ground, and the photo
   gets whatever is left.

   Two things this went through before it worked on a phone.

   The column it started as — photo, then filter, then list — sized the photo at
   46vh and the sheet at 90vh. On iOS `vh` is the LARGE viewport, so with the
   keyboard up the photo held 46% of the whole screen and 65 neighbours were
   listed one row at a time.

   `dvh` did not fix it. `dvh` tracks the browser's own disappearing toolbars,
   NOT the software keyboard: iOS never resizes the layout viewport for the
   keyboard, so a sheet measured in `dvh` still ran down behind it. Only
   `visualViewport` knows where the keyboard is, and `app.js` publishes it here
   as `--vv-h` / `--vv-top`. The `dvh` fallbacks are for the browser that has no
   visualViewport at all, which is none that we know of and every one we don't.

   Everything else follows from that. The dialog is positioned against the space
   that is actually visible, and the panel holds a fixed share of it rather than
   shrinking to its contents — a panel that shrank as the filter narrowed walked
   the field and the last matching name straight down behind the keyboard. */
dialog {
  position: fixed; margin: 0; left: 50%; transform: translateX(-50%);
  top: calc(var(--vv-top, 0px) + 8px);
  /* The user agent's own dialog rule sets `inset: 0`, and a box with both `top`
     and `bottom` set stretches to fill whatever `height: auto` would otherwise
     have shrunk to. Releasing the bottom is what lets it be as tall as its
     photo. */
  bottom: auto;
  border: none; border-radius: 16px; padding: 0; width: min(440px, 94vw);
  background: var(--panel); color: var(--fg); overflow: hidden;
  /* As tall as the photo, floored at what the panel needs and capped at what is
     visible. Pinning it to the full height instead left a band of empty ground
     under a short photo whenever the keyboard was down — which on a desktop is
     always. The floor gives way to the cap on a phone with the keyboard up,
     where what is visible is less than the floor. */
  height: auto;
  min-height: min(calc(var(--vv-h, 92dvh) - 16px), 420px);
  max-height: calc(var(--vv-h, 92dvh) - 16px);
}
/* `display` is set only while it is open: an author `display` on a `dialog`
   overrides the user agent's `display: none` and the closed dialog would show. */
dialog[open] { display: flex; flex-direction: column; }
/* Dismissed, there is no panel to make room for, so the sheet closes down onto
   the photo rather than leaving a band of empty ground under it. */
dialog:not(.naming) { min-height: 0; }
dialog::backdrop { background: #000b; }
.sheet { position: relative; display: flex; overflow: hidden; flex: 1 1 auto; min-height: 0; width: 100%; }

/* Whole photo, never cropped, never stretched. Top-aligned, not centred: the
   panel covers the foot of the sheet, so a centred photo puts its subject
   behind the names and leaves empty ground in the strip that is visible. */
.shot {
  flex: 1 1 auto; min-height: 0;
  display: flex; align-items: flex-start; justify-content: center;
  background: #0002; overflow: hidden; cursor: pointer;
}
/* The cap is on the photo rather than on its frame: dismissed, the frame takes
   its height from the photo, so `max-height: 100%` would have nothing to
   resolve against and a tall Card would run off the screen. */
.shot img {
  max-width: 100%; max-height: calc(var(--vv-h, 92dvh) - 16px);
  width: auto; height: auto; display: block;
}

/* The panel: the filter and the names, over the foot of the photo, translucent
   enough to read the photo through.

   Its height is a fixed share of what is visible, NOT its content. The field
   therefore sits in the same place from the first keystroke to the last, well
   clear of the keyboard and of the autofill and dictation overlays iOS floats
   above it — and the last matching name stays where the list was, instead of
   following the shrinking panel down behind the keyboard. */
.panel {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;
  display: flex; flex-direction: column; height: min(72%, 420px);
  transition: transform 0.18s ease;
  /* The box is a fixed share; only the parts with something in them are
     painted, so a filter narrowed to two names shows the photo below them
     instead of a slab of empty ground. The transparent remainder must not
     swallow the tap that dismisses the panel. */
  pointer-events: none; background: none;
}
.panel > * { pointer-events: auto; }
.panel .find, .panel ul {
  background: color-mix(in srgb, var(--panel) 82%, transparent);
  backdrop-filter: blur(16px) saturate(1.4);
  -webkit-backdrop-filter: blur(16px) saturate(1.4);
}
.panel .find { border-top: 1px solid color-mix(in srgb, var(--fg) 18%, transparent); }
/* No blur to hide behind: fall back to opaque rather than to unreadable names
   over a photograph. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .panel .find, .panel ul { background: var(--panel); }
}
/* Dismissed: out of the way, out of the tab order, and off the screen reader.
   `visibility` is what takes it out of both; `transform` alone would leave a
   focusable filter sitting under the photo. */
dialog:not(.naming) .panel { transform: translateY(100%); visibility: hidden; }

.sheet .find { padding: 12px 12px 8px; flex: 0 0 auto; }
/* Not an <input>. Chrome offers saved form history and Safari offers Contacts
   over any text field, covering the name list underneath. Nothing autofills a
   contenteditable. */
.qbox {
  display: block; width: 100%; padding: 11px 13px; border-radius: 10px; font: inherit;
  border: 1px solid var(--line); background: var(--bg); color: var(--fg);
  white-space: nowrap; overflow-x: auto; outline: none; min-height: 1.45em; cursor: text;
}
.qbox:focus { border-color: var(--accent); }
.qbox:empty::before { content: attr(data-ph); color: var(--dim); }
/* `0 1 auto`, so the list is as tall as the names it is showing and no taller,
   and shrinks inside the panel when there are more than fit. The field above it
   does not move either way — that is the panel's fixed height's job. */
.sheet ul {
  list-style: none; margin: 0; padding: 0 6px 6px; overflow: auto; flex: 0 1 auto;
  min-height: 0; overscroll-behavior: contain;
}
.sheet li { padding: 12px; border-radius: 9px; cursor: pointer; }
.sheet li:hover, .sheet li.on { background: var(--accent); color: var(--accent-fg); }
/* The only name left: armed for Enter. An outline, not a fill, so a row that
   is both the sole match and the current guess reads as both. */
.sheet li.sole { outline: 2px solid var(--accent); outline-offset: -2px; }
/* Already the current guess, so the fill is accent and an accent outline would
   vanish into it. Draw the ring in the fill's own foreground instead. */
.sheet li.sole.on, .sheet li.sole:hover { outline-color: var(--accent-fg); }
.sheet li .key {
  float: right; font-size: 13px; opacity: 0.75; padding-left: 10px;
  font-variant-numeric: tabular-nums;
}
/* No Enter key on a touch keyboard to point at. The armed outline stays. */
@media (hover: none) { .sheet li .key { display: none; } }
.sheet li.none { color: var(--dim); text-align: center; cursor: default; }
.sheet li.none:hover { background: none; color: var(--dim); }

/* The way back to the panel once the photo has been tapped clear of it. Tapping
   the photo again does the same thing; this is the part that says so. */
.rename {
  position: absolute; left: 50%; bottom: 16px; transform: translateX(-50%); z-index: 2;
  padding: 11px 20px; border: none; border-radius: 999px;
  background: var(--accent); color: var(--accent-fg); font-weight: 600;
  box-shadow: 0 2px 12px #0007;
}
dialog.naming .rename { display: none; }

.close {
  position: absolute; top: 8px; right: 10px; z-index: 3; background: #0009; color: #fff;
  border: none; border-radius: 999px; width: 32px; height: 32px; font-size: 18px;
}

/* progress */
.progress { list-style: none; margin: 0; padding: 0 16px; }
.progress li {
  display: flex; align-items: center; gap: 10px;
  padding: 9px 0; border-bottom: 1px solid var(--line);
}
.progress .nm { flex: 0 0 auto; min-width: 9em; }
.bar-wrap { flex: 1; height: 8px; background: var(--line); border-radius: 999px; }
.bar-fill { display: block; height: 100%; background: var(--accent); border-radius: 999px; }
.n { font-variant-numeric: tabular-nums; color: var(--dim); margin-left: auto; }

/* admin */
.actions { display: flex; gap: 10px; align-items: center; padding: 4px 16px 14px; flex-wrap: wrap; }

/* Cohort blocks: the message you are about to send, above the button that
   sends it. Layout follows prototype-admin-results.html — one centred column
   of blocks, now that the funnel and the 65-row table are gone. */
.wrap { max-width: 1000px; margin: 0 auto; padding: 18px 20px 40px; }
.wrap .lede { margin: 0 0 22px; max-width: 62ch; }
.wrap .warn { margin: 0 0 16px; }
.blocks { padding: 0 0 6px; }
.block {
  background: var(--card); border: 1px solid var(--line); border-radius: 12px;
  padding: 18px 18px 16px; margin: 0 0 16px;
}
.block-head { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
.block-head h2 { margin: 0 0 4px; font-size: 21px; }
.count {
  font-weight: 600; font-size: 15px; color: var(--accent); background: var(--mount);
  border-radius: 999px; padding: 2px 11px; white-space: nowrap;
}
.block .why { color: var(--dim); font-size: 14px; margin: 4px 0 14px; max-width: 62ch; }
.field-label {
  display: flex; justify-content: space-between; align-items: baseline; gap: 12px;
  flex-wrap: wrap; font-size: 13px; color: var(--dim); margin: 0 0 5px;
}
.tokens code { background: var(--mount); border-radius: 4px; padding: 1px 5px; font-size: 12px; }
.block textarea {
  width: 100%; min-height: 150px; resize: vertical; display: block;
  font: 14px/1.55 ui-monospace, SFMono-Regular, Menlo, monospace;
  color: var(--fg); background: var(--mount);
  border: 1px solid var(--line); border-radius: 8px; padding: 11px 12px;
}
.block textarea:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
.preview-cap { font-size: 13px; color: var(--dim); margin: 14px 0 5px; }
.preview {
  background: var(--mount); border: 1px dashed var(--line); border-radius: 8px;
  padding: 12px 13px; margin: 0;
  font: 13px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace;
  white-space: pre-wrap; word-break: break-word;
}
.block .warn {
  color: var(--bad); font-size: 13px; font-weight: 700;
  margin: 10px 0 0; padding: 0; border: 0; background: none;
}
.block .actions { padding: 14px 0 0; }
.block .actions button {
  background: var(--accent); color: var(--accent-fg); border: 0; border-radius: 8px;
  padding: 9px 16px; font-weight: 500;
}
.block .actions button[disabled] { opacity: .4; cursor: not-allowed; }

/* The Reveal: two acts in one block, in order. The figure the decision hangs
   on sits above the button that ends the game, and the button is the only
   dangerous thing on the page — hence the border the block wears. */
.block.reveal { border: 2px solid var(--accent); }
.gate { font-size: 17px; font-weight: 600; margin: 2px 0 12px; }
.gate b { color: var(--accent); }
.step { display: flex; gap: 11px; align-items: flex-start; padding: 9px 0; }
/* Not the absolutely-positioned badge a Card wears: this one sits in the row. */
.step .num {
  position: static; flex: 0 0 24px; height: 24px; padding: 0;
  border-radius: 999px; background: var(--mount); color: var(--accent);
  font-size: 13px; font-weight: 600; display: grid; place-items: center;
}
.step.done .num { background: var(--good); color: var(--card); }
.step h3 { margin: 2px 0 4px; font-size: 16px; font-weight: 650; }
/* One short sentence pair, indented past the step number: it reads as one line
   rather than wrapping at the 62ch measure the Cohort blocks are set to. */
.step .why { margin: 2px 0 8px; max-width: none; }
/* The second act holds a whole message, so it takes the rest of the row. */
.step .grows { flex: 1; min-width: 0; }
.step form { margin: 0; }
/* The page's one dangerous button. `--bg` rather than white for the label:
   in dark mode `--bad` is a light salmon, and white on it is barely there. */
.step .danger {
  background: var(--bad); color: var(--bg); border: 0; border-radius: 8px;
  padding: 9px 16px; font-weight: 500;
}
.small { font-size: 13px; }

.warn {
  margin: 14px 16px; padding: 10px 12px; border-radius: 9px;
  border: 1px solid var(--bad); background: #a8362f22;
}

/* The names inside a block: closed until asked for, so a Cohort can be read
   before it is mailed without burying the button that mails it. */
.block > details.who { margin: 14px 0 0; }
summary.disclose {
  width: fit-content; cursor: pointer; color: var(--accent);
  font-size: 14px; font-weight: 500;
  text-decoration: underline; text-underline-offset: 3px;
}
summary.disclose::marker { color: var(--dim); }
.names { margin: 12px 0 0; border-top: 1px solid var(--line); max-height: 270px; overflow: auto; }
.name {
  display: flex; align-items: center; gap: 10px; padding: 7px 2px;
  border-bottom: 1px solid var(--line); font-size: 14px;
}
.name .nm { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.name .meta { color: var(--dim); font-size: 13px; white-space: nowrap; }
.name form { margin: 0; }
.block .none { color: var(--dim); font-size: 14px; margin: 14px 0 0; }
button.tiny {
  font-size: 13px; padding: 4px 9px; border: 0; border-radius: 7px;
  background: var(--accent); color: var(--accent-fg);
}
button.ghost { background: transparent; color: var(--accent); border: 1px solid var(--line); }

@media (max-width: 620px) {
  /* A row of name, address and two buttons does not fit a phone in one line. */
  .name { flex-wrap: wrap; }
  .name .nm { flex: 1 0 100%; }
  .name .meta { flex: 1 1 auto; overflow: hidden; text-overflow: ellipsis; }
}

/* ---------- the Results: five pages behind one pill row ----------
   Layout follows prototype-admin-results.html. The prototype's webfonts are
   not ported, here or on the Admin page: the app is set in the system stack
   throughout, and one page in Fraunces would read as a different site. */
.rhead { border-bottom: 1px solid var(--line); padding-bottom: 18px; margin: 0 0 4px; }
.eyebrow {
  font-size: 15px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--accent); margin: 0 0 8px;
}
.rhead h1 { margin: 0; font-size: clamp(28px, 5vw, 44px); line-height: 1.05; }
.rhead .lede { margin: 10px 0 0; }

/* The row that moves between the five pages. On every page, and it wraps to
   two lines on a phone rather than scrolling sideways. */
.pagenav { display: flex; flex-wrap: wrap; gap: 6px; margin: 16px 0 26px; }
.pagenav a {
  text-decoration: none; color: var(--accent); font-size: 14px;
  border: 1px solid var(--line); border-radius: 999px; padding: 7px 14px;
}
.pagenav a.on { background: var(--accent); color: var(--accent-fg); border-color: var(--accent); }

/* The reader's own score: the one number on the front door, and the only one
   on it that is about the reader rather than about somebody's face. */
.youline {
  background: var(--card); border: 1px solid var(--line); border-radius: 10px;
  padding: 12px 16px; margin: 0 0 20px;
  display: flex; gap: 12px; align-items: baseline; flex-wrap: wrap;
}
.youline b { font-size: 20px; font-weight: 650; }
.linky { font-size: 15px; text-underline-offset: 3px; }

/* The gallery: every Card, named. Four across, two on a phone. */
.gallery { display: grid; gap: 16px; grid-template-columns: repeat(4, 1fr); }
@media (max-width: 820px) { .gallery { grid-template-columns: repeat(2, 1fr); } }
.gallery figure {
  margin: 0; background: var(--card); border: 1px solid var(--line);
  border-radius: 10px; padding: 9px 9px 10px;
}
.gallery figure.wide { grid-column: span 2; }
/* Flex, not grid. With `display:grid; place-items:center` the image's
   `max-height:100%` does not resolve against the aspect-ratio box, so a
   landscape photo in the 5:3 frame overflows and is silently cropped. */
.mount {
  background: var(--mount); border-radius: 6px; aspect-ratio: 3 / 4;
  display: flex; align-items: center; justify-content: center; overflow: hidden;
}
.gallery figure.wide .mount { aspect-ratio: 5 / 3; }
.mount img {
  max-width: 100%; max-height: 100%; width: auto; height: auto;
  display: block; border-radius: 3px;
}
.gallery figcaption { margin: 8px 2px 0; }
.gallery .who { display: block; font-size: 15px; font-weight: 650; line-height: 1.2; }
/* Not the badge a play Card wears: here the position sits in the caption, out
   of the way of the name, which is what the page is for. */
.gallery .num {
  position: static; float: right; background: none; color: var(--dim);
  font-size: 12px; padding: 0;
}

/* The foot of the front door: what each onward page is, in a sentence.
   "Hardest to spot" means nothing until you have read one. */
.hub {
  display: grid; gap: 12px; margin: 32px 0 0;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
}
.hubcard {
  display: block; text-decoration: none; color: var(--fg); background: var(--card);
  border: 1px solid var(--line); border-radius: 12px; padding: 15px 16px;
}
.hubcard:hover { border-color: var(--accent); }
.hubcard .hl {
  display: block; font-size: 17px; font-weight: 650; line-height: 1.2;
  color: var(--accent); margin: 0 0 5px;
}
.hubcard .hb { display: block; font-size: 14px; color: var(--dim); }

.gated {
  background: var(--card); border: 1px solid var(--line); border-radius: 12px;
  padding: 26px; text-align: center;
}
.gated .lede { margin: 0 auto 18px; max-width: 46ch; }
.gated h2 { margin: 0 0 8px; }

/* ---------- a row of name, marker, bar and number ----------
   Two lists are this shape: the spread below, and the ranking further down.
   The shared part is declared once and above both, so that neither can drift
   from the other and each can still override the part that is its own.

   The name takes the slack and gives it back on a phone: an ellipsis, never a
   second line that breaks the row's rhythm. The ellipsis is on the name alone
   — the marker beside it never shrinks, because on a 400px screen it is the
   first thing a single nowrap span would eat, and on the ranking it is the
   reason that row is on the page at all. */
ol.board .nm, ol.spreadrows .nm, ol.ranked .nm {
  min-width: 0; display: flex; align-items: baseline; gap: 6px;
}
ol.board .nm > span, ol.spreadrows .nm > span, ol.ranked .nm > span {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* "— you", "— right family", "— that's who it is": quiet and upright, a marker
   rather than an emphasis, and never shrunk or trimmed — it must not read as
   louder than the name it follows, and it must not disappear before it. */
ol.board em, ol.spreadrows em {
  flex: 0 0 auto; font-style: normal; font-size: 13px; color: var(--dim);
}

/* ---------- "What people guessed": the rail, and one Card's spread ----------
   One Card at a time, because the page is about dwelling on one: the photo
   large, every name offered for it beside the photo, and a rail to move on. */

/* The rail is an index rather than a gallery, so the chips are square and
   cropped. AGENTS.md names this as the one deliberate exception to the
   never-crop rule: at 52px there is no photograph left to protect. */
.rail {
  display: flex; gap: 7px; overflow-x: auto; padding: 0 0 10px;
  margin: 0 0 22px; border-bottom: 1px solid var(--line);
}
.rail a {
  flex: 0 0 auto; line-height: 0; opacity: 0.55;
  border: 2px solid transparent; border-radius: 5px;
}
.rail a.on, .rail a:hover, .rail a:focus-visible { opacity: 1; }
.rail a.on { border-color: var(--accent); }
.rail img {
  width: 52px; height: 52px; display: block; border-radius: 3px;
  object-fit: cover; object-position: center;
}

/* The Card and its names side by side, and stacked on a phone. The photo column
   is capped rather than proportional: past 340px the names stop being readable
   against it. */
.spread {
  display: grid; gap: 24px; align-items: start;
  grid-template-columns: minmax(0, 340px) 1fr;
}
@media (max-width: 760px) { .spread { grid-template-columns: 1fr; } }

.focus {
  margin: 0; background: var(--card); border: 1px solid var(--line);
  border-radius: 12px; padding: 12px 12px 14px;
}
.focus.wide .mount { aspect-ratio: 5 / 3; }
.focus figcaption { margin: 10px 3px 0; }
/* Not the badge a play Card wears, and the reset has to be explicit: the bare
   `.num` is an absolutely positioned pill over a photo, so a caption that only
   restyles the type leaves the label floating in the corner of the page. */
.focus .num {
  position: static; background: none; padding: 0;
  font-size: 12px; color: var(--dim);
  letter-spacing: 0.1em; text-transform: uppercase;
}
.focus .who { display: block; font-size: 21px; font-weight: 650; line-height: 1.15; }
/* How many got it — counts, never a rate. "2 of 11" and "8 of 44" are
   different claims, and a percentage hides which one this is. The prototype
   calls this `.pc`, which is the one name it must not have. */
.focus .got { display: block; font-size: 14px; color: var(--dim); margin: 5px 0 0; }

.spreadlist h3 { margin: 0 0 10px; }
ol.spreadrows { list-style: none; margin: 0 0 12px; padding: 0; }
ol.spreadrows li {
  display: flex; align-items: center; gap: 10px;
  padding: 6px 0; border-bottom: 1px solid var(--line);
}
/* The name column is a ranking row's, declared once for both lists above, so
   the two cannot drift apart. What differs here is the width: fixed rather than
   elastic, so every bar starts at the same place — and 46% rather than the
   prototype's 42%, because with the marker held back from the ellipsis 42%
   clips a long name that the prototype fits by clipping the marker instead. */
ol.spreadrows .nm { flex: 0 0 46%; }
ol.spreadrows em { font-size: 12.5px; }
ol.spreadrows li.right { font-weight: 700; }
ol.spreadrows li.right em { color: var(--good); font-weight: 700; }
ol.spreadrows em.fam { color: var(--near); }
ol.spreadrows .n {
  min-width: 2em; text-align: right; font-variant-numeric: tabular-nums;
}
/* The bar is against the most-offered name on this Card, not against the
   number of Residents: the page is the shape of one Card's confusion. */
.barw {
  flex: 1; min-width: 40px; height: 8px; overflow: hidden;
  background: var(--mount); border-radius: 99px;
}
.barf { display: block; height: 100%; background: var(--accent); border-radius: 99px; }
li.right .barf { background: var(--good); }

/* On a phone the fixed column is what breaks: 46% of 368px is not a name, and
   the rows read "Er…" and "Taylor …". The names are the whole content of this
   page, so there they wrap instead of being trimmed, the marker drops below a
   name too long to sit beside, and the bar gives up its share of the width.
   Only a long name costs a second line; the rest of the list keeps its rhythm. */
@media (max-width: 620px) {
  ol.spreadrows .nm { flex: 1 1 auto; flex-wrap: wrap; }
  ol.spreadrows .nm > span { white-space: normal; overflow: visible; }
  ol.spreadrows .barw { flex: 0 0 70px; }
}

/* The names only one person offered, folded away. Both labels are in the
   markup and CSS shows one, so the wording stays the server's and the
   disclosure needs no script — no Results page loads any. */
/* No triangle: the prototype's disclosure is a plain underlined "Show the 12
   names offered once", and the words carry the affordance on their own. The
   Admin page keeps its marker — that list is one of several inside a block and
   the marker is what sets it apart from them. */
details.singles summary { list-style: none; }
details.singles summary::-webkit-details-marker { display: none; }
details.singles .hide { display: none; }
details.singles[open] .show { display: none; }
details.singles[open] .hide { display: inline; }

/* ---------- "How you did": the score, then Card by Card ----------
   The grid is the page. A score is read once and remembered; what gets read out
   to a neighbour is "I said you were Carol". */
.score {
  font-size: clamp(34px, 7vw, 58px); font-weight: 700; line-height: 1;
  letter-spacing: -0.03em; margin: 0 0 6px;
}
.score small {
  font-size: 17px; font-weight: 400; color: var(--dim); letter-spacing: 0;
}

/* A heading in the flow of a Results page rather than in the app's chrome: the
   page already has its gutter, so the heading does not carry one of its own. */
.wrap > h2 { margin: 26px 0 8px; font-size: clamp(19px, 2.6vw, 25px); }

.named {
  display: grid; gap: 10px; margin: 16px 0 0;
  grid-template-columns: repeat(auto-fill, minmax(132px, 1fr));
}
.named figure { margin: 0; position: relative; }
.named figcaption { font-size: 12.5px; line-height: 1.35; margin: 5px 0 0; }
/* Who it really was, in the colour the whole app uses for a right answer — on
   the ones they missed as much as on the ones they got, because on both it is
   the answer rather than their Guess. What they said goes under it. */
.named .was { display: block; font-weight: 700; color: var(--good); }
.named .said { color: var(--dim); }

/* The mark: a badge on the corner of the photo, reinforcing the caption rather
   than carrying the meaning on its own — "you got it" and "you said Carol
   Baker" are the words, and a reader who cannot tell green from red reads
   those. A Card with no Subject is neither, and gets no badge. */
.named .right::after, .named .wrong::after {
  position: absolute; top: 6px; right: 6px; width: 22px; height: 22px;
  display: flex; align-items: center; justify-content: center;
  border-radius: 50%; font-size: 12px; font-weight: 700; line-height: 1;
  color: var(--bg); box-shadow: 0 1px 3px rgb(0 0 0 / 0.35);
}
.named .right::after { content: "\2713"; background: var(--good); }
.named .wrong::after { content: "\2717"; background: var(--bad); }

/* ---------- "Who knew the most": the ten, and the reader's own row ----------
   A celebration, not a list with a bottom. The reader's own row is bordered in
   the accent rather than merely tinted, because below the ten it stands alone
   with no neighbours to be read against. */
ol.board {
  list-style: none; margin: 0; padding: 0; background: var(--card);
  border: 1px solid var(--line); border-radius: 12px; overflow: hidden;
}
ol.board li {
  display: flex; align-items: center; gap: 12px;
  padding: 9px 15px; border-bottom: 1px solid var(--line);
}
ol.board li:last-child { border-bottom: 0; }
ol.board li.me { background: var(--mount); }
ol.board.yours { margin-top: 12px; border: 2px solid var(--accent); }

.rank {
  flex: 0 0 30px; text-align: right; font-weight: 650; color: var(--dim);
  font-variant-numeric: tabular-nums;
}
/* The ranking's name column is elastic; the spread's is fixed. That is the
   whole of what the two lists do differently with a row. */
ol.board .nm { flex: 1; }
/* How much of the game they played, quietly: it explains the row of a Resident
   who named only part of it, and it is not a second number to read the ranking
   by. */
ol.board .of { color: var(--dim); font-size: 13px; white-space: nowrap; }
ol.board .sc {
  min-width: 2.2em; text-align: right; font-size: 17px; font-weight: 650;
  font-variant-numeric: tabular-nums;
}

/* Everyone the page did not list, as a number rather than as names. */
.rest { margin: 14px 0 0; }

/* ---------- "Hardest to spot": every Card, least recognised first ----------
   A ranking, not the gallery again: the photo is a 40px square down the side of
   an order, the bar is the page's shape, and the two numbers say what it means.
   Denser than the ten above — forty-four rows, read down rather than dwelt on. */
ol.ranked {
  list-style: none; margin: 0; padding: 0; background: var(--card);
  border: 1px solid var(--line); border-radius: 12px; overflow: hidden;
}
ol.ranked li {
  display: flex; align-items: center; gap: 11px;
  padding: 7px 14px; border-bottom: 1px solid var(--line);
}
ol.ranked li:last-child { border-bottom: 0; }
/* Square, and therefore cropped — the rail's exception, for the rail's reason:
   at 40px this is an index entry rather than a photograph, and a row of
   letterboxed babies would read as the gallery. See AGENTS.md. */
ol.ranked .rk { flex: 0 0 40px; line-height: 0; }
ol.ranked .rk img {
  width: 40px; height: 40px; display: block; border-radius: 4px;
  object-fit: cover; object-position: center;
}
/* Fixed, like the spread's and unlike the ranking of people: every bar starts
   at the same place, which is what makes forty-four rows readable as one shape. */
ol.ranked .nm { flex: 0 0 32%; }
/* What the order is by, and what it is out of. The percentage is the loud one;
   the counts are there so that 67% of twelve cannot be read as 67% of forty-four. */
ol.ranked .pc {
  min-width: 3em; text-align: right; font-weight: 650;
  font-variant-numeric: tabular-nums;
}
ol.ranked .of {
  min-width: 3.6em; text-align: right; font-size: 13px; color: var(--dim);
  font-variant-numeric: tabular-nums;
}
/* A Card too few people saw to hold a title, in the third colour — the same
   amber that marks the right household on the spread, and for the same reason:
   neither right nor wrong, but a different kind of claim. The note under the
   list says so in words. */
ol.ranked .of.thin { color: var(--near); font-weight: 650; }
.thinnote { margin: 14px 0 0; max-width: 62ch; }

/* On a phone the name takes the slack and the bar goes: the percentage and the
   counts are the row, and a 40px bar is decoration. */
@media (max-width: 560px) {
  ol.ranked .nm { flex: 1; }
  ol.ranked .barw { display: none; }
}

/* The two statistics under the ranking: one sentence each, read as a remark
   rather than as a row of a table. */
.remark {
  background: var(--mount); border: 1px solid var(--line); border-radius: 12px;
  padding: 20px; margin: 22px 0 0; text-align: center;
}
.remark .big {
  font-size: clamp(20px, 3.4vw, 28px); font-weight: 650; line-height: 1.25;
  letter-spacing: -0.02em; margin: 0 0 6px;
}
.remark .muted { margin: 0; }

/* Rehearsal and Live: the state, said out loud, in both states. A loud
   Rehearsal and a quiet Live — if silence meant Live, the dangerous state
   would be the one with no marking on it. */
.mode {
  margin: 0 0 16px; padding: 11px 13px; border-radius: 9px;
  max-width: 62ch; font-size: 14px; line-height: 1.5;
}
.mode.rehearsal { border: 2px solid var(--near); background: #a76c2518; }
.mode.live { border: 1px solid var(--line); background: var(--card); color: var(--dim); }
.mode.live strong { color: var(--fg); }
.mode [data-test="admin-identity"] { font-weight: 650; white-space: nowrap; }

/* The way out of a Rehearsal: the same steps the Reveal is built from, and the
   same danger colour on the two that destroy something. */
.block.rehearsal { border: 2px dashed var(--near); }
.block.rehearsal .danger {
  background: var(--bad); color: var(--bg); border: 0; border-radius: 8px;
  padding: 9px 16px; font-weight: 500;
}
.block.rehearsal button { background: var(--mount); color: var(--fg); border: 1px solid var(--line);
  border-radius: 8px; padding: 9px 16px; font-weight: 500; }
.block.rehearsal input {
  font: inherit; width: 12em; padding: 8px 10px; border-radius: 8px;
  border: 1px solid var(--line); background: var(--card); color: var(--fg);
}
.block.rehearsal .field-label { display: block; margin: 6px 0 4px; }

/* The strip a player-facing page wears during the Rehearsal, and loses on
   Going Live. Full width and above everything, so it cannot be scrolled past
   without being read. */
.strip {
  margin: 0; padding: 9px 16px; font-size: 14px;
  background: var(--near); color: var(--bg);
}
.strip strong { font-weight: 700; }

/* The Admin's own strip: the same band, with the way through to /admin at the
   end of it. It replaces the Rehearsal strip rather than stacking on it. */
.strip.admin { display: flex; flex-wrap: wrap; gap: 0 8px; align-items: baseline; }
.strip.admin a { color: var(--bg); text-decoration: underline; margin-left: auto; }
.strip.admin .stop { display: inline; margin: 0; }
.strip.admin .stop button {
  background: none; border: 0; padding: 0; font: inherit; color: var(--bg);
  text-decoration: underline; cursor: pointer;
}

/* ---- engagement ---------------------------------------------------------- */

/* Screen-reader-only: a column that is a bar rather than a word still needs a
   header, or the table has a heading row with a hole in it. */
.vh {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip-path: inset(50%); white-space: nowrap;
}

.bad { color: var(--bad); font-weight: 650; }

/* The funnel: the note under each label is what the count is true of, and it
   carries as much as the number does. */
ol.spreadrows.funnel .nm { flex: 0 0 38%; flex-wrap: wrap; }
ol.spreadrows.funnel .nm > span { flex: 1 0 100%; font-weight: 650; }
ol.spreadrows.funnel em { flex: 1 0 100%; color: var(--dim); font-style: normal; }
ol.spreadrows.funnel .lost {
  min-width: 3.4em; text-align: right; color: var(--dim);
  font-size: 13px; font-variant-numeric: tabular-nums;
}

/* Seven columns of numbers do not fold, so on a phone the table scrolls inside
   its own box rather than taking the page with it. */
/* `position: relative` is load-bearing: the hidden column header inside is
   absolutely positioned, and without a positioned ancestor it escapes this box
   and widens the whole document on a phone. */
.scroller { position: relative; overflow-x: auto; }
table.days {
  width: 100%; border-collapse: collapse; font-size: 14px;
  font-variant-numeric: tabular-nums;
}
table.days th, table.days td { padding: 6px 10px; text-align: right; white-space: nowrap; }
table.days thead th {
  font-size: 12px; font-weight: 600; color: var(--dim); text-align: right;
  border-bottom: 1px solid var(--line);
}
table.days th[scope="row"] { text-align: left; font-weight: 600; }
table.days thead th:first-child { text-align: left; }
table.days tbody tr { border-bottom: 1px solid var(--line); }
table.days tbody tr:last-child { border-bottom: 0; }
table.days td.spark { width: 34%; min-width: 90px; }
/* `.barw` is a flex item everywhere else; in a table cell it is an inline span,
   which ignores its height and draws nothing. */
table.days td.spark .barw { display: block; width: 100%; }

/* Twenty-four columns, all of them present. The empty hours are the half of
   this that says when not to send. */
.hours { display: flex; align-items: flex-end; gap: 3px; height: 120px; margin: 4px 0 0; }
.hours .hr { flex: 1; display: flex; flex-direction: column; justify-content: flex-end; height: 100%; }
.hours .hb {
  display: block; width: 100%; background: var(--accent);
  border-radius: 3px 3px 0 0; min-height: 0;
}
.hours em {
  display: block; font-style: normal; font-size: 10px; color: var(--dim);
  text-align: center; height: 13px; line-height: 13px; white-space: nowrap;
}

ol.spreadrows.delivery .nm { flex: 0 0 26%; }
ol.spreadrows.delivery .n { margin-left: 0; min-width: 11em; text-align: left; }
ol.spreadrows.delivery .meta { margin-left: auto; color: var(--dim); font-size: 13px; }

ol.ranked.pull .nm { flex: 0 0 4em; color: var(--dim); }

ol.trail {
  list-style: none; margin: 0; padding: 0; font-size: 14px;
  max-height: 420px; overflow: auto;
}
ol.trail li {
  display: flex; align-items: baseline; gap: 10px;
  padding: 5px 0; border-bottom: 1px solid var(--line);
}
ol.trail li:last-child { border-bottom: 0; }
ol.trail .when {
  flex: 0 0 8.4em; color: var(--dim); font-size: 13px;
  font-variant-numeric: tabular-nums;
}
ol.trail .nm { flex: 0 0 11em; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
ol.trail em { flex: 1; font-style: normal; color: var(--dim); }
ol.trail .meta { color: var(--dim); font-size: 12px; white-space: nowrap; }

@media (max-width: 560px) {
  ol.spreadrows.funnel .nm { flex: 1 1 100%; }
  ol.trail li { flex-wrap: wrap; }
  ol.trail .when { flex: 0 0 7.2em; }
  ol.trail .nm { flex: 1 1 auto; }
  /* The delivery row is a name, a count and a date, and three of those do not
     fit across a phone. It folds rather than pushing the page sideways. */
  ol.spreadrows.delivery li { flex-wrap: wrap; }
  ol.spreadrows.delivery .nm { flex: 0 0 100%; }
  ol.spreadrows.delivery .n { min-width: 0; }
  /* The ranking hides its bar on a phone because its rows carry a percentage
     and two counts without it. These rows carry one number, and the bar is the
     only thing that makes forty-five of them a shape rather than a column. */
  ol.ranked.pull .nm { flex: 0 0 3.5em; }
  ol.ranked.pull .barw { display: block; flex: 1; }
}
