/* Document-level defaults. These began as the WASM spike's styling and are what
   survived it: the spike's own selectors (.sub, .checks, .ok, pre, h1) went with
   the page, and what is left is either used by the real client or is the frame
   the board is drawn in. */
:root { color-scheme: light dark; }

body {
    margin: 0;
    padding: 1.5rem;
    font: 15px/1.5 ui-sans-serif, system-ui, -apple-system, sans-serif;
    max-width: 60rem;
}

h2 { font-size: 1rem; margin: 1.75rem 0 .5rem; opacity: .75; }

/* Seven dialogs render an <h2>; CollectionView is the one caller of .bad, for
   the message shown when a city's dataset will not load. */
.bad { color: #b3261e; font-weight: 600; }

@media (prefers-color-scheme: dark) {
    .bad { color: #ff8a80; }
}

/* Buttons behave like buttons on a touch screen.
   Reported from a phone: a tap on End turn sometimes selected the button
   instead of pressing it, and it went black. Three separate browser defaults
   between them, none of which a button wants:

   * a button's label is text, so a tap that moves a pixel or dwells a moment
     is a selection gesture — and once the label is selected the tap is spent on
     selecting rather than on clicking;
   * iOS paints -webkit-tap-highlight-color over the element, a translucent
     black which on a pale button reads as it turning black and can linger
     while the selection stands;
   * without touch-action the browser holds every tap back ~300ms waiting to
     see whether a second one is coming, which is most of the window in which a
     press becomes a drag.

   Nothing is lost by turning all three off. A button label is not content
   anybody wants to copy — the three things in this app that are (the seed and
   the two match codes) are a <code>, a <p> and a <span>, and each sets
   user-select: all for itself.

   :active replaces the highlight that was removed rather than leaving a tap
   with no feedback at all, which on a phone reads as a dead control. Opacity
   rather than a transform: it works the same on a reversed-out primary and on
   an outlined secondary, and it is not motion, so there is nothing for
   prefers-reduced-motion to object to. */
button {
    -webkit-tap-highlight-color: transparent;
    touch-action: manipulation;
    -webkit-user-select: none;
    user-select: none;
}

button:active:not(:disabled) { opacity: .75; }

#blazor-error-ui {
    display: none;
    position: fixed; bottom: 0; left: 0; right: 0;
    padding: .75rem 1rem; background: #ffe0b2; color: #4a2c00;
}

/* ---- Sector board -------------------------------------------------------
   The map fills the viewport and the inspector floats over it. On a phone the
   board is the screen; anything that permanently steals height from it makes
   the legibility problem worse, which is the one D4 accepted. */

.board, .board-canvas { position: fixed; inset: 0; }
.board-canvas { background: #e9e9e4; }

@media (prefers-color-scheme: dark) { .board-canvas { background: #1a1a19; } }

.board-error {
    position: absolute; inset: auto 0 0 0; margin: 0;
    padding: .6rem 1rem; background: #b3261e; color: #fff; font-size: 13px;
}


.sheet h2 { font-size: 1rem; margin: 0 0 .15rem; }
.sheet p { margin: 0 0 .5rem; font-size: 13px; }
.sheet .meta { opacity: .6; font-family: ui-monospace, Menlo, monospace; font-size: 11px; }
.sheet .hint { opacity: .6; }
.sheet .sponsor { font-weight: 600; }

.sheet button {
    font: inherit; font-size: 13px;
    padding: .45rem .8rem; margin: .15rem .3rem .15rem 0;
    border: 1px solid color-mix(in srgb, CanvasText 25%, transparent);
    border-radius: 8px; background: Canvas; color: CanvasText; cursor: pointer;
}
.sheet button:hover { background: color-mix(in srgb, CanvasText 8%, Canvas); }
.sheet button.secondary { opacity: .8; }

.loading { padding: 2rem; opacity: .7; }

.board-status {
    position: absolute; z-index: 3; top: .75rem; left: .75rem; margin: 0;
    padding: .4rem .7rem; border-radius: 8px; font-size: 12px;
    background: color-mix(in srgb, Canvas 85%, transparent); color: CanvasText;
    font-family: ui-monospace, Menlo, monospace;
}

.views { display: flex; flex-wrap: wrap; gap: .25rem; margin-top: .5rem; }
.views button { margin: 0; font-size: 11px; }

.legend {
    display: flex; flex-wrap: wrap; gap: .3rem .6rem;
    font-size: 11px;
}
.legend .key { display: inline-flex; align-items: center; gap: .35rem; white-space: nowrap; }

/* The swatch sits on the basemap's own surface, not on the panel's.

   NodeGlyph and TransitGlyph draw in the light half of the palette because the
   board is pinned light, and that is correct — a key that drifts from the map is
   worse than no key. But the panel follows the system theme, so in dark mode
   those glyphs were light-on-black: the muted grey shared by landmarks and the
   walking line came out very nearly invisible, and the darker line colours were
   indistinguishable from each other.

   Giving the swatch the surface the glyphs were drawn against fixes the contrast
   without touching the encoding, and says something true while doing it — this
   is a sample cut out of the map. */
.swatch,
.log-rounds .log-mark,
.notice-mark {
    display: inline-flex; align-items: center; justify-content: center;
    border-radius: 5px;
    background: #fcfcfb;
    border: 1px solid color-mix(in srgb, #52514e 22%, transparent);
}

/* The key's swatch is a fixed square: the chips line up in a grid and a row of
   boxes that each shrink to their glyph reads as debris. */
.swatch { width: 1.45rem; height: 1.45rem; flex: none; }

/* A line is not a square. What identifies a transit mode is its dash pattern,
   so its swatch has to be long enough to show one repeating — squeezing it into
   the node glyphs' box is what left the legend distinguishing modes by colour,
   which repeats every third mode. */
.swatch.line { width: auto; padding: 0 .3rem; }

/* Inline marks size to their glyph instead. They sit in a line of prose, so a
   fixed square would either crop the 26px line glyphs or leave the 16px node
   glyphs swimming. */
.log-rounds .log-mark,
.notice-mark { padding: .18rem .28rem; }

/* Both marks carry either kind of glyph — a transit line for "Travelled by",
   a node for "the exit is live" — and an inline SVG sits on the text baseline,
   which leaves a gap under it inside a box that is drawn to fit. */
.log-rounds .log-mark .transit-glyph,
.log-rounds .log-mark .glyph,
.notice-mark .transit-glyph,
.notice-mark .glyph { display: block; }

.key-group { margin-top: .5rem; }
.key-title {
    margin: 0 0 .35rem; font-size: 10px; font-weight: 600;
    text-transform: uppercase; letter-spacing: .06em; opacity: .5;
}

/* ---- Turn panel --------------------------------------------------------- */

.round { display: flex; justify-content: space-between; align-items: baseline; gap: .5rem; }
.round .r { font-weight: 600; font-size: 13px; }
.round .ap { font-size: 11px; opacity: .7; font-family: ui-monospace, Menlo, monospace; }

.intel { margin: .4rem 0 !important; font-size: 12px; }
.verdict { font-size: 1.05rem; margin: 0 0 .2rem; }

.alarm {
    margin: .4rem 0 !important; padding: .35rem .55rem; border-radius: 6px;
    font-size: 12px; font-weight: 600;
    background: color-mix(in srgb, #e34948 15%, transparent);
}

/* The exit's ring, ahead of the text rather than beside it as a grid cell —
   this is a paragraph, so the mark rides the first line and the text wraps
   under it normally. */
.alarm-mark {
    display: inline-flex; align-items: center;
    margin-right: .3rem; vertical-align: -2px;
}

.clues { margin: .5rem 0; padding-left: 1rem; font-size: 12px; }
.clues li { margin-bottom: .2rem; }

.refusal { margin: .4rem 0 !important; font-size: 12px; color: #b3261e; }

/* ---- Confirm dialog ------------------------------------------------------ */

.dialog-scrim { position: fixed; inset: 0; z-index: 10; background: rgb(0 0 0 / .35); }

.dialog {
    position: fixed; z-index: 11;
    left: 50%; top: 50%; transform: translate(-50%, -50%);
    width: min(22rem, calc(100vw - 2rem));
    padding: 1.1rem 1.2rem;
    border-radius: 14px;
    background: Canvas; color: CanvasText;
    box-shadow: 0 8px 40px rgb(0 0 0 / .3);

    /* Never taller than the window it is centred in.
       Without this, a dialog whose content outgrows the viewport puts half of
       itself above the top edge — and because it is centred by transform rather
       than laid out in the flow, there is nothing to scroll. It is simply gone.
       Reported on the match recap, where everything above "time" had vanished.

       Any short window does it: a phone in landscape, a laptop with the dev
       console open along the bottom. The briefing already carried its own cap
       for exactly this reason and every other dialog was one fact away from the
       same bug — so the cap belongs here rather than on each of them.

       dvh after vh: the dynamic unit is the correct one on a phone, where the
       address bar makes vh lie, and the static one is the fallback. */
    max-height: calc(100vh - 1.5rem);
    max-height: calc(100dvh - 1.5rem);
    overflow-y: auto;

    /* A dialog that has run out of scroll should not start scrolling the board
       behind it. */
    overscroll-behavior: contain;
}

.dialog-message { margin: 0 0 1rem; font-size: 14px; line-height: 1.45; }
.dialog-actions { display: flex; justify-content: flex-end; gap: .5rem; }
.dialog-actions button { margin: 0; font-size: 13px; padding: .5rem .9rem; }
.dialog-actions .primary {
    background: CanvasText; color: Canvas; border-color: CanvasText; font-weight: 600;
}

/* ---- Gadgets ------------------------------------------------------------ */

.gadgets { display: flex; gap: .3rem; margin: .5rem 0 .2rem; }
.gadgets button { margin: 0; flex: 1; font-size: 12px; padding: .4rem .3rem; }
.gadgets button:disabled { opacity: .35; cursor: not-allowed; }
.gadgets .price { display: block; font-size: 10px; opacity: .65; }

.aiming {
    display: flex; align-items: center; justify-content: space-between; gap: .5rem;
    margin: .5rem 0 !important; padding: .4rem .3rem .4rem .6rem;
    border-radius: 8px; font-size: 12px; font-weight: 600;
    background: color-mix(in srgb, currentColor 10%, transparent);
}
.aiming button { margin: 0; font-size: 11px; padding: .3rem .6rem; }

/* Lockdown is the one purchase with nothing on the map to look at, so this line
   is the whole of its feedback. Tinted from currentColor like .aiming rather
   than from a hand-picked hue, which is what keeps it legible on the dark sheet
   without a second rule in the dark-mode block below. */
.lockdown {
    margin: .5rem 0 !important; padding: .45rem .6rem;
    border-radius: 8px; font-size: 12px; line-height: 1.35;
    background: color-mix(in srgb, currentColor 10%, transparent);
    border-left: 3px solid color-mix(in srgb, currentColor 45%, transparent);
}

/* ---- Briefing cards and the consent disclosure ---------------------------- */

/* The pips sit left, the button right: progress belongs beside the control that
   advances it, not stacked above where it reads as decoration. */
.briefing-actions { display: flex; align-items: center; justify-content: space-between; gap: .6rem; }

/* Disabled on the first card rather than hidden, so paging does not reflow the
   row under the thumb that is doing the paging. */
.briefing-actions .secondary:disabled { opacity: .35; cursor: default; }

/* The cards are swiped as well as tapped, and a drag that selects the paragraph
   instead of turning the page reads as the gesture having failed. */
.dialog.briefing { touch-action: pan-y; -webkit-user-select: none; user-select: none; }

/* The mark beside the sentence that names it, on the basemap's own surface —
   the same reasoning as the map key: these glyphs are drawn in the light half of
   the palette, so on a dark panel they would be light on black. */
.briefing-glyph { display: flex; align-items: center; gap: .5rem; }

.briefing-progress { display: inline-flex; gap: .3rem; }
.briefing-progress .pip {
    width: 6px; height: 6px; border-radius: 50%;
    background: currentColor; opacity: .25;
}
.briefing-progress .pip.on { opacity: .8; }

/* The detail behind "What is kept". A summary that does not look tappable is a
   paragraph nobody opens, which is the failure this replaced. */
.notice-detail { margin: .2rem 0 .1rem; font-size: 13px; }
.notice-detail > summary {
    cursor: pointer; font-weight: 600; padding: .2rem 0;
    text-decoration: underline; text-underline-offset: 3px;
}
.notice-detail > p { margin: .35rem 0 0; opacity: .85; }

/* ---- Disclosure banner --------------------------------------------------- */

.effect {
    margin: .45rem 0 !important; padding: .4rem .6rem;
    border-radius: 7px; font-size: 12px; font-weight: 600;
    border-left: 3px solid currentColor;
}
.effect.found   { background: color-mix(in srgb, #e34948 14%, transparent); color: #a3302f; }
.effect.ruled   { background: color-mix(in srgb, #52514e 14%, transparent); color: #52514e; }
.effect.neutral { background: color-mix(in srgb, #2a78d6 14%, transparent); color: #1f5ea8; }

.effect { display: flex; align-items: center; justify-content: space-between; gap: .4rem; }

/* Teaching, not disclosure: the same shape as a banner reporting a fact, a
   shade quieter, because it is explaining one rather than announcing one. */
.effect.coach { font-weight: 500; }

/* The turn is being held open for this one. No dismiss button — the only way
   past it is the button below, which is the point. */
.effect.waiting {
    font-weight: 600;
    box-shadow: 0 0 0 2px color-mix(in srgb, currentColor 35%, transparent);
}
/* The same measurement as .dialog-close, for the same reason. This one had no
   vertical padding at all, so its target was the height of the glyph. Kept to
   2.25rem rather than 2.75 because it sits inside a banner it must not stretch:
   36px clears WCAG's floor comfortably and the banner stays one line. */
.effect .dismiss {
    flex: none;
    display: inline-flex; align-items: center; justify-content: center;
    width: 2.25rem; height: 2.25rem;
    margin: -.5rem -.35rem -.5rem 0; padding: 0;
    border: 0; border-radius: 50%; background: none; cursor: pointer;
    color: inherit; font-size: 13px; line-height: 1; opacity: .6;
}
.effect .dismiss:hover {
    opacity: 1;
    background: color-mix(in srgb, currentColor 14%, transparent);
}

/* ---- Bottom sheet -------------------------------------------------------
   Phone first: the board is the screen, so the sheet takes only the height it
   needs and everything referential hides behind one tap. Alerts and the turn
   bar stay out where they can be seen without one. */

.sheet {
    position: fixed; z-index: 5; left: 0; right: 0; bottom: 0;
    padding: .4rem .7rem calc(.5rem + env(safe-area-inset-bottom));
    border-radius: 16px 16px 0 0;
    background: color-mix(in srgb, Canvas 92%, transparent);
    backdrop-filter: blur(14px);
    box-shadow: 0 -2px 22px rgb(0 0 0 / .16);
    max-height: 82vh; overflow-y: auto;
    font-size: 13px;
}

/* The city, above the bar. Quiet on purpose: it is read once a match, which is
   the same reason the stat labels are quiet, so it takes their register rather
   than a heading's. Truncates instead of wrapping — "San Francisco" must not
   push the bar down a line on a narrow phone. */
.sheet-city {
    margin: 0 0 .1rem;
    font-size: 10px; font-weight: 600;
    text-transform: uppercase; letter-spacing: .09em;
    opacity: .55;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}

.sheet-bar {
    display: flex; align-items: center; gap: .55rem;
    padding: .1rem 0 .35rem;
}
/* Four numbers, one shape each, rather than four notations in a row. */
.hud { display: flex; align-items: stretch; gap: .15rem; flex: 1 1 auto; min-width: 0; }
.hud .stat {
    display: flex; flex-direction: column; gap: .05rem;
    padding: .1rem .5rem; min-width: 0; flex: 1 1 0;
    border-radius: 7px;
}
.hud .stat + .stat {
    border-left: 1px solid color-mix(in srgb, CanvasText 10%, transparent);
}
.stat-label {
    font-size: 9px; text-transform: uppercase; letter-spacing: .07em;
    opacity: .5; white-space: nowrap;
}
.stat-value {
    font-size: 15px; font-weight: 650; line-height: 1.1;
    font-variant-numeric: tabular-nums; white-space: nowrap;
}
.stat-of { font-size: 11px; font-weight: 400; opacity: .45; }

/* Out of AP is the one state the bar should say out loud — it is the answer to
   "why will nothing highlight". */
.hud .stat.spent .stat-value { opacity: .45; }
.hud .stat.spent .stat-label { opacity: .8; font-weight: 600; }

/* Sized rather than left to the glyph. This was about 24x18 — under WCAG's
   floor on the short axis — and it is the control that opens the detail panel,
   which on a phone is the only way to reach the log, the key and the collection
   at all. The smallest target in the app was the one guarding the most.

   Fixing the target left the glyph behind, which is the half a thumb does not
   care about and an eye does: at 11px, in ▴/▾ — Unicode's *small* triangles,
   drawn at roughly half the height of ▲/▼ — the arrow was about six pixels of
   ink in a 36px button, and reported on a phone as too small to tell which way
   it pointed. Both halves are now sized: the full-size triangle at 14px, which
   is a little over twice the ink and still smaller than the ⋯ and ? beside it. */
.grabber {
    display: inline-flex; align-items: center; justify-content: center; flex: none;
    width: 2.25rem; height: 2.25rem;
    margin: 0 .1rem 0 0 !important; padding: 0 !important;
    border: 0 !important; background: color-mix(in srgb, CanvasText 8%, transparent) !important;
    font-size: 14px; line-height: 1; border-radius: 8px; cursor: pointer;
}

.sheet-actions { display: flex; gap: .3rem; align-items: stretch; }
.sheet-actions button { margin: 0; flex: 1; font-size: 12px; padding: .45rem .3rem; }
.sheet-actions button:disabled { opacity: .35; cursor: not-allowed; }
.sheet-actions .price { display: block; font-size: 10px; opacity: .6; }
.sheet-actions .primary {
    background: CanvasText; color: Canvas; border-color: CanvasText; font-weight: 600;
}
.sheet-actions .wide { flex: 1.4; }

/* Acknowledging a result is not the same control as ending a turn, so it does
   not look like one. Reversed out and alone on its row — the shape of the row
   is the part that stops a thumb already on its way to the usual place. */
.sheet-actions .primary.holding {
    background: #2a78d6; border-color: #2a78d6; color: #fff;
    flex: 1; padding: .55rem .3rem;
}
.sheet-actions .primary.holding:hover { background: #1f5ea8; border-color: #1f5ea8; }

/* Referential, not time-critical: shown only when the sheet is opened. */
.sheet-detail { display: none; padding-top: .5rem; }
.sheet.open .sheet-detail { display: block; }

/* ---- The game menu ------------------------------------------------------- */

.sheet-menu {
    display: grid; gap: .25rem;
    margin: .35rem 0 .1rem; padding: .4rem;
    border-radius: 10px;
    background: color-mix(in srgb, CanvasText 6%, transparent);
}
.sheet-menu button {
    margin: 0; width: 100%; text-align: left;
    font-size: 12px; padding: .5rem .6rem;
}

/* ---- Folded sections ----------------------------------------------------
   The panel was a flat stack: hint, log, key, key, collection, seed, all at one
   weight and all open at once. Everything past the log is reference, so it folds
   — and a section that folds needs a head that reads as a control rather than as
   a heading, or nobody touches it. */

.panel-section {
    margin-top: .7rem; padding-top: .55rem;
    border-top: 1px solid color-mix(in srgb, CanvasText 12%, transparent);
}
.section-head {
    display: flex; align-items: baseline; gap: .5rem; width: 100%;
    margin: 0 !important; padding: .1rem 0 !important;
    border: 0 !important; background: none !important;
    font: inherit; color: inherit; text-align: left; cursor: pointer;
}
.section-head:hover .chev { opacity: 1; }
.section-title {
    font-size: 11px; font-weight: 600; text-transform: uppercase;
    letter-spacing: .05em; opacity: .6;
}
.section-head .chev {
    margin-left: auto; font-size: 11px; opacity: .55;
    text-decoration: underline; font-weight: 400;
}

/* The report button is a sibling of the seed line now, not inside it. */
.panel-section > .linky {
    margin: .5rem 0 0; padding: 0; border: 0; background: none;
    font: inherit; font-size: 11px; color: inherit; opacity: .6;
    text-decoration: underline; cursor: pointer;
}
.panel-section > .linky:hover { opacity: 1; }




/* A side panel, where covering the map is a trade there is no need to make.
   Gated on the pointer as well as the width, because width alone was the wrong
   question: an iPad in landscape is wider than plenty of desktop windows, and it
   was getting the panel while being held in two hands. What decides this is how
   the thing is being used, not how many pixels it has — a thumb wants controls
   at the bottom edge whatever the screen measures. `hover` answers that directly,
   and a tablet reports no hover in either orientation.

   The panel is sized by its contents rather than stretched to the viewport. It
   had been pinned top and bottom, so a quiet early round left a column of empty
   glass down the side of the screen. The cap is what keeps a long activity log
   scrolling inside the panel instead of running off the display. */
@media (min-width: 46rem) and (hover: hover) {
    .sheet {
        left: auto; top: .75rem; bottom: auto; right: .75rem;
        width: 21rem; border-radius: 14px;
        max-height: calc(100vh - 1.5rem);
        box-shadow: 0 2px 22px rgb(0 0 0 / .16);
    }
    .sheet-detail { display: block; }
    .grabber { display: none; }

    /* Room the phone does not have, spent on the numbers rather than on more
       content. A side panel that merely repeats the sheet at a larger size is a
       sheet with wasted space down both sides. */
    .sheet { width: 22rem; padding: .7rem .8rem .8rem; }
    .sheet-city { font-size: 11px; }
    .hud { gap: .2rem; }
    .hud .stat { padding: .15rem .55rem; }
    .stat-value { font-size: 17px; }
    .stat-label { font-size: 10px; }

    .sheet-menu { gap: .3rem; }
    .sheet-menu button { font-size: 12.5px; padding: .55rem .7rem; }

    /* A pointer can hit a smaller target, and the key is worth more room than
       the extra line of chips it would otherwise wrap to. */
    .legend { gap: .4rem .8rem; }
    .swatch { width: 1.6rem; height: 1.6rem; }
    .panel-section { margin-top: .8rem; padding-top: .65rem; }
}

/* ---- Alerts, in both themes ---------------------------------------------
   These were written as dark ink on a pale tint, which is unreadable on the
   dark sheet a dark-mode phone gets. The board is pinned light because its
   basemap is; the chrome around it is not, so anything with a hand-picked
   colour needs both halves. Dark steps come from the same validated palette. */

@media (prefers-color-scheme: dark) {
    .effect.found   { background: color-mix(in srgb, #e66767 22%, transparent); color: #ff9b9a; }
    .effect.ruled   { background: color-mix(in srgb, #c3c2b7 18%, transparent); color: #d8d7cc; }
    .effect.neutral { background: color-mix(in srgb, #3987e5 22%, transparent); color: #8ab8f0; }
    .refusal, .bad  { color: #ff8a80; }
    .alarm          { background: color-mix(in srgb, #e66767 24%, transparent); color: #ff9b9a; }
}

/* ---- Round recap --------------------------------------------------------- */

.notice-title {
    margin: 0 0 .6rem; font-size: 1.05rem;
    padding-bottom: .4rem;
    border-bottom: 1px solid color-mix(in srgb, CanvasText 15%, transparent);
}

/* The map picker closes with the same ✕ in the same corner as the recap, and
   the rule under its title runs the full width beneath it. At .4rem the rule
   lands at 49.2 and the button's bottom edge at 49.6 — close enough that the
   hover circle sits on the line. This clears it by 5px. */
.match-over .notice-title { padding-bottom: .75rem; }
.notice-lines { margin: 0 0 1rem; padding-left: 1.1rem; font-size: 13px; line-height: 1.5; }
.notice-lines li { font-size: 13px; }
.notice-lines li { margin-bottom: .3rem; }

/* The transit line beside the mode that was travelled by, so the recap can be
   matched to the board without dismissing it to reach the legend.

   inline-flex on the mark rather than on the li: the text has to stay ordinary
   wrapping prose, and a flex li would lay each wrapped line out as a flex item
   and lose the hanging indent from the list bullet. */
.notice-mark { margin-right: .4rem; vertical-align: middle; }

/* ---- Rogue activity log -------------------------------------------------- */

.log { margin-top: .6rem; }
.log-head { display: flex; align-items: baseline; justify-content: space-between; gap: .5rem; }
.log-head h3 { margin: 0; font-size: 12px; text-transform: uppercase; letter-spacing: .04em; opacity: .6; }
.log-head button { margin: 0; font-size: 11px; padding: .25rem .55rem; }

.log-rounds { list-style: none; margin: .35rem 0 0; padding: 0; font-size: 12px; }
.log-rounds li {
    display: grid; grid-template-columns: 2.4rem 1fr; gap: .1rem .5rem;
    padding: .3rem 0;
    border-top: 1px solid color-mix(in srgb, CanvasText 9%, transparent);
}
.log-rounds li:first-child { border-top: 0; }
.log-rounds .r { font-family: ui-monospace, Menlo, monospace; font-size: 11px; opacity: .55; }

/* Same size and family as the line above it. These are two halves of one
   round's account — what the Rogue did, and what that round disclosed — and
   setting them at different sizes made them read as unrelated kinds of thing.
   The distinction is carried by indent and weight instead, which is enough. */
.log-rounds .what { font-size: 12px; }

/* The mode's own line, beside the round that was travelled by it — the same
   mark the legend and the round recap draw, so all three agree. Decorative:
   the mode is named in the text right beside it. */
.log-rounds .log-mark { margin-right: .35rem; vertical-align: middle; }
.log-rounds .extra {
    grid-column: 2; font-size: 12px; opacity: .72; padding-top: .15rem;
}

/* ---- End of match -------------------------------------------------------- */

/* No position here. .dialog sets position:fixed and this rule sits later in the
   file at the same specificity, so a position declared here silently replaces it
   — which turned the recap from a centred dialog into a block in the document
   flow, offset by top/left:50% from wherever it happened to land and then hauled
   up out of view by the transform meant to centre it.
   It was added to anchor an absolutely-placed ✕ that is now a float. */
.match-over { width: min(23rem, calc(100vw - 2rem)); text-align: center; }

/* Top right, where a close control is looked for. Outside the flow so it cannot
   push the verdict off centre, and generously padded because it is the smallest
   target in the dialog and sits nearest the edge of a phone. */
/* Floated rather than absolutely placed. The dialog is a scroll container now,
   so an absolute ✕ would sit at the top of the *content* and scroll out of
   reach on the short window that made scrolling necessary in the first place.
   A float stays in the flow, keeps its place, and lets the verdict sit beside
   it without either of them needing to know the other's size. */
/* 44px square, which is Apple's guidance and nearly twice WCAG 2.5.8's floor of
   24. It was 23 — a 13px glyph with .3rem of padding round it — which is under
   even the floor, and was reported as hard to hit before anybody measured it.

   The mark stays 13px. What grows is the target, which is the part a finger
   aims at and the part nothing on screen draws. The negative margins pull that
   larger box back into the dialog's own padding so it sits in the corner rather
   than pushing the verdict down. */
/* position/z-index because a float is hit-tested *underneath* the in-flow
   content that follows it, and the target now reaches far enough down to meet
   some. The button spans 5.6px to 49.6px below the dialog's top edge; the
   verdict's block box — full content width, because only its line boxes wrap
   around a float, not the box itself — spans 17.6 to 46.1. That is 28 of the
   44 pixels dead, with the ✕ itself at 27.6, dead centre of them. All that
   could be pressed was 12px above the heading and 3.5px below it.

   Which is why it read as the target appearing and then vanishing: opacity
   below 1 paints as a stacking context, so at rest the button sat on top and
   lit up on approach — and :hover's opacity:1 destroyed that context, dropped
   it back under the heading, and took the hover away with it. Approach, glow,
   nothing there. Enlarging the target is what exposed this: at 23x23 it never
   reached the heading. */
.dialog-close {
    float: right;
    position: relative; z-index: 1;
    display: inline-flex; align-items: center; justify-content: center;
    width: 2.75rem; height: 2.75rem;
    margin: -.75rem -.75rem 0 .3rem !important;
    padding: 0 !important;
    border: 0 !important; border-radius: 50%;
    background: none !important;
    font-size: 13px; line-height: 1; opacity: .45; cursor: pointer;
}

/* A hover state on the whole target, not just the glyph — it is the only thing
   that tells a pointer how big the button actually is. */
.dialog-close:hover {
    opacity: 1;
    background: color-mix(in srgb, CanvasText 10%, transparent) !important;
}
.match-over .verdict { margin: 0 0 .2rem; font-size: 19px; }

/* Label above value rather than beside it: "rogue's exit" carries a place name
   and a hop count, which does not fit on a line shared with its own label. */
.debrief { margin: .9rem 0 1.1rem; display: grid; gap: .55rem; text-align: left; }
.debrief div { display: flex; flex-direction: column; gap: .1rem; }
.debrief dt {
    font-size: 10px; letter-spacing: .08em; text-transform: uppercase; opacity: .5;
}
.debrief dd { margin: 0; font-size: 13.5px; }

/* Wraps because a daily debrief carries three buttons — copy report, share
   result, and the rewards or continue — and .dialog-actions is otherwise a
   single non-wrapping row. Three of them do not fit across a phone, and without
   this they squeeze rather than stack. */
.match-over .dialog-actions { justify-content: center; flex-wrap: wrap; }

/* The report, demoted. A bug-reporting tool should not be one of three equal
   buttons in the moment somebody has just won. */
.report-line { margin-top: .5rem; }
.report-line .linky {
    margin: 0; padding: .3rem; border: 0; background: none;
    font: inherit; font-size: 11px; color: inherit; opacity: .55;
    text-decoration: underline; cursor: pointer;
}
.report-line .linky:hover { opacity: 1; }

.next-choices { display: grid; gap: .45rem; margin: 1rem 0 .2rem; }
.next-choices button { margin: 0; width: 100%; font-size: 13px; padding: .55rem .9rem; }
.next-note { margin: .6rem 0 0; font-size: 11px; opacity: .5; }

/* Scrolled rather than sized to fit: two cities need no list at all and ten
   need one that does not push the buttons off the screen. */
.city-list {
    list-style: none; margin: .5rem 0 0; padding: 0;
    max-height: 11rem; overflow-y: auto;
    display: grid; gap: .3rem;
}
.city-list button {
    display: flex; align-items: baseline; justify-content: space-between; gap: .5rem;
    width: 100%; margin: 0; font-size: 13px; padding: .5rem .7rem;
}
.city-progress { font-size: 11px; opacity: .55; }

/* ---- Daily Challenge or Free Play ---------------------------------------- */

/* The mode buttons carry a second line — which daily it is and which city, or
   what free play means — so they stack rather than centring a single label. The
   subtitle is what makes "Daily Challenge" answerable without tapping it. */
.mode-choice .next-choices button {
    display: flex; flex-direction: column; align-items: flex-start; gap: .15rem;
    text-align: left; padding: .65rem .9rem;
}
.daily-tag { font-size: 11px; opacity: .6; font-weight: 400; }

/* Shown in place of the Daily button once the day is spent. A disabled button
   invites tapping to find out why; this says why instead. */
.daily-done {
    border: 1px solid color-mix(in srgb, CanvasText 12%, transparent);
    border-radius: 6px; padding: .6rem .9rem; text-align: left;
}
.daily-line { margin: 0; font-size: 13px; }
.daily-result { margin: .15rem 0 0; font-size: 12px; opacity: .7; }

/* Wraps, because six modes of Paris and a three-figure time do not fit across a
   phone on one line, and the separators have to stay with what they separate. */
.daily-stats { margin: .2rem 0 0; font-size: 11.5px; opacity: .6; line-height: 1.45; }
.daily-done .next-note { margin: .35rem 0 0; }

/* ---- Destination Card ---------------------------------------------------- */

/* Darker than the ordinary dialog scrim: the card is the only thing on screen
   for a moment, and a reward that shares the stage is not a moment. */
.card-scrim { background: rgb(0 0 0 / .55); }

.card-reveal {
    position: fixed; z-index: 11;
    left: 50%; top: 50%;
    width: min(20rem, calc(100vw - 3rem));
    padding: 1rem 1rem 1.1rem;
    border-radius: 18px;
    background: Canvas; color: CanvasText;
    box-shadow: 0 14px 60px rgb(0 0 0 / .45);
    cursor: pointer; text-align: center;
    transform: translate(-50%, -50%);
    animation: card-arrive .34s cubic-bezier(.2, .9, .25, 1.15) both;
}

/* Up from a point, with a half-turn. D7's "spinning and expanding from a dot" —
   the turn is what makes it read as a card being dealt rather than a modal
   appearing. */
@keyframes card-arrive {
    from { transform: translate(-50%, -50%) scale(.05) rotate(-180deg); opacity: 0; }
    60%  { opacity: 1; }
    to   { transform: translate(-50%, -50%) scale(1) rotate(0deg); opacity: 1; }
}

.card-reveal.leaving { animation: card-leave .22s ease-in both; }

@keyframes card-leave {
    from { transform: translate(-50%, -50%) scale(1) rotate(0deg); opacity: 1; }
    to   { transform: translate(-50%, -50%) scale(.06) rotate(140deg); opacity: 0; }
}

/* A spin is decoration; the card is the information. Anyone who has asked for
   less motion gets the card and none of the theatre. */
@media (prefers-reduced-motion: reduce) {
    .card-reveal, .card-reveal.leaving { animation-duration: .01ms; }
}

.card-earned {
    font-size: 10.5px; letter-spacing: .1em; text-transform: uppercase;
    opacity: .55; margin-bottom: .6rem;
}

/* The picture is square and takes the majority of the card, photograph or
   placeholder alike — so a photograph arriving later changes what is in the
   frame and not the shape of the card. */
.card-picture {
    width: 100%; aspect-ratio: 1; border-radius: 12px; overflow: hidden;
    background: color-mix(in srgb, CanvasText 6%, transparent);
}
.card-picture img, .card-picture svg { width: 100%; height: 100%; object-fit: cover; display: block; }

.card-title { margin: .7rem 0 0; font-size: 17px; line-height: 1.25; }
.card-flavour { margin: .35rem 0 0; font-size: 12.5px; line-height: 1.4; opacity: .75; }

.card-notes { display: flex; flex-wrap: wrap; gap: .35rem; justify-content: center; margin-top: .6rem; }
.card-note {
    font-size: 11px; padding: .2rem .5rem; border-radius: 999px;
    background: color-mix(in srgb, CanvasText 9%, transparent);
}
.card-note.offer { background: color-mix(in srgb, CanvasText 14%, transparent); }
.card-note.credit { background: none; opacity: .5; }

.card-dismiss { margin: .8rem 0 0; font-size: 11px; opacity: .5; }

/* Set apart from the date it shares a line with, because they answer different
   questions — when this one was earned, and how far through the pile it is. The
   second is the only thing on screen suggesting there is anywhere to swipe. */
.card-place {
    margin-left: .5rem; padding-left: .5rem;
    border-left: 1px solid color-mix(in srgb, CanvasText 25%, transparent);
    font-variant-numeric: tabular-nums;
}

/* ---- Collection status --------------------------------------------------- */

.collection-status { margin: .9rem 0 0; }

.collection-head {
    display: flex; align-items: baseline; gap: .35rem; flex-wrap: wrap;
    width: 100%; margin: 0; padding: .3rem 0; border: 0; background: none;
    color: inherit; font: inherit; text-align: left; cursor: pointer;
}
.collected-count { font-size: 14px; font-weight: 600; }
.collected-count .of { font-weight: 400; opacity: .55; }
.collected-label { font-size: 11px; opacity: .55; }
.collection-head .chev { margin-left: auto; font-size: 11px; opacity: .6; text-decoration: underline; }

/* Capped and scrolled: a fresh profile has every node in here, and a list that
   pushes the seed and the picker off the panel is worse than one you scroll. */
.uncollected-list {
    list-style: none; margin: .35rem 0 0; padding: 0;
    max-height: 9rem; overflow-y: auto;
    font-size: 12px;
}
.uncollected-list li { margin: 0 0 .1rem; }
.uncollected-list .linky {
    padding: 0; border: 0; background: none; color: inherit; font: inherit;
    opacity: .75; cursor: pointer; text-align: left;
}
.uncollected-list .linky:hover { opacity: 1; text-decoration: underline; }

.see-collection {
    display: block; margin: .45rem 0 0; padding: 0;
    border: 0; background: none; color: inherit; font: inherit;
    font-size: 12px; opacity: .7; cursor: pointer; text-decoration: underline;
}
.see-collection:hover { opacity: 1; }

/* ---- Collection ---------------------------------------------------------- */

/* A sheet rather than a page: the board stays behind it, because this is opened
   mid-match and closing it should read as putting something down rather than as
   navigating back. */
.collection {
    position: fixed; z-index: 11;
    left: 50%; top: 50%; transform: translate(-50%, -50%);
    width: min(46rem, calc(100vw - 2rem));
    max-height: min(44rem, calc(100vh - 2rem));
    display: flex; flex-direction: column;
    padding: 1rem 1.1rem 1.2rem;
    border-radius: 18px;
    background: Canvas; color: CanvasText;
    box-shadow: 0 14px 60px rgb(0 0 0 / .45);
}

.collection-top { display: flex; align-items: center; gap: .75rem; }

/* The auto margin is what puts the ✕ at the far end. It sits here rather than
   on the button because .dialog-close is a float everywhere else — a float does
   nothing to a flex item — and its own margins are !important, so the heading is
   the side of this row that can be told to give way. */
.collection-top h2 { margin: 0 auto 0 0; font-size: 18px; }

/* Left to the one place that still uses it: the Done under a card being read in
   the collection, which is a button to finish with something rather than a way
   out of a screen. */
.collection-close {
    padding: .3rem .8rem; border-radius: 999px;
    border: 1px solid color-mix(in srgb, CanvasText 20%, transparent);
    background: none; color: inherit; font: inherit; font-size: 12px; cursor: pointer;
}
.collection-close:hover { background: color-mix(in srgb, CanvasText 8%, transparent); }

/* One row collapsed, at seven cities or at twenty.

   This was `flex-wrap: wrap` over a chip per city, which is a good shape while
   the chips fit on one line and a bad one after that: .card-grid below is
   `flex: 1 1 auto`, so every row the chips gained the grid lost, and the grid is
   what the screen is for. The open state is the map picker's .city-list rather
   than anything new — same 11rem scroll, same name-and-count row. */
.collection-cities { margin: .8rem 0 0; }

.city-current {
    display: flex; align-items: baseline; gap: .5rem; width: 100%;
    margin: 0; padding: .4rem .7rem; border-radius: 999px;
    border: 1px solid color-mix(in srgb, CanvasText 16%, transparent);
    background: none; color: inherit; font: inherit; font-size: 12.5px; cursor: pointer;
}
.city-current:hover { background: color-mix(in srgb, CanvasText 7%, transparent); }

/* The name takes the slack so the count and the arrow stay together at the
   right, where they are in the same place whatever the city is called — and
   truncates rather than shoving them off it, since the imported names run to
   "San Francisco Central" and nothing caps how long the next one is. */
.city-current .city-name {
    margin-right: auto; min-width: 0;
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
.city-chev { font-size: 10px; opacity: .55; }

.city-count { font-size: 11px; opacity: .6; }
.city-count.complete { opacity: 1; font-weight: 600; }

/* The city being shown, marked in the list as well as named in the collapsed
   row above it. Quieter than the map picker's `.primary`, because this is
   "you are here" rather than a recommendation. */
.city-list button.on {
    background: color-mix(in srgb, CanvasText 10%, transparent);
    font-weight: 600;
}

.collection-note { margin: .8rem 0 0; font-size: 12px; opacity: .6; }
.collection-note.bad { opacity: .9; }

/* Scrolls inside the sheet, so the city chips and the count stay put while a
   forty-eight card city is read. */
.card-grid {
    list-style: none; margin: .7rem 0 0; padding: .2rem 0 0;
    overflow-y: auto; flex: 1 1 auto;
    display: grid; gap: .7rem;
    grid-template-columns: repeat(auto-fill, minmax(6.2rem, 1fr));
}

.card-tile button {
    display: block; width: 100%; padding: 0;
    border: 0; background: none; color: inherit; font: inherit;
    text-align: left; cursor: pointer;
}
.card-tile .tile-frame {
    width: 100%; aspect-ratio: 1; border-radius: 10px; overflow: hidden;
    background: color-mix(in srgb, CanvasText 6%, transparent);
}
.card-tile .tile-frame img, .card-tile .tile-frame svg {
    width: 100%; height: 100%; object-fit: cover; display: block;
}
.card-tile .tile-name {
    display: block; margin-top: .3rem; font-size: 11px; line-height: 1.25;
}

/* An uncollected card is a place, drawn as an empty frame. Dashed rather than
   dimmed-out art: showing the picture greyed would mean showing a card the
   player has not earned. */
.card-tile.empty .tile-frame {
    background: none;
    border: 1px dashed color-mix(in srgb, CanvasText 22%, transparent);
}
.card-tile.empty .tile-name { opacity: .45; }

/* The same card as the reveal, standing still. */
.card-reveal.held { animation: none; cursor: default; }
.card-reveal.held .collection-close { margin: .9rem auto 0; }

/* Opened from the collection, so it has to clear the sheet — otherwise the
   sheet stays lit behind the card and swallows the click that dismisses it. */
.dialog-scrim.over-collection { z-index: 12; }
.card-reveal.over-collection { z-index: 13; }

/* ---- Seed ---------------------------------------------------------------- */

.seed {
    margin: .6rem 0 0 !important; font-size: 11px; opacity: .55;
    display: flex; align-items: center; gap: .4rem;
}
.seed code {
    font-family: ui-monospace, Menlo, monospace; font-size: 11px;
    padding: .15rem .35rem; border-radius: 4px; cursor: pointer;
    background: color-mix(in srgb, CanvasText 9%, transparent);
    /* One long-press selects the whole seed rather than a word of it. */
    user-select: all; -webkit-user-select: all;
}
.seed .copied { opacity: .8; font-style: italic; }

/* The build id shares the seed's register and none of its behaviour: tapping
   the seed copies a link, and a second <code> that looks identical but does
   nothing would read as broken. So the pointer goes, and the row sits tighter
   under the seed than a new section would. */
.seed.build { margin-top: .3rem !important; }
.seed.build code { cursor: text; }
.consent-line {
    margin: .5rem 0 0 !important; font-size: 11px; opacity: .6;
    display: flex; align-items: baseline; gap: .35rem;
}
.consent-line b { font-weight: 600; }
.consent-line .linky {
    margin-left: auto; padding: 0; border: 0; background: none;
    font: inherit; color: inherit; text-decoration: underline; cursor: pointer;
}

.seed-label {
    font-size: 10px; text-transform: uppercase; letter-spacing: .06em; opacity: .7;
}

/* Sits in a row of small print, so it reads as a link rather than competing
   with the gadget buttons above it. */
.seed .linky {
    margin-left: auto; padding: 0; border: 0; background: none;
    font: inherit; color: inherit; text-decoration: underline; cursor: pointer;
}

/* ---- Match report -------------------------------------------------------- */

/* Only ever rendered when the clipboard refused, so it is allowed to take the
   top of the sheet: at that point selecting this text by hand is the task. */
.report-fallback {
    margin-bottom: .5rem; padding: .5rem;
    border-radius: 8px; font-size: 12px;
    background: color-mix(in srgb, CanvasText 7%, transparent);
}
.report-fallback p { margin: 0 0 .35rem; }
.report-fallback textarea {
    width: 100%; box-sizing: border-box; resize: vertical;
    font-family: ui-monospace, Menlo, monospace; font-size: 11px;
    color: CanvasText; background: Canvas;
    border: 1px solid color-mix(in srgb, CanvasText 20%, transparent);
    border-radius: 6px; padding: .35rem;
}
.report-fallback button { margin-top: .35rem; }

/* The glyph sits on the text baseline in both the key and the stat row, so it
   reads as part of the label rather than floating beside it. */
.glyph { vertical-align: -0.15em; flex: none; }
.sheet-bar .stat { display: inline-flex; align-items: center; gap: .22rem; }

/* A re-showable disclosure. Sits on the same line as a plain one and reads the
   same, with a target marker to say it can be tapped — the wording is the point,
   the affordance is a hint. */
.log-rounds button.extra {
    display: block; width: 100%; margin: 0; text-align: left;
    padding: .15rem .35rem .15rem 0;
    border: 0; background: none; color: inherit; cursor: pointer;
    font: inherit; font-size: 12px; opacity: .72;
}
.log-rounds button.extra::before { content: "◎ "; opacity: .7; }
.log-rounds button.extra:hover,
.log-rounds button.extra:focus-visible {
    opacity: 1; background: color-mix(in srgb, CanvasText 7%, transparent);
    border-radius: 5px;
}

/* ---- Briefing ------------------------------------------------------------ */

.briefing { max-height: 82vh; overflow-y: auto; }
.briefing p { margin: 0 0 .7rem; font-size: 13px; line-height: 1.5; }
/* The rule belongs between the briefing and the way out of it, not floating
   above whichever paragraph happened to be last. */
.briefing .dialog-actions {
    margin-top: .9rem; padding-top: .8rem;
    border-top: 1px solid color-mix(in srgb, CanvasText 15%, transparent);
}

.icon-btn {
    margin: 0 !important; padding: 0 !important;
    width: 1.7rem; height: 1.7rem; flex: none;
    display: inline-flex; align-items: center; justify-content: center;
    border: 0 !important; border-radius: 50%;
    background: color-mix(in srgb, CanvasText 10%, transparent) !important;
    font-size: 12px; line-height: 1; cursor: pointer;
}
.icon-btn:hover { background: color-mix(in srgb, CanvasText 18%, transparent) !important; }
.icon-btn.on {
    background: CanvasText !important; color: Canvas;
}

/* ---- Challenge Mode -------------------------------------------------------
   The setup screen, before there is a board to draw. Deliberately plain: it is
   two short forms and a code to copy, and it is on screen for about fifteen
   seconds of a match that runs for twenty minutes. */

.challenge-setup {
    max-width: 30rem;
    margin: 0 auto;
    padding: 1.25rem 1rem 2rem;
}

.challenge-setup h1 { font-size: 1.25rem; margin: 0 0 1rem; }
.challenge-setup h2 { font-size: .95rem; margin: 0 0 .5rem; }

.challenge-setup label {
    display: block;
    font-size: 12px;
    opacity: .75;
    margin-bottom: .6rem;
}

.challenge-setup select,
.challenge-setup input {
    display: block;
    width: 100%;
    margin-top: .2rem;
    padding: .5rem;
    font: inherit;
    font-size: 14px;
    color: CanvasText;
    background: Canvas;
    border: 1px solid color-mix(in srgb, CanvasText 25%, Canvas);
    border-radius: 6px;
}

/* Full width and a generous target: this screen is used on a phone with one
   thumb, and 44px is the figure check-tap-targets.py looks for. */
.challenge-setup button {
    display: block;
    width: 100%;
    min-height: 44px;
    margin-top: .5rem;
    padding: .55rem 1rem;
    font: inherit;
    font-size: 13px;
    border-radius: 6px;
    border: 1px solid color-mix(in srgb, CanvasText 25%, Canvas);
    background: Canvas;
    color: CanvasText;
    cursor: pointer;
}

.challenge-setup button:disabled { opacity: .35; cursor: not-allowed; }

/* The share button carries the whole flow on a phone — tap, pick Messages,
   done — so it is the one filled control on the screen.

   Reversed out of CanvasText, which is what .primary means everywhere else in
   this app (.dialog-actions, .sheet-actions). It was #2a78d6, and that blue is
   spoken for: .primary.holding uses it for the one control that acknowledges a
   result, and a second meaning would cost that one its distinctness. */
.challenge-setup button.primary {
    background: CanvasText;
    color: Canvas;
    border-color: CanvasText;
    font-weight: 600;
}

.challenge-setup button.secondary { opacity: .8; }

/* The code itself. Monospace and widely spaced because its whole job is to be
   read off one screen and typed into another — the alphabet already excludes
   I, L, O and U for the same reason. Selectable on purpose: copying it is the
   commonest way it will travel. */
.challenge-code {
    font-family: ui-monospace, Menlo, monospace;
    font-size: clamp(1.1rem, 6vw, 1.6rem);
    letter-spacing: .12em;
    user-select: all;
    margin: .25rem 0 .75rem;
}

/* The Challenge board's map key.
   A <details> so it costs one line when closed and is still reachable on round
   twelve — the Rogue's side has no briefing deck of its own, and a player who
   opened a link is meeting every mark on the board for the first time.

   The contents are the board legend's own .key-group / .legend / .key /
   .swatch, so there is nothing to style here beyond the disclosure itself. */
.map-key { margin: .5rem 0; font-size: 12px; }
.map-key summary { cursor: pointer; opacity: .75; padding: .35rem 0; }
.map-key .hint { margin: .5rem 0 0; line-height: 1.4; }

/* Whose turn it is, kept out of .sheet-detail so it survives the sheet being
   collapsed — which is exactly what a player does while waiting, since there is
   nothing to do but look at the map.

   Built on .effect's shape rather than a new one: same radius, same padding,
   same 12px 600, same left rule in currentColor. Every other line that reports
   a state on this sheet looks like this, and a status bar that did not would
   read as something bolted on. */
.turn-status {
    display: flex; align-items: center; gap: .45rem;
    margin: .45rem 0 !important; padding: .4rem .6rem;
    border-radius: 7px; font-size: 12px; font-weight: 600;
    opacity: .7;
}

/* Tinted by the same formula the disclosure banners use — a hue at 14% behind
   a darker step of it — so waiting, found and ruled are visibly the same kind
   of object. Grey, because waiting is a state rather than a disclosure and the
   blue in this vocabulary means the board is telling you something. */
.turn-status.waiting {
    opacity: 1;
    color: #52514e;
    background: color-mix(in srgb, #52514e 14%, transparent);
    border-left: 3px solid currentColor;
}

/* The result. Larger and heavier than a turn, because a match ending is the one
   thing on this screen that has to be unmissable — and the line it replaced was
   the same weight as "Waiting for them…".

   Green and red are the palette's own exfil and start, at the same 14% the
   disclosure banners use. The words carry the result on their own; the colour
   is the second channel. */
.turn-status.over {
    opacity: 1;
    font-size: 14px; font-weight: 700;
    padding: .55rem .6rem;
    border-left: 3px solid currentColor;
}

.turn-status.over.won {
    color: #006b00;
    background: color-mix(in srgb, #008300 16%, transparent);
}

.turn-status.over.lost {
    color: #a3302f;
    background: color-mix(in srgb, #e34948 16%, transparent);
}

.spinner {
    width: 12px;
    height: 12px;
    flex: none;
    border-radius: 50%;
    border: 2px solid color-mix(in srgb, CanvasText 25%, Canvas);
    border-top-color: CanvasText;
    animation: spin .7s linear infinite;
}

@keyframes spin { to { transform: rotate(360deg); } }

/* Honours a reduced-motion preference: the message is the information and the
   spin is decoration, so the decoration is what goes. */
@media (prefers-reduced-motion: reduce) {
    .spinner { animation: none; }
}

/* The match code, opposite the way out of the match. It is the only handle on a
   challenge — the mailbox is addressed by it and there is no list of games to
   return to — so it belongs somewhere a player can find it without going back
   to the text thread they were sent. */
.match-footer {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: .6rem;
    margin-top: .5rem;
}

.match-code {
    font-family: ui-monospace, Menlo, monospace;
    font-size: 12px;
    letter-spacing: .06em;
    opacity: .7;
    user-select: all;
}

.match-footer button { margin: 0; }

/* The result, stated where the turn used to be.
   A match ending is the one thing on this screen that has to be unmissable, and
   the line that said "Match over." was the same weight as "Waiting for them…".
   Larger, bolder, and tinted by whether this player won — the words carry the
   result on their own, so the colour is a second channel rather than the
   information. */
.turn-status.over {
    font-size: 14px;
    font-weight: 700;
    opacity: 1;
    padding: .55rem;
}

.turn-status.over.won {
    background: color-mix(in srgb, #008300 16%, Canvas);
    border: 1px solid color-mix(in srgb, #008300 40%, Canvas);
}

.turn-status.over.lost {
    background: color-mix(in srgb, #e34948 14%, Canvas);
    border: 1px solid color-mix(in srgb, #e34948 38%, Canvas);
}

/* Out of the Challenge setup without starting anything.
   Quiet, because it is an escape hatch rather than an offer — the screen's job
   is to make a challenge, and this is for the player who arrived by accident or
   changed their mind. */
.challenge-setup .back-link {
    width: auto;
    min-height: 0;
    margin: 0 0 .75rem;
    padding: .3rem 0;
    border: 0;
    background: none;
    font-size: 12px;
    opacity: .7;
}

.challenge-setup .back-link:hover { opacity: 1; }
