/* Midnight Alibi: a manila case file open on a desk at night. */
@font-face { font-family: 'Bodoni Moda'; src: url('../fonts/bodoni-moda.woff2') format('woff2'); font-weight: 400 900; font-style: normal; font-display: swap; }
@font-face { font-family: 'Bodoni Moda'; src: url('../fonts/bodoni-moda-italic.woff2') format('woff2'); font-weight: 400 900; font-style: italic; font-display: swap; }
@font-face { font-family: 'Courier Prime'; src: url('../fonts/courier-prime.woff2') format('woff2'); font-weight: 400; font-display: swap; }
@font-face { font-family: 'Karla'; src: url('../fonts/karla.woff2') format('woff2'); font-weight: 200 800; font-display: swap; }

/* Previews of looks (www/css/cosmetics.css) start from these same defaults,
   whatever the player has in use: the preview scene and standard swatches. */
:root, .look-scene, .look-preview[data-look="standard"] {
  /* the desk */
  --desk: #141217;
  --desk-ink: #efe8d8;
  --desk-muted: #aaa08b;
  --brass: #d6a54d;
  /* the folder */
  --manila: #ded2b8;
  --manila-back: #c5b390;
  --manila-edge: #c4ae7c;
  --card: #eee7d6;
  --ink: #2b2417;
  /* Small text on the folder needs 4.5:1 (Android's accessibility
     guidance), so --muted keeps that on --manila and the lighter papers in
     every file look, and an unselected tab's label (--tab-ink) keeps it on
     --manila-back. */
  --muted: #645844;
  --tab-ink: var(--ink);
  --line: rgba(43, 36, 23, 0.16);
  --stamp: #b3372d;
  --stamp-on-dark: #e0605a; /* the red stamp where it lands on the desk */
  /* textures a desk or file look can add (www/css/cosmetics.css) */
  --desk-detail: radial-gradient(ellipse at 85% 0, var(--lamp), transparent 65%);
  --file-detail: repeating-linear-gradient(0deg, transparent 0 25px, var(--paper-rule) 25px 26px);
  --pen: #1f3a68;
  --highlight: #f2c230;
  --good: #2f7a4a;
  --room-ink: #f7efdc;
  /* One color per room (Engine.ROOMS), the same in every case, each in a
     colour that suits it: the Kitchen brick red, the Study leather, the Hall
     brass, the Attic dust, the Garden leaf green, the Conservatory a deeper
     green, the Cellar stone, the Library ink blue, the Lounge blue velvet,
     the Chapel purple, the Ballroom pink and the Gallery wine. Any two can
     meet in a case, so all twelve stay apart in lightness as well as hue:
     rooms next to each other on the colour wheel are a light and a dark
     one, which also helps with red-green colour blindness. (Twelve hues at
     one lightness put neighbours too close, and testers mixed them up.)
     Each keeps at least 5.3:1 against --room-ink, and every hunch ink
     (cosmetics.css) keeps the contrast it had on the old colours.
     tests/engine.test.js checks the distances and the 5.3:1. */
  --room-kitchen: #974a4a; --room-study: #7d2a05; --room-hall: #7e5200;
  --room-attic: #534f29; --room-garden: #107127; --room-conservatory: #014b31;
  --room-cellar: #2a636a; --room-library: #104668; --room-chapel: #4e2795;
  --room-lounge: #575b9c; --room-ballroom: #963687; --room-gallery: #692c45;
  /* the noir board */
  --noir: #141217;
  --noir-2: #1d1a21;
  --noir-ink: #ece4d1;
  --noir-muted: #9b917d;
  --noir-line: rgba(236, 228, 209, 0.13);
  --surface-shadow: 0 8px 22px rgba(0, 0, 0, .28);
  --paper-shadow: 0 3px 8px rgba(0, 0, 0, .2);
  --surface-radius: 5px;
  --paper-radius: 2px;
  --fabric: #292725;
  --paper-rule: rgba(72, 54, 29, .045);
  --wood: #554130;
  --mist: rgba(153, 168, 178, .32);
  --lamp: rgba(255, 214, 140, 0.16);
  --newsprint: #ece3cc;
  /* the briefing's fold tab: the clipping's corner turned down, its underside
     darker and casting a shadow onto the paper (each case file in
     cosmetics.css sets its own pair, from its --newsprint and --ink) */
  --newsprint-fold: #d8ccaf;
  --fold-shadow: -2px 3px 5px rgba(27, 23, 18, .28), inset 1px -1px 0 rgba(27, 23, 18, .2);
  /* pencil notes: the ink cosmetic changes the hand, never the room colours */
  --note-ink: var(--room-ink);
  --note-font: 800 1em/1.15 var(--sans); /* sized by the board square (.notes) */
  --note-tracking: -.02em; /* a hunch chip's letter spacing, by ink */
  --note-variation: normal; /* a hunch chip's font variation, by ink */
  --ink-face: 800 0.75rem var(--sans); /* an ink's hand at swatch size */
  --display: 'Bodoni Moda', 'Didot', 'Bodoni 72', Georgia, serif;
  --sans: 'Karla', system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
  --type: 'Courier Prime', 'Courier New', Courier, monospace;
  color-scheme: dark;
}

/* The app's one size: everything that grows with text is in rem, from this
   root. --ui-scale (the phone's text size x the Text size setting, 1 to 2)
   is set on <html> by display.js. It is deliberately not declared in the
   tokens above, whose selector also matches the looks previews. The browser
   must not resize text on its own as well. */
/* --base-root is the root size at Standard. A display heading (X above
   1.25rem) grows with the text only up to 1.25 times its Standard size, but
   never below 1.25rem, so it stays above the text it heads at every scale:
   max(min(Xrem, X * 1.25 * var(--base-root)), 1.25rem). tests/layout.test.js
   checks the form. Smaller headings are plain rem and grow with the text. */
/* No screen sits in a centred column (spec 005 settled point 11, Task 10):
   every view spans the device, less only each edge's spacing or the inset
   the device reports there, the larger. A page whose lines would run past a
   readable measure (about 75 characters) lays its cards out in columns
   instead of leaving margins (scripts/check-layout.cjs detector 9). */
:root {
  --base-root: 16px;
  /* A readable measure for a block of words (about 75 characters at their
     own size): where a wide page sets a paragraph across a card or a sheet,
     it wraps at this rather than running on. */
  --measure: 34em;
  /* The app's own spacing at a full-screen page's sides where no bar is
     there: the casebook, its lists, the profile and How to play keep it
     (scripts/check-layout.cjs detector 9 holds every page to it). */
  --page-edge: min(1rem, 5vw);
  font-size: calc(var(--base-root) * var(--ui-scale, 1));
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
}

* { box-sizing: border-box; }
[hidden] { display: none !important; }
html, body { height: 100%; }
body {
  margin: 0;
  background: var(--desk);
  background-image: radial-gradient(120% 60% at 50% -10%, #232a4a 0%, transparent 60%);
  color: var(--desk-ink);
  font: 1rem/1.45 var(--sans);
  overflow: hidden;
  overflow-wrap: break-word;
  -webkit-tap-highlight-color: transparent;
  /* Pinch zooms the whole app; a double tap never does (iOS would, and the
     game's double taps are its own). */
  touch-action: manipulation;
  -webkit-user-select: none;
  user-select: none;
}
button { font: inherit; color: inherit; }
/* The focus ring, for keyboard and switch access: the same on every
   control, a light line 2px out from it between two dark bands (the shadow,
   out to 2px past the line, under the outline): dark, light, dark. No one
   colour stands 3:1 apart from both the darkest case-file look's folder and
   the corkboard, so the ring has two, 15.8:1 apart, and the light line
   always has dark on both sides: it shows on any fill and any background
   (an ink-filled choice on manila, brass, the corkboard, every look). The
   bands take the place of a control's own shadow while it has focus. Board
   squares, always on the dark board, keep their own shadows (the selection,
   the hinted square) and take the light line alone; a lit clue keeps its
   highlight as the inner band. A control whose outer ring would be clipped
   draws it inset (outline-offset -4px, the bands inset shadows on each side
   of the line). tests/layout.test.js checks every focus rule against this. */
:root { --focus-ring: #f4ecd8; --focus-ring-edge: #141217; }
:focus-visible:not(.cell) { outline: 2px solid var(--focus-ring); outline-offset: 2px; box-shadow: 0 0 0 6px var(--focus-ring-edge) !important; }
.cell:focus-visible { outline: 2px solid var(--focus-ring); outline-offset: 2px; }
.hint-clue:focus-visible { box-shadow: 0 0 0 2px var(--highlight), 0 0 0 6px var(--focus-ring-edge) !important; }

/* The case view. It is a query container: rows that run out of room
   rearrange by the space they have, measured in rem, so they follow the
   text size as well as the screen (a media query can't see the text size).
   Outer spacing gives way to the content on a very narrow screen (the vw
   cap); text and tap targets never do.
   System bars: each edge keeps the larger of its spacing and the bar's
   inset (env() is 0 where there is no bar), never the two added, so a thin
   gesture strip leaves the spacing as it is. The case view spans the screen
   (Task 10): a bar at the side (a phone on its side) takes the room on its
   own edge only. Backgrounds and artwork run under the bars everywhere;
   only text and controls keep clear. */
.app {
  --app-edge: min(.5rem, 2.5vw);
  height: 100%;
  display: flex;
  flex-direction: column;
  padding: max(.625rem, env(safe-area-inset-top, 0px)) max(var(--app-edge), env(safe-area-inset-right, 0px))
    max(.625rem, env(safe-area-inset-bottom, 0px)) max(var(--app-edge), env(safe-area-inset-left, 0px));
  container: app / size;
  overflow: hidden auto; overscroll-behavior-y: contain;
}
/* Short for its text (large text, a short screen), the case view flows:
   the header, the tabs and the whole open page scroll as one, instead of
   squeezing the folder (clues.js shows every clue then, with no pager).
   .app is both the size container that decides it and the scroller it uses.
   Flow mode by layout (spec 005 Tasks 7 and 8):
   - One page at a time: the open page grows, and the view scrolls.
   - Split: it stacks (layout.js sets data-stacked, below the Split rules): no
     handle, the board's page, then the tabs and the clue page, each as tall
     as its content, one after the other in the scrolling view. It stacks
     too wherever it can't give the board its least frame and one whole clue.
   - Board first: already one long page; flowing changes nothing but who
     scrolls (the view, not the folder).
   - Clue drawer: no drawer either; the board, then the tabs and the clue
     page, as a stacked Split.
   - Side by side never flows (the tablet query asks for 560px and more).
   The hint card stays on screen while the view scrolls (.hint-dock), and
   opens within the part on screen (hints.js placeHint); so does the jump
   button that Board first and a stacked layout have (.jump-dock). */
.folder { --flow: 0; }
@container app (max-height: 32rem) {
  .app:not(.side-by-side) > .folder { --flow: 1; flex: none; }
  .app:not(.split-workspace) .page { position: relative; inset: auto; }
  /* the pages grow instead of scrolling, so a drag anywhere scrolls the view */
  .app:not(.side-by-side) #page-board, .app:not(.side-by-side) .page.scrolls { overflow: visible; }
  .split-workspace:not(.side-by-side) .folder { overflow: visible; }
}
/* A view that scrolls as a whole (the case view when it flows, the casebook
   and the case list, a profile that can't fit, the splash with large text)
   scrolls under the transparent system bars. A band in the desk colour
   covers a bar, exactly its height (none where there is no bar), while the
   view in front has content under it (screens.js watchStatusBar): the top band
   once the view is scrolled from the top, the bottom band until it is
   scrolled to the end (so also at rest at the top of a long list). Nothing
   then reads behind the clock and icons or the navigation buttons; with
   nothing under a bar, the backgrounds run under it, as on every screen.
   The bands sit over those views (z-index 8 to 14) and under How to play,
   the case file opening and the sheets. */
.bar-scrim {
  position: fixed; left: 0; right: 0; z-index: 15;
  background: var(--desk);
  opacity: 0; pointer-events: none;
}
.bar-scrim-top { top: 0; height: env(safe-area-inset-top, 0px); }
.bar-scrim-bottom { bottom: 0; height: env(safe-area-inset-bottom, 0px); }
.bar-scrim.shown { opacity: 1; }
@media (prefers-reduced-motion: no-preference) { .bar-scrim { transition: opacity 150ms ease-out; } }

/* ---------- header ---------- */
/* The title gives way first (its case line wraps); short of more room, the
   buttons move under it, on the right. */
/* The timer shares the title's line, at its right end (the title is
   shorter than the case line under it), so at scale 1 from 360px wide the
   title and the case line keep one line each beside the three buttons with
   any timer up to seven characters (1000:00, after 1,000 minutes on a
   case): the gaps and the case line's tracking are set for it (sizes stay
   as they are). Short of room, the timer takes a line of its own under the
   title, still on the right (spec 005 Task 8 Step 0). */
.top { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: .25rem .375rem; }
.brand { flex: 1 1 7rem; min-width: 0; }
.brand-line { display: flex; flex-wrap: wrap; align-items: baseline; column-gap: .25rem; }
.brand-line .timer { margin-left: auto; }
.brand h1 { margin: 0; font: 500 1.22rem/1.05 var(--display); letter-spacing: -0.01em; }
.brand h1 em { color: var(--brass); }
.case-line {
  margin: 3px 0 0; font-size: 0.75rem; font-weight: 500;
  text-transform: uppercase; letter-spacing: 0.08em; color: var(--desk-muted);
}
.top-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; align-items: center; gap: .25rem; margin-left: auto; }
/* Narrow for the text: Cases shows only its folder (its label stays as the
   accessible name), so the title and the case line keep one line each
   beside the three buttons (Cases, Layout and How to play); that takes
   27rem with the label. */
@container app (max-width: 27rem) {
  #new-btn { width: 2.75rem; padding: 0; }
  #new-btn .btn-label { display: none; }
  #new-btn .ui-icon { margin: 0; }
}
.timer { font: 400 0.95rem var(--type); font-variant-numeric: tabular-nums; white-space: nowrap; color: var(--desk-muted); }
.icon-btn {
  flex: none; white-space: nowrap; min-height: 2.75rem; padding: 0 .75rem; border-radius: 1.375rem; cursor: pointer;
  border: 1.5px solid rgba(239, 232, 216, 0.22); background: transparent;
  font-weight: 700; font-size: 0.85rem;
}
.icon-btn.round { width: 2.75rem; padding: 0; }
/* The web page's Get the app, a link to the store that looks like the
   header's buttons. */
a.icon-btn { display: inline-flex; align-items: center; text-decoration: none; color: inherit; }
.icon-btn.round > .ui-icon { margin: 0; width: 1.25rem; height: 1.25rem; vertical-align: -.3em; }

/* ---------- folder tabs ---------- */
/* One row of tabs. A narrow screen drops their icons first; past that the row
   scrolls sideways under an edge fade rather than shrinking the labels. The
   top padding holds the count badges, and the row (not each tab) overlaps
   the folder's top edge by a pixel, so nothing sticks out of the scroller. */
.tabs {
  display: flex; flex: none; gap: 2px; margin: .25rem 0 -1px; padding: .5rem 0 0 .25rem;
  position: relative; z-index: 2;
  overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: none;
}
.tabs::-webkit-scrollbar { display: none; }
/* An area that scrolls sideways fades out at each edge with more beyond it
   (dom.js watchScrollX sets data-more): tab rows, wide tables, the board. */
[data-more] { -webkit-mask-image: var(--more-fade); mask-image: var(--more-fade); }
[data-more="start"] { --more-fade: linear-gradient(to right, transparent, #000 1.5rem); }
[data-more="end"] { --more-fade: linear-gradient(to right, #000 calc(100% - 1.5rem), transparent); }
[data-more="start end"] { --more-fade: linear-gradient(to right, transparent, #000 1.5rem, #000 calc(100% - 1.5rem), transparent); }
.tab {
  position: relative; flex: 1 0 auto; min-height: 2.75rem;
  padding: .5rem .25rem;
  border: 1px solid var(--manila-edge); border-bottom: 0;
  border-radius: 5px 5px 0 0;
  background: var(--manila-back); color: var(--tab-ink);
  font-size: 0.8rem; font-weight: 700; letter-spacing: 0.01em;
  cursor: pointer; white-space: nowrap; overflow: visible;
  box-shadow: inset 0 -6px 8px -6px rgba(0, 0, 0, 0.25);
}
.tab[aria-selected="true"] {
  background: var(--manila); color: var(--ink);
  box-shadow: none; z-index: 1;
}
/* The row scrolls sideways, which clips anything outside it: the focus ring
   is drawn inside the tab, in the 4px of its side padding (.25rem at
   Standard), so it never runs under the label of a tab at its narrowest:
   a 1px dark band, the 2px light line, a 1px dark band. Wider padding
   would make the row scroll sideways at 280px wide. */
.tab:focus-visible { outline-offset: -4px; box-shadow: inset 0 0 0 4px var(--focus-ring-edge) !important; }
.tab .count {
  position: absolute; top: -.4375rem; right: .25rem; min-width: 1.25rem; padding: 0 .3125rem;
  border-radius: .625rem; background: var(--ink); color: var(--manila);
  font-size: 0.75rem; line-height: 1.25rem; font-variant-numeric: tabular-nums;
}
.tab .count:empty { display: none; }
.tab.has-hint::after {
  content: ''; position: absolute; top: .375rem; right: .4375rem;
  width: .4375rem; height: .4375rem; border-radius: 50%; background: var(--stamp);
}
@media (prefers-reduced-motion: no-preference) {
  .tab.has-hint::after { animation: pulseRing 1.2s infinite; }
}

.folder {
  position: relative; flex: 1; min-height: 0;
  background: var(--manila); color: var(--ink);
  border: 1px solid var(--noir-line); border-radius: 0 5px 5px 5px;
  box-shadow: var(--surface-shadow);
  overflow: hidden;
  perspective: 1400px;
  /* a sideways swipe turns the page (clues.js bindSwipe); two fingers zoom
     the whole app. Zoomed in, a sideways drag looks around it instead. */
  touch-action: pan-y pinch-zoom;
}
html[data-zoomed] .folder, html[data-zoomed] .app[data-layout="drawer"]:not([data-stacked]) #case-tabs { touch-action: manipulation; }
/* A page lays its sheets out to fit (clues.js layoutSheets). When even one item
   can't fit a sheet, the page shows everything and scrolls (.scrolls); the
   board page scrolls whenever it can't fit, so nothing is ever cut off. */
.page {
  --page-pad: min(.5rem, 2.5vw);
  position: absolute; inset: 0;
  padding: var(--page-pad);
  display: flex; flex-direction: column;
  overflow: hidden;
  background: var(--manila);
  backface-visibility: hidden;
  container: page / inline-size;
}
.page.scrolls, #page-board { overflow-y: auto; overscroll-behavior-y: contain; }
/* The clue pages never serve as the browser's scroll anchor. They fold in
   and their cards rise, and while a lit clue is aimed dom.js atRest holds
   those animations at their end, then plays them on. Chrome took an
   evidence note as its anchor, followed it as the page moved, and scrolled
   the long page 306px: Follow the thread left the lit note half below the
   fold (Task 9, found by the layout check). The board's page still anchors,
   so a status line that wraps keeps the room pad in place. */
#page-testimony, #page-evidence, #page-methods { overflow-anchor: none; }
@media (prefers-reduced-motion: no-preference) {
  .page.fold-right { animation: fold-right 320ms cubic-bezier(0.2, 0.7, 0.2, 1); transform-origin: right center; }
  .page.fold-left { animation: fold-left 320ms cubic-bezier(0.2, 0.7, 0.2, 1); transform-origin: left center; }
}
@keyframes fold-right { from { transform: rotateY(-75deg); opacity: 0.2; } to { transform: none; opacity: 1; } }
@keyframes fold-left { from { transform: rotateY(75deg); opacity: 0.2; } to { transform: none; opacity: 1; } }

.page-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 0 .5rem; margin: 2px 2px .625rem; }
.page-head h2 { margin: 0; font: italic 500 max(min(1.4rem, 1.4 * 1.25 * var(--base-root)), 1.25rem) var(--display); }
.page-tip { margin-left: auto; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.08em; color: var(--muted); text-align: right; text-wrap: balance; }
.page-tip.liar { color: var(--stamp); font-weight: 700; }

.ui-icon { display: inline-block; width: 1.125rem; height: 1.125rem; flex: none; vertical-align: -.25em; fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
.tool > .ui-icon, .icon-btn > .ui-icon { margin-right: .3125rem; }
.tab > .ui-icon { width: .9375rem; height: .9375rem; margin-right: .25rem; vertical-align: -.2em; }
/* A narrow case view keeps the tab labels and drops their icons. */
@container app (max-width: 21rem) { .tab > .ui-icon { display: none; } }
/* A case's stars: its rating within its level, filled up to the rating. */
.stars { display: inline-flex; gap: 1px; vertical-align: -0.12em; color: var(--brass); white-space: nowrap; }
.star { width: 0.95em; height: 0.95em; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linejoin: round; }
.star.on { fill: currentColor; }

/* ---------- board page ---------- */
/* The crime briefing: a newspaper clipping with the scene's photograph. */
.crime {
  flex: none; display: grid; grid-template-columns: 5.5rem minmax(0, 1fr); gap: .75rem; overflow: hidden;
  border-radius: var(--paper-radius); background: var(--newsprint); color: #1b1712;
  box-shadow: var(--paper-shadow);
}
.crime[hidden] { display: none; }
.briefing-visual { position: relative; overflow: hidden; align-self: stretch; background: var(--noir-2); color: var(--brass); }
.briefing-visual img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: 65% center; }
.briefing-visual .ui-icon { position: absolute; right: .25rem; bottom: .25rem; width: 1.5rem; height: 1.5rem; padding: 3px; background: var(--noir); border-radius: 2px; }
.briefing-copy { min-width: 0; padding: .625rem .625rem .625rem 0; }
/* Stacked, the photo is a banner over the copy, and the fold tab sits on the
   copy's corner, below the photo. */
@container page (max-width: 16rem) {
  .crime { grid-template-columns: minmax(0, 1fr); gap: 0; }
  .briefing-visual { height: 4rem; }
  .briefing-copy { padding: .625rem; }
}
.crime .paper { display: flex; flex-wrap: wrap; justify-content: space-between; column-gap: .5rem; margin-bottom: .3125rem; font: .75rem var(--type); letter-spacing: .04em; text-transform: uppercase; }
.crime h3 { margin: 0 0 2px; font: 600 max(min(1.28rem, 1.28 * 1.25 * var(--base-root)), 1.25rem)/1.12 var(--display); letter-spacing: -.02em; overflow-wrap: anywhere; }
.crime b { color: #8e2a20; font-weight: 700; }
.briefing-copy p { margin: .375rem 0 0; font: .75rem/1.3 var(--sans); }
/* The briefing folds from a tab in the clipping's top corner (the copy's,
   when the photo stacks above it); folded, the tab becomes a bar with where
   and when, wrapping rather than cutting off. */
.briefing-toggle {
  flex: none; width: 100%; min-height: 2.75rem; margin-bottom: .3125rem; padding: .25rem .5rem;
  border: 1px solid var(--noir-line); border-radius: var(--surface-radius); background: var(--noir-2); color: var(--noir-ink); cursor: pointer;
  text-align: left; font: .75rem/1.4 var(--type); white-space: normal; overflow-wrap: anywhere;
}
#briefing-toggle[aria-expanded="true"] {
  position: absolute; z-index: 2; top: var(--page-pad); right: var(--page-pad);
  width: 2.75rem; height: 2.75rem; margin: 0; padding: 0; border: 0;
  border-radius: 0 var(--paper-radius) 0 .625rem; background: var(--newsprint-fold); color: #1b1712;
  box-shadow: var(--fold-shadow);
  text-align: center; font: 700 1rem/1 var(--sans);
}
#briefing-toggle[aria-expanded="true"]:active { background: var(--newsprint); }
#briefing-toggle[aria-expanded="true"]:focus-visible { outline-offset: -4px; box-shadow: inset 0 0 0 6px var(--focus-ring-edge) !important; }
/* the clipping's text keeps clear of the tab */
.briefing-copy::before { content: ''; float: right; width: 2.25rem; height: 2.25rem; }
@container page (max-width: 16rem) {
  #briefing-toggle[aria-expanded="true"] { top: calc(var(--page-pad) + 4rem); border-radius: 0 0 0 .625rem; }
}
/* Above the board: the status row (the status line, with Accuse or Reveal),
   the same in every arrangement. */
.board-section-label { flex: none; margin-top: .25rem; min-width: 0; }

/* The board (spec 005 R3.1): one component that sizes itself to the area a
   layout gives it. A column of suspects, a row of hours, then a square per
   suspect and hour. The squares follow the area, the smaller of its width and
   its height, but never go below --sq-min (2.75rem, a tap target): short of
   room the board scrolls inside its own area (#board-scroll), with the
   portrait column pinned on the left and the hour row on top, rather than
   shrinking. Layouts decide only how big the frame is; nothing else sizes
   the board, its squares or its portraits.
   - The portrait column is 12/11 of a square (3rem beside 2.75rem squares),
     so the portraits and their plates grow and shrink with the squares.
   - The labels in the board (hours, hunch chips, plates) never go below a
     fixed share of the smallest square: on phones exactly R1.4's minimums,
     and on tablets, whose square floor is 44px × the scale (ruling R3a)
     beside a larger root, small enough to fit the square they label.
   - #page-board carries --n, the number of suspects (board.js), so the frame
     keeps at least the board's height at the smallest square (--board-floor),
     unless a layout lets it go to --board-least (Split's handle).
   - margin: auto centres a board smaller than its area and keeps every edge
     of a larger one reachable.
   - --ring: the room kept past the last column and row for a square's ring,
     which reaches that far out of the square (the rest is drawn inside it).
   - --fit, --sq and --col hold container units, which measure the board's
     area only where they're used outside any other size container: nothing
     inside the board but a portrait is one, and a portrait doesn't use them. */
.board-frame {
  --hour-h: 1.25rem; --gap: .125rem; --sq-min: 2.75rem; --ring: 1px;
  --board-floor: calc(var(--hour-h) + var(--n, 4) * (var(--sq-min) + var(--gap)) + var(--ring));
  /* the hour row and two rows of squares: the least frame a layout may give
     the board (--frame-min), which then pans inside it */
  --board-least: calc(var(--hour-h) + 2 * (var(--sq-min) + var(--gap)) + var(--ring));
  /* --frame-h: the height a layout gives the frame where the page doesn't
     (Board first: the board as wide as its page); otherwise the frame takes
     the page's room */
  position: relative; flex: 1 1 auto; height: var(--frame-h, auto); min-height: var(--frame-min, var(--board-floor)); margin-top: .125rem;
}
/* One under another (the stack), the frame is never taller than the board
   as wide as its page (100cqw: the page's width): what room is left goes
   below the room pad, not around the board, so the status row, the board,
   the tools and the pad stay together. */
#page-board:not([data-arrange]) > .board-frame {
  max-height: calc(var(--hour-h) + var(--n, 4) * (max(var(--sq-min), (100cqw - var(--n, 4) * var(--gap) - var(--ring)) / (var(--n, 4) + 12 / 11)) + var(--gap)) + var(--ring));
}
/* A sideways drag pans the board and goes no further (no page swipe); at the
   top or foot of a board that scrolls up and down, the next drag scrolls the
   page, so a case view that scrolls as a whole (flow mode) never traps it.
   No page anchors its scrolling on a square: a page scrolled part-way would
   otherwise scroll itself whenever a redraw moved that square. Two fingers
   are the web view's own pinch zoom of the whole app. */
#board-scroll {
  position: absolute; inset: 0;
  display: flex; container-type: size;
  overflow: auto; overscroll-behavior-x: contain; touch-action: pan-x pan-y pinch-zoom; overflow-anchor: none;
}
/* A board that pans up and down fades out under its hour row and at its
   foot wherever there is more of it (layout.js watchBoardPan sets
   data-more-y), so a board cut off by a short frame doesn't read as a case
   with fewer suspects. The fades never catch a tap. */
.board-frame[data-more-y~="top"]::before, .board-frame[data-more-y~="bottom"]::after {
  content: ''; position: absolute; left: 0; right: 0; height: 1.25rem; z-index: 1; pointer-events: none;
}
.board-frame[data-more-y~="top"]::before { top: var(--hour-h); background: linear-gradient(var(--noir), transparent); }
.board-frame[data-more-y~="bottom"]::after { bottom: 0; background: linear-gradient(transparent, var(--noir)); }
.timeline {
  --fit: max(var(--sq-min), min((100cqw - var(--n) * var(--gap) - var(--ring)) / (var(--n) + 12 / 11), (100cqh - var(--hour-h) - var(--n) * var(--gap) - var(--ring)) / var(--n)));
  --sq: var(--fit);
  --col: calc(var(--fit) * 12 / 11);
  flex: none; margin: auto; padding: 0 var(--ring) var(--ring) 0;
  display: grid; gap: var(--gap);
  grid-template-columns: var(--col) repeat(var(--n), var(--sq));
  grid-template-rows: var(--hour-h) repeat(var(--n), var(--sq));
}
.timeline > :first-child { position: sticky; left: 0; top: 0; z-index: 3; }
/* At rest the board sits on the desk, its look included. While it can scroll
   (layout.js watchBoardPan sets data-pans), it is a dark panel, and the pinned
   column, hour row and corner, in the same colour, hide the squares that
   pass under them. */
#board-scroll[data-pans] .timeline, #board-scroll[data-pans] .timeline > :first-child,
#board-scroll[data-pans] .hour, #board-scroll[data-pans] .suspect { background: var(--noir); }
.hour {
  position: sticky; top: 0; z-index: 2;
  display: grid; place-items: center; font: .75rem var(--type); color: var(--noir-muted); white-space: nowrap;
  font-size: clamp(calc(var(--sq-min) * 12 / 44), var(--sq) * .38, .75rem);
}
.hour.crime-hour { color: var(--stamp-on-dark); font-weight: 700; }
/* A suspect: the portrait with its name plate, filling the column, opening
   their dossier. The selected row's portrait is framed in brass and its plate
   lit (the plate component, below). */
.suspect { position: sticky; left: 0; z-index: 2; display: flex; min-width: 0; min-height: 0; }
.suspect button { display: block; width: 100%; height: 100%; padding: 0; border: 0; background: none; cursor: pointer; color: inherit; }
.suspect .mug { --mug-frame: var(--noir-line); width: 100%; height: 100%; }
.suspect.sel .mug { --mug-frame: var(--brass); }
.suspect .plate { font-size: calc(var(--sq-min) * 12 / 44); font-size: clamp(calc(var(--sq-min) * 12 / 44), min(22cqw, 27cqh), 1.1rem); }
.cell {
  position: relative; width: 100%; height: 100%; padding: 0; border: 0;
  display: grid; place-items: center; cursor: pointer;
  background: rgba(236, 228, 209, 0.045); border-radius: 3px;
  box-shadow: inset 0 0 0 1px var(--noir-line);
}
.cell.in-line { background: rgba(236, 228, 209, 0.09); }
.cell.crime-col { box-shadow: inset 0 0 0 1px rgba(224, 96, 90, 0.45); }
.cell.filled { background: transparent; }
/* A square's ring: 1px outside it (in the gap, or the --ring kept at the far
   edges) and the rest inside, around the room's tag (inset 2px), so no ring
   is ever cut off at the board's edge. */
.cell.same-room { box-shadow: 0 0 0 1px var(--noir-ink), inset 0 0 0 1px var(--noir-ink); }
.cell.bad { box-shadow: 0 0 0 1px var(--stamp-on-dark), inset 0 0 0 1px var(--stamp-on-dark); }
.cell.selected { box-shadow: 0 0 0 1px var(--brass), inset 0 0 0 2px var(--brass); z-index: 1; }
.cell.hint-focus { box-shadow: 0 0 0 1px var(--highlight), inset 0 0 0 2px var(--highlight), 0 0 16px rgba(242, 194, 48, 0.55); z-index: 1; }
.cell.hint-line:not(.filled) { background: rgba(242, 194, 48, 0.16); }
.cell.hint-wrong { box-shadow: 0 0 0 1px var(--stamp), inset 0 0 0 2px var(--stamp), 0 0 12px rgba(179, 55, 45, 0.6); z-index: 1; }
/* A filled square: the room's symbol on its colour, as on the picker. The
   symbol follows the square's size. */
.tag {
  position: absolute; inset: 2px; display: grid; place-items: center; border-radius: 3px;
  background: var(--room); color: var(--room-ink);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12), inset 0 -2px 0 rgba(0, 0, 0, 0.3);
}
.tag .ui-icon { width: 80%; height: 80%; stroke-width: 1.8; }
.cell.bad .tag { outline: 2px dashed var(--stamp); outline-offset: -2px; }
.cell.locked::after {
  content: ''; position: absolute; right: 3px; top: 3px; z-index: 1;
  width: .25rem; height: .25rem; border-radius: 50%; background: var(--room-ink);
}
/* Hunches: a chip per room, two to a row (three rows for six rooms), over
   the whole square, apart by a pixel. The chips follow the square's size,
   never below 0.625rem (R1.4), and each ink spaces its own hand
   (--note-tracking, --note-variation; cosmetics.css) so a room's three
   letters stay inside half a square, clear of the next chip and of each
   other in every ink (scripts/check-layout.cjs, the inks screen). (A square
   is no size container: --sq holds container units, which must keep measuring the
   board's area.) */
.notes {
  position: absolute; inset: 0;
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1px;
  place-items: center; align-content: center;
  font-size: clamp(calc(var(--sq-min) * 10 / 44), var(--sq) * .22, 1rem);
}
/* The board's edge fade at the start is drawn from the pinned column, in the
   board's colour, over the squares (and hours) passing under it, so the
   portraits never fade; the area's own mask fades only the end. Its first
   1rem is solid, so no sliver of a square or chip shows beside the column
   (a chip is cut there only once it is wide enough to read as passing
   under it). */
#board-scroll[data-more="start"] { -webkit-mask-image: none; mask-image: none; }
#board-scroll[data-more="start end"] { --more-fade: linear-gradient(to right, #000 calc(100% - 1.5rem), transparent); }
#board-scroll[data-more~="start"] .suspect::after,
#board-scroll[data-more~="start"] .timeline > :first-child::after {
  content: ''; position: absolute; left: 100%; top: 0; bottom: 0; width: 2rem;
  background: linear-gradient(to right, var(--noir) 1rem, transparent); pointer-events: none;
}
/* Without container units (iOS 15, ruling P5) the board fits at the floor:
   squares of --sq-min and the column beside them, scrolling in its area as it
   does when short of room. (The fit is replaced, not --sq: --col reads it
   too, and a container unit left in either voids the whole grid.) */
@supports not (container-type: size) {
  .timeline { --fit: var(--sq-min); }
}

.notes span {
  width: 100%; text-align: center; font: var(--note-font); letter-spacing: var(--note-tracking); font-variation-settings: var(--note-variation);
  white-space: nowrap; overflow-wrap: normal;
  background: var(--note); color: var(--note-ink); border-radius: 2px;
}
/* A room ruled out: the same chip, hollow and struck through. */
.notes span.out {
  background: none; box-shadow: inset 0 0 0 1px var(--note); opacity: 0.85;
  text-decoration-line: line-through; text-decoration-thickness: 2px; text-decoration-color: var(--stamp);
}
@media (prefers-reduced-motion: no-preference) {
  .cell.fresh .tag { animation: tagDrop 380ms cubic-bezier(0.2, 0.8, 0.3, 1.2); }
  .just-solved .cell.filled { animation: glow 1.2s ease-in-out 1; }
}
@keyframes glow { 50% { filter: brightness(1.15); } }

/* ---------- the board page's arrangements (spec 005 settled point 11, Task 10) ----------
   Where its page is wide for its height, the board grows instead of
   leaving room beside it. layout.js arrangeBoard lays the page out each way,
   measured (the choice compares how large the board comes out each way and
   whether the page then holds its controls, which depend on the heights of
   its content: no container query can decide that), keeps the way whose
   board is largest, and sets the widths and columns it worked out. A phone
   held upright is always the stack: the status row, the board, the tools
   and the room pad, one under another. Elsewhere:
   - tools-only: the tools beside the board (--side-w), the room pad across
     the page under both;
   - beside: the board down the page under the briefing, as wide as the
     board at that height (--board-w), and beside it the status row, the
     tools and the room pad.
   The status row (in .board-section-label) is above the board in every
   way.
   The pad's columns (--pad-arr-cols), the tools' (--tool-cols, their icons
   off where a column is narrow) and each room's face (data-face: its
   symbol beside its name, or above it where the column is narrow) follow
   the column they sit in. The open briefing's fold tab keeps its place in
   the page's top corner (it isn't placed in the grid). */
#page-board[data-arrange] { display: grid; grid-template-columns: minmax(0, 1fr); column-gap: .625rem; align-content: start; }
/* (Its children by name: a rule for every child would restyle all that is
   in them each time layout.js tries a way.) */
#page-board[data-arrange] > :is(.briefing-toggle, .crime, .board-section-label, .board-frame, .pad, .tools) { min-width: 0; }
/* The clipping's row is as tall as the clipping (max-content): it clips its
   own overflow, which would otherwise let an auto row shrink to nothing
   where the page is short. The rows beside the board end in 1fr, not
   minmax(0, 1fr), so they hold at least the board's frame at its least: a
   page too short for it scrolls rather than drawing the board over the
   rows below it. */
#page-board[data-arrange="tools-only"] {
  grid-template-columns: minmax(0, 1fr) var(--side-w, 6rem);
  grid-template-rows: auto max-content auto auto 1fr auto;
  grid-template-areas: "toggle toggle" "crime crime" "label label" "frame tools" "frame ." "pad pad";
}
#page-board[data-arrange="beside"] {
  grid-template-columns: var(--board-w, minmax(0, 1fr)) minmax(0, 1fr);
  grid-template-rows: auto max-content auto auto auto 1fr;
  grid-template-areas: "toggle toggle" "crime crime" "frame label" "frame tools" "frame pad" "frame .";
}
#page-board:is([data-arrange="tools-only"], [data-arrange="beside"]) > .briefing-toggle[aria-expanded="false"] { grid-area: toggle; }
#page-board:is([data-arrange="tools-only"], [data-arrange="beside"]) > .crime { grid-area: crime; }
#page-board:is([data-arrange="tools-only"], [data-arrange="beside"]) > .board-section-label { grid-area: label; }
#page-board:is([data-arrange="tools-only"], [data-arrange="beside"]) > .board-frame { grid-area: frame; }
#page-board:is([data-arrange="tools-only"], [data-arrange="beside"]) > .pad { grid-area: pad; }
#page-board:is([data-arrange="tools-only"], [data-arrange="beside"]) > .tools { grid-area: tools; }
#page-board[data-arrange] > .pad { grid-template-columns: repeat(var(--pad-arr-cols, 3), minmax(0, 1fr)); }
#page-board[data-arrange] > .tools { grid-template-columns: repeat(var(--tool-cols, 5), minmax(0, 1fr)); }
.tools[data-icons="off"] .ui-icon { display: none; }
/* A room whose column is narrow: its symbol above its name, as side by side
   sets them. */
.pad[data-face="stack"] .room-btn { flex-direction: column; justify-content: center; gap: 2px; padding: .3rem .15rem; text-align: center; }
/* Beside the board, the column's controls start at its top: the status
   (centred in its column) and the tools fill their column's width. */
#page-board[data-arrange="beside"] > .board-section-label { margin-top: 0; }

/* ---------- portraits with name plates (spec 005 R2) ---------- */
/* Each suspect's plate is a material that suits the character, with the
   name cut into it. Plate and lettering are at least 5:1 apart, and at least
   4.5:1 under every layer of the plate's finish that crosses the lettering
   (15% to 88% of the plate's height): tests/layout.test.js adds up each
   finish's layers there for every Engine.SUSPECTS name. So within that band
   a finish may go further only in the direction that strengthens the
   lettering (lightening behind dark letters, as the polished glint does, or
   darkening behind light ones). The direction that would wash the lettering
   out stays within about --plate-shade behind dark letters and --plate-glint
   behind light ones (Pearl's enamel gloss reaches a little past it at the
   band's top edge, and still holds 4.5:1). Bright highlights and deep
   shadows stay at the plate's top and bottom edges, and a dark plate's glint
   is a shadow (a light sweep across light lettering would wash it out). */
:root {
  --plate-ada: #d99a88;    --plate-ada-ink: #3b1a14;    /* rose gold */
  --plate-basil: #cdb66c;  --plate-basil-ink: #2f2507;  /* polished brass: paler and greener than gold */
  --plate-clara: #f1ead6;  --plate-clara-ink: #3d3526;  /* ivory enamel */
  --plate-dmitri: #efd682; --plate-dmitri-ink: #3b2c05; /* gilt: gold leaf, the lightest */
  --plate-esme: #c8845a;   --plate-esme-ink: #1f0c03;   /* copper */
  --plate-felix: #cfd3d8;  --plate-felix-ink: #262b31;  /* silver */
  --plate-greta: #2e2f33;  --plate-greta-ink: #d9dbe0;  /* blackened iron */
  --plate-hugo: #5c4228;   --plate-hugo-ink: #f1d3a0;   /* bronze: brown metal, polished lettering */
  --plate-iris: #79b5a3;   --plate-iris-ink: #0f2b25;   /* verdigris */
  --plate-jasper: #e3e7ec; --plate-jasper-ink: #1d232b; /* chrome */
  --plate-lena: #f4f6f8;   --plate-lena-ink: #1f3a68;   /* porcelain, navy lettering */
  --plate-monty: #d9a330;  --plate-monty-ink: #2a1c02;  /* gold: deep and warm */
  --plate-nora: #a3a9ad;   --plate-nora-ink: #15191c;   /* pewter */
  --plate-otto: #5b2a1e;   --plate-otto-ink: #f3dccb;   /* mahogany */
  --plate-pearl: #141217;  --plate-pearl-ink: #e2b84f;  /* black enamel, gold lettering */
  --plate-rufus: #d0b183;  --plate-rufus-ink: #2a1b0a;  /* light oak */
  --plate-glint: rgb(255 255 255 / .1);
  --plate-shade: rgb(0 0 0 / .1);
  /* the edge a plate is set into: dark, or the metal an enamel sits in */
  --plate-rim-dark: rgb(0 0 0 / .5);
  --plate-rim-gold: #a8823a;
  --plate-rim-silver: #8f99a6;
  /* finishes, laid over the plate's colour */
  --finish-polished: linear-gradient(105deg, transparent 30%, rgb(255 255 255 / .2) 46%, transparent 60%),
    linear-gradient(180deg, rgb(255 255 255 / .55), var(--plate-glint) 22%, transparent 48%, var(--plate-shade) 78%, var(--plate-shade) 88%, rgb(0 0 0 / .38));
  --finish-polished-dark: linear-gradient(105deg, transparent 30%, rgb(0 0 0 / .12) 46%, transparent 60%),
    linear-gradient(180deg, rgb(255 255 255 / .35), var(--plate-glint) 15%, transparent 48%, var(--plate-shade) 78%, rgb(0 0 0 / .4));
  --finish-mirror: linear-gradient(105deg, transparent 34%, rgb(255 255 255 / .3) 46%, transparent 56%),
    linear-gradient(180deg, rgb(255 255 255 / .8), var(--plate-glint) 24%, transparent 46%, var(--plate-shade) 50%, transparent 76%, rgb(0 0 0 / .3));
  --finish-enamel: linear-gradient(180deg, rgb(255 255 255 / .4), rgb(255 255 255 / .14) 17%, transparent 19%, transparent 82%, rgb(0 0 0 / .18));
  --finish-patina: radial-gradient(circle at 30% 62%, rgb(255 255 255 / .05) 0 .22em, transparent .5em),
    radial-gradient(circle at 63% 38%, rgb(0 0 0 / .05) 0 .3em, transparent .6em),
    radial-gradient(circle at 86% 66%, rgb(255 255 255 / .05) 0 .18em, transparent .42em),
    radial-gradient(circle at 14% 34%, rgb(0 0 0 / .05) 0 .2em, transparent .45em),
    linear-gradient(180deg, rgb(255 255 255 / .22), transparent 15%, transparent 88%, rgb(0 0 0 / .32));
  --finish-wood: repeating-linear-gradient(177deg, rgb(0 0 0 / .1) 0 .06em, transparent .06em .2em, rgb(255 255 255 / .08) .2em .26em, transparent .26em .41em),
    linear-gradient(180deg, rgb(255 255 255 / .24), transparent 15%, transparent 88%, rgb(0 0 0 / .26));
}
/* mug(): a suspect's portrait with the plate across its foot. The plate is
   live text (it scales, and follows a replay's names). Its size follows the
   portrait's (the smaller of 22% of its width and 27% of its height), never
   below 0.75rem; a portrait shorter than its plate needs keeps the plate's
   size, and the plate covers more of it. A portrait is a size container for
   that; nothing in it reads the board's --sq or --col. */
.mug { position: relative; display: block; flex: none; container-type: size; border-radius: var(--paper-radius); }
.mug > .portrait, .mug > .figure { display: block; width: 100%; height: 100%; object-fit: cover; object-position: center 24%; border-radius: inherit; }
.mug > .figure { object-fit: contain; object-position: center bottom; }
/* the photograph's edge */
.mug::after { content: ''; position: absolute; inset: 0; border: 1px solid var(--mug-frame, rgba(43, 36, 23, 0.22)); border-radius: inherit; pointer-events: none; }
/* The plate spans the portrait, short of its edges by up to .15em: less on a
   photo only just wide enough (3.8em, a six-letter name), never less than
   nothing. */
.plate {
  position: absolute; left: clamp(0px, (100% - 3.8em) / 2, .15em); right: clamp(0px, (100% - 3.8em) / 2, .15em); bottom: max(.15em, 4%);
  padding: .14em .22em .1em; text-indent: .02em;
  /* Karla, bold, in the name's own case: at a plate's size Bodoni's
     hairlines and small capitals (lowercase drawn at 70%) were too thin to
     read (owner, 2026-10-08, as with the book's small type) */
  font: 700 .75rem/1.1 var(--sans);
  font-size: clamp(.75rem, min(22cqw, 27cqh), 1.1rem);
  letter-spacing: .02em;
  text-align: center; white-space: nowrap;
  color: var(--plate-ink, #2b2417);
  background-color: var(--plate-base, #d6b25e);
  background-image: var(--plate-finish, none);
  border-radius: .12em;
  /* bevelled: lit along the top and left, in shadow along the foot and
     right; set into its rim, standing just off the photograph */
  box-shadow: inset 0 1px 0 rgb(255 255 255 / .5), inset 0 -1px 0 rgb(0 0 0 / .32),
    inset 1px 0 0 rgb(255 255 255 / .2), inset -1px 0 0 rgb(0 0 0 / .2),
    0 0 0 1px var(--plate-rim, var(--plate-rim-dark)), 0 1px 2px 1px rgb(0 0 0 / .45);
}
/* A portrait 7rem wide or more (the accusation's full-length figure) gets
   a plate sized to its name, centred, with a screw at each end; narrower
   ones (the dossier's print, the testimony card, the board) have no room for
   screws beside the longest names. */
@container (min-width: 7rem) {
  /* large enough for the letters to look cut in: light catches the lower
     wall of each stroke, the upper wall is in shadow (smaller, the shadow
     only blurred them) */
  .plate {
    left: 0; right: 0; width: fit-content; min-width: 62%; max-width: calc(100% - .3em); margin-inline: auto; padding-inline: 1.35em;
    text-shadow: 0 .06em 0 var(--plate-lip, rgb(255 255 255 / .55)), 0 -.04em 0 var(--plate-cut, rgb(0 0 0 / .25));
  }
  .plate::before, .plate::after {
    content: ''; position: absolute; top: 50%; width: .42em; height: .42em; margin-top: -.21em; border-radius: 50%;
    background: radial-gradient(circle at 35% 30%, rgb(255 255 255 / .7), transparent 50%),
      linear-gradient(135deg, transparent 42%, rgb(0 0 0 / .5) 42% 58%, transparent 58%), var(--plate-base, #d6b25e);
    box-shadow: inset 0 0 0 1px rgb(0 0 0 / .35), 0 1px 0 rgb(255 255 255 / .3);
  }
  .plate::before { left: .38em; }
  .plate::after { right: .38em; }
}
/* The selected board row: its plate's edge lit. */
.suspect.sel .plate {
  box-shadow: inset 0 1px 0 rgb(255 255 255 / .5), inset 0 -1px 0 rgb(0 0 0 / .32),
    inset 1px 0 0 rgb(255 255 255 / .2), inset -1px 0 0 rgb(0 0 0 / .2),
    0 0 0 1px var(--highlight), 0 0 .4rem .0625rem rgba(242, 194, 48, 0.6);
}
[data-suspect="ada"] { --plate-base: var(--plate-ada); --plate-ink: var(--plate-ada-ink); --plate-finish: var(--finish-polished); }
[data-suspect="basil"] { --plate-base: var(--plate-basil); --plate-ink: var(--plate-basil-ink); --plate-finish: var(--finish-polished); }
[data-suspect="clara"] { --plate-base: var(--plate-clara); --plate-ink: var(--plate-clara-ink); --plate-finish: var(--finish-enamel); --plate-rim: var(--plate-rim-gold); }
[data-suspect="dmitri"] { --plate-base: var(--plate-dmitri); --plate-ink: var(--plate-dmitri-ink); --plate-finish: var(--finish-polished); }
[data-suspect="esme"] { --plate-base: var(--plate-esme); --plate-ink: var(--plate-esme-ink); --plate-finish: var(--finish-polished); }
[data-suspect="felix"] { --plate-base: var(--plate-felix); --plate-ink: var(--plate-felix-ink); --plate-finish: var(--finish-polished); }
[data-suspect="greta"] { --plate-base: var(--plate-greta); --plate-ink: var(--plate-greta-ink); --plate-finish: var(--finish-patina); --plate-lip: rgb(255 255 255 / .1); --plate-cut: rgb(0 0 0 / .7); }
[data-suspect="hugo"] { --plate-base: var(--plate-hugo); --plate-ink: var(--plate-hugo-ink); --plate-finish: var(--finish-polished-dark); --plate-lip: rgb(255 255 255 / .1); --plate-cut: rgb(0 0 0 / .6); }
[data-suspect="iris"] { --plate-base: var(--plate-iris); --plate-ink: var(--plate-iris-ink); --plate-finish: var(--finish-patina); }
[data-suspect="jasper"] { --plate-base: var(--plate-jasper); --plate-ink: var(--plate-jasper-ink); --plate-finish: var(--finish-mirror); }
[data-suspect="lena"] { --plate-base: var(--plate-lena); --plate-ink: var(--plate-lena-ink); --plate-finish: var(--finish-enamel); --plate-rim: var(--plate-rim-silver); }
[data-suspect="monty"] { --plate-base: var(--plate-monty); --plate-ink: var(--plate-monty-ink); --plate-finish: var(--finish-polished); }
[data-suspect="nora"] { --plate-base: var(--plate-nora); --plate-ink: var(--plate-nora-ink); --plate-finish: var(--finish-polished); }
[data-suspect="otto"] { --plate-base: var(--plate-otto); --plate-ink: var(--plate-otto-ink); --plate-finish: var(--finish-wood); --plate-lip: rgb(255 255 255 / .1); --plate-cut: rgb(0 0 0 / .6); }
[data-suspect="pearl"] { --plate-base: var(--plate-pearl); --plate-ink: var(--plate-pearl-ink); --plate-finish: var(--finish-enamel); --plate-rim: var(--plate-rim-gold); --plate-lip: rgb(255 255 255 / .1); --plate-cut: rgb(0 0 0 / .8); }
[data-suspect="rufus"] { --plate-base: var(--plate-rufus); --plate-ink: var(--plate-rufus-ink); --plate-finish: var(--finish-wood); }

/* Reveal appears beside the status once a case closes with squares still
   empty, or under it where the status would get under 8rem beside it.
   Side by side they're centred together. The row's spacing (around it and
   between its lines) leaves room for the keyboard focus ring around Reveal,
   clear of the board above and the room pad's rule. */
/* The row keeps the room of the lines it can show (layout.js fitStatusRoom,
   Task 10 fix round 4): unseen copies of the tallest and widest of them
   (.status-sizer, with Reveal once the case is closed) share its one cell,
   and the line shown (.status-line) is laid over them, so no line sizes the
   row and none moves the board. The line sits at the row's top. */
.status-row { position: relative; display: grid; padding: .25rem .625rem; min-width: 0; }
.status-line, .status-sizer { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: .625rem .875rem; }
.status-line { position: absolute; inset: 0; padding: inherit; align-content: flex-start; min-width: 0; }
/* (With no min-width of 0, so the row's least width is the room's: its
   longest word or button.) */
.status-sizer { grid-area: 1 / 1; visibility: hidden; }
/* Unseen, a line needn't be balanced: balancing keeps the number of lines,
   so the room is the same, and it costs a search in every trial of the
   board's page. */
.status-sizer .status { text-wrap: wrap; }
.status {
  flex: 1 1 8rem; max-width: max-content; margin: 0; min-width: 0; min-height: 1.35em; font-size: 0.75rem; color: var(--noir-muted);
  text-align: center; text-wrap: balance;
}
.status.warn { color: var(--stamp-on-dark); }
.status.good { color: #7fcf9a; font-weight: 700; }
/* A case closed with squares still empty: reveal the rest. */
.reveal-btn {
  flex: none; min-height: 2.75rem; padding: 0 1rem; border: 0; border-radius: 1.375rem; cursor: pointer;
  background: transparent; color: var(--brass); box-shadow: inset 0 0 0 1px var(--brass);
  font-weight: 800; font-size: 0.85rem; letter-spacing: 0.02em;
}
@media (prefers-reduced-motion: no-preference) {
  .reveal-btn:not([hidden]) { animation: fadeUp 300ms ease-out; }
}

/* The room pad: three across (two for four rooms), fewer on a narrow page,
   and each room's symbol above its name where the name wouldn't fit beside
   it (data-face, set by layout.js arrangeNow as for a side column). */
.pad {
  display: grid; grid-template-columns: repeat(var(--pad-cols, 3), minmax(0, 1fr)); gap: .3125rem;
  margin-top: .5rem; padding-top: .625rem; border-top: 1px solid var(--noir-line);
}
@container page (max-width: 19rem) { .pad { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@container page (max-width: 12.5rem) { .pad { grid-template-columns: minmax(0, 1fr); } }
.room-btn {
  display: flex; align-items: center; gap: .3125rem; min-height: 2.75rem;
  padding: .3125rem; border-radius: 4px; cursor: pointer;
  border: 1px solid var(--noir-line); background: var(--noir-2); text-align: left;
  font-size: 0.76rem; font-weight: 600; line-height: 1.1; color: var(--noir-ink);
}
.room-btn i {
  display: grid; place-items: center; flex: none; width: 1.375rem; height: 1.375rem; border-radius: 4px; background: var(--room); color: var(--room-ink);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2);
}
.room-btn span { min-width: 0; hyphens: manual; }
.room-btn:active { transform: translateY(1px); }
.room-btn[aria-pressed='true'] { border-color: var(--brass); box-shadow: inset 0 0 0 1px var(--brass); }
.room-btn.ruled-out { border-color: var(--stamp); box-shadow: inset 0 0 0 1px var(--stamp); }
.room-btn.ruled-out span { text-decoration-line: line-through; text-decoration-thickness: 2px; text-decoration-color: var(--stamp); }
.room-btn.ruled-out i { opacity: 0.45; }

/* Hunch, Erase, Undo, Hint and Accuse: five across, as wide as one another
   where there is room and each as wide as its name where there is not
   (min-content), so the five fit one row on more phones; short of room for
   that, three across, then two. Accuse, the last, fills the rest of its row
   (--accuse-span; layout.js sets it with --tool-cols where the board's page is
   arranged). The widths are the tools' own: five need 15.75rem, three
   9.5rem. */
.tools { display: grid; grid-template-columns: repeat(5, minmax(min-content, 1fr)); gap: .375rem; margin-top: .375rem; }
.tool-accuse { grid-column: span var(--accuse-span, 1); }
/* Short of room beside their names (24.5rem for the five), every icon goes
   above its name, so one long name ("Accuse") never wraps alone. */
@container page (max-width: 24.5rem) { .tool > .ui-icon { display: block; margin: 0 auto .125rem; } }
@container page (max-width: 15.75rem) {
  .tools { grid-template-columns: repeat(3, minmax(min-content, 1fr)); }
  #page-board:not([data-arrange]) > .tools { --accuse-span: 2; }
}
@container page (max-width: 9.5rem) { .tools { grid-template-columns: repeat(2, minmax(min-content, 1fr)); } }
.tool {
  min-width: 2.75rem; min-height: 2.75rem; padding: 0 .25rem; border: 0; border-radius: 3px; cursor: pointer; background: transparent;
  font-weight: 700; font-size: 0.75rem; color: var(--noir-ink);
}
/* Only a tool that is on (Hunch) is filled. Hint is a plain tool like Erase
   and Undo, so it never looks pressed before it is (testers took the brass
   fill for a hint already on). */
.tool[aria-pressed="true"] { background: var(--brass); color: #1a1405; }
/* Accuse: in brass outline, dimmed until the board puts someone at the scene
   at the time of death, and again once the case is closed (board.js
   updateAccuse), so it is always in its place and its coming never moves
   the board. It pulses when it comes on. A complete, correct board lights
   it with a ring inside its edge (the sheet still opens only when the
   player presses it), clear of the tools beside it and of the keyboard
   focus ring outside it. The ring pulses three times, then holds. */
.tool-accuse { border: 1px solid var(--brass); color: var(--brass); }
.tool-accuse:disabled { opacity: .45; cursor: default; }
.tool-accuse.ready { position: relative; }
.tool-accuse.ready::after {
  content: ''; position: absolute; inset: .1875rem; pointer-events: none;
  border: 2px solid var(--highlight); border-radius: 2px;
}
@media (prefers-reduced-motion: no-preference) {
  .tool-accuse:enabled { animation: pulseRing 1.4s 2; }
  .tool-accuse.ready:enabled { animation: none; }
  .tool-accuse.ready::after { animation: pulseRing 1.6s ease-out 3; }
}

/* The hint card hangs from its dock, a line at the foot of the case view
   that stays there while a flowing view scrolls (sticky, as far up as the
   view's bottom padding). It lies over the case view where hints.js placeHint
   puts it: never over the square the hint points at, the room pad or the
   tools. placeHint sets its place and width, and its height when there is
   too little room (its words then scroll above its buttons). It rises into a place at the foot and
   drops into one at the top. */
.hint-dock { position: sticky; bottom: max(.625rem, env(safe-area-inset-bottom, 0px)); z-index: 6; flex: none; height: 0; }
/* A column: its words in a body that scrolls when the card is given less
   than its height, and its buttons in a row at its foot that always shows
   (.hint-actions, below). The card itself never scrolls. */
.hint-card {
  position: absolute; left: 0; right: 0; bottom: 0;
  display: flex; flex-direction: column; overflow: hidden;
  padding: .75rem min(.875rem, 4vw); border-radius: 10px;
  background: var(--card); color: var(--ink);
  border: 1px solid var(--manila-edge); border-top: 4px solid var(--highlight);
  box-shadow: 0 6px 24px rgba(0, 0, 0, 0.45);
}
@media (prefers-reduced-motion: no-preference) {
  .hint-card { animation: rise 180ms ease-out; }
  .hint-card[data-at="top"] { animation-name: drop; }
}
@keyframes rise { from { transform: translateY(20px); opacity: 0; } to { transform: none; opacity: 1; } }
@keyframes drop { from { transform: translateY(-20px); opacity: 0; } to { transform: none; opacity: 1; } }
.hint-body { flex: 0 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; }
/* Fades at its foot only while more of the words lie below (hints.js sets
   data-more-y as the card is placed and as the words scroll). */
.hint-body[data-more-y~="bottom"] { -webkit-mask-image: linear-gradient(#000 calc(100% - 1.25rem), transparent); mask-image: linear-gradient(#000 calc(100% - 1.25rem), transparent); }
.hint-kicker { margin: 0 0 .25rem; font-size: 0.75rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.12em; color: var(--muted); }
/* The method's chip moves to a line of its own as one pill when it can't
   follow the kicker. */
.hint-tech { display: inline-block; max-width: 100%; margin-left: .375rem; padding: 1px .5rem; border-radius: .5rem; background: var(--ink); color: var(--manila); letter-spacing: 0.06em; }
.hint-text { margin: 0; font-size: 0.98rem; font-weight: 700; line-height: 1.35; }
/* the words still to type, and the reasons until then, keep their room */
.hint-text .rest, .hint-reasons li.waiting { visibility: hidden; }
.hint-reasons { margin: .5rem 0 0; padding-left: 1.1em; display: grid; gap: .3125rem; font-size: 0.86rem; line-height: 1.4; }
.hint-reasons:empty { display: none; }
.hint-thread {
  display: flex; align-items: center; gap: .375rem; min-height: 2.75rem; margin-top: .25rem; padding: 0; border: 0; background: none; cursor: pointer; text-align: left;
  color: var(--stamp); font-weight: 700; font-size: 0.84rem;
}
.hint-thread .lead-line { flex: none; width: 1.125rem; border-top: 1.5px dashed var(--stamp); }
.caret {
  display: inline-block; width: 2px; height: 1em; margin-left: 1px; vertical-align: -2px; background: currentColor;
}
.caret.block { width: .4375rem; }
@media (prefers-reduced-motion: no-preference) {
  .caret { animation: caret 0.7s steps(1) infinite; }
  .hint-reasons li:not(.waiting) { animation: fadeUp 300ms ease-out both; animation-delay: calc(var(--i, 0) * 60ms); }
}
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
/* The card's buttons, its foot: always in sight below the words. They share
   a row in equal parts where each keeps its full target, its longest word
   and, in the full card, its label on one line; otherwise (hints.js placeHint,
   hint-wrap) the row wraps, each button as wide as its label on one line and
   never under a tap target (review round 3b). */
.hint-actions { flex: none; display: flex; flex-wrap: wrap; gap: .5rem; margin-top: .625rem; }
.hint-actions .btn { flex: 1 1 0; }
.hint-card.hint-wrap .hint-actions .btn { flex: 1 1 auto; min-width: 2.75rem; }
/* A card given less than its height (hints.js placeHint, hint-tight) keeps its
   buttons in one compact row, a label taking more than one line, where each
   still keeps its full target and its longest word. */
.hint-card.hint-tight { padding-block: .5rem; }
.hint-card.hint-tight .hint-actions { margin-top: .375rem; }
.hint-card.hint-tight .hint-actions .btn { padding-block: .25rem; line-height: 1.15; }
/* A strip too short for the kicker and the words both shows the words
   first, the kicker after them (hint-brief). */
.hint-card.hint-brief .hint-body { display: flex; flex-direction: column; }
.hint-card.hint-brief .hint-kicker { order: 1; margin: .375rem 0 0; }

/* The jump button (spec 005 Task 8): Board first, and a Split or drawer
   that stacks, are one long page, the board's page then the tabs and the
   clues. The button floats at the foot of the case view, on the right,
   hung from a dock that stays at the foot while the page scrolls (as the
   hint card's does), clear of the bottom bar. While the board is in sight
   it jumps to the clues, and once the board has scrolled away, back to the
   board (layout.js jump, at once when the player asks for less motion). It
   hides while it would lie over any control in the case view, a name plate
   or the hint card (ruling T9-6), and comes back once its spot is clear
   (layout.js updateJump, judged again when the page settles); the clue page
   ends with room for it, so the last clue scrolls clear of it. Under the
   hint card. */
.jump-dock { position: sticky; bottom: max(.625rem, env(safe-area-inset-bottom, 0px)); z-index: 5; flex: none; height: 0; }
.jump-btn {
  position: absolute; right: .5rem; bottom: .5rem;
  display: flex; align-items: center; gap: .375rem; min-width: 2.75rem; min-height: 2.75rem; padding: 0 1rem 0 .75rem;
  border: 0; border-radius: 1.375rem; background: var(--brass); color: #1a1405; cursor: pointer;
  font-weight: 800; font-size: .85rem; letter-spacing: .02em; white-space: nowrap;
  box-shadow: 0 4px 14px rgba(0, 0, 0, .45);
}
.jump-btn .ui-icon { margin: 0; width: 1.125rem; height: 1.125rem; stroke-width: 2.2; }
.jump-btn.blocked { visibility: hidden; opacity: 0; pointer-events: none; }
@media (prefers-reduced-motion: no-preference) {
  /* It fades in as it comes back, and hides at once: fading out, it would
     still show over the room pad or a tab passing beneath it. */
  .jump-btn { transition: opacity 150ms ease-out, visibility 0s linear 0s; }
  .jump-btn.blocked { transition: none; }
}
/* The room at the clue page's end: the button's top stands 3.875rem above
   the case view's foot (its dock's .625rem, its own .5rem and 2.75rem), and
   it keeps .25rem clear of what it would cover (layout.js updateJump). With a
   bottom bar the button stands above the bar's inset, so the room is
   anchored the same way the button is: the larger of the page's own padding
   and the inset, then the button's room (ruling T9-7). This is not the
   double count R6 forbids: the last clue has to clear a control that sits
   above the inset. Less, and the last card never scrolls clear (at 200%
   text, or with three-button navigation), so the button never shows on a
   page of cards. */
.app[data-jump] #page-testimony, .app[data-jump] #page-evidence, .app[data-jump] #page-methods { padding-bottom: calc(max(var(--page-pad), env(safe-area-inset-bottom, 0px)) + 4.125rem); }

/* ---------- clue pages ---------- */
.clues { list-style: none; margin: 0; padding: 0; display: grid; gap: .375rem; }
/* A wide page lays its cards out in columns rather than drawing each across
   it (Task 10): a statement or a method keeps a readable measure. One
   column on a phone held upright; clues.js layoutSheets splits the rows into
   sheets. */
.clues.testimony { grid-template-columns: repeat(auto-fill, minmax(min(100%, 22rem), 1fr)); align-items: start; }
.methods { grid-template-columns: repeat(auto-fill, minmax(min(100%, 16rem), 1fr)); align-items: start; }
.clues button {
  width: 100%; display: grid; gap: 1px; text-align: left; cursor: pointer;
  padding: .4375rem .6875rem .5rem; border-radius: 4px;
  border: 1px solid var(--manila-edge); background: var(--card); color: var(--ink);
  /* index card: a red rule under the top line */
  background-image: linear-gradient(transparent 0 22px, rgba(179, 55, 45, 0.35) 22px 23px, transparent 23px);
}
.testimony .said { min-height: 2.7em; }
.testimony .rest { visibility: hidden; }
.portrait {
  display: block; width: 100%; object-fit: cover; object-position: center 24%;
  background: var(--manila-back);
  box-shadow: inset 0 0 0 1px rgba(43, 36, 23, 0.16); border-radius: 2px;
}
@media (prefers-reduced-motion: no-preference) {
  .testimony.typing li { animation: fadeUp 300ms ease-out both; animation-delay: calc(var(--i, 0) * 60ms); }
}
.clues .said { font: 400 0.9rem/1.35 var(--type); }
.testimony .said::before { content: '\201C'; }
.testimony .said::after { content: '\201D'; }
.clues button.done { opacity: 0.45; }
.clues button.done .said { text-decoration: line-through; text-decoration-color: var(--stamp); }
.clues button.hint-clue { border-color: var(--highlight); box-shadow: 0 0 0 2px var(--highlight); }

/* sheets inside a tab when a list is longer than the page */
.pager { display: flex; align-items: center; gap: .25rem; font: 400 0.78rem var(--type); color: var(--muted); }
.pager:empty { display: none; }
.pager button {
  flex: none; width: 2.75rem; height: 2.75rem; border-radius: 50%; cursor: pointer;
  border: 1px solid var(--manila-edge); background: var(--card); color: var(--ink); font-weight: 800;
}
.pager button:disabled { opacity: 0.35; cursor: default; }
.page-foot { margin-top: auto; padding-top: .5rem; display: flex; justify-content: center; }
.sheet-list > li[hidden] { display: none !important; }
@media (prefers-reduced-motion: no-preference) {
  .sheet-list.fold-right { animation: fold-right 280ms cubic-bezier(0.2, 0.7, 0.2, 1); transform-origin: right center; }
  .sheet-list.fold-left { animation: fold-left 280ms cubic-bezier(0.2, 0.7, 0.2, 1); transform-origin: left center; }
}

/* ---------- methods page ---------- */
.methods-lead { margin: 0 2px .625rem; display: flex; flex-wrap: wrap; gap: .3125rem; align-items: center; }
.lead-label { width: 100%; font-size: 0.75rem; font-weight: 800; text-transform: uppercase; letter-spacing: 0.12em; color: var(--muted); }
.mchip { padding: 2px .5625rem; border-radius: .625rem; background: var(--card); border: 1px solid var(--manila-edge); font-size: 0.8rem; font-weight: 700; }
.lead-note { width: 100%; font-size: 0.78rem; color: var(--muted); margin-top: 2px; }
.methods { list-style: none; margin: 0; padding: 0; display: grid; gap: .3125rem; }
.methods li {
  display: grid; grid-template-columns: auto 1fr; gap: 2px .625rem; align-items: baseline;
  padding: .4375rem .625rem; border-radius: 4px; border: 1px solid var(--line); background: rgba(248, 243, 230, 0.5);
}
.methods li.needed { background: var(--card); border-color: var(--manila-edge); }
.methods li:not(.needed) { opacity: 0.55; }
.methods .level { font: 400 0.75rem var(--type); color: var(--muted); grid-row: span 2; }
.methods b { font-size: 0.9rem; }
.methods p { margin: 0; font-size: 0.8rem; line-height: 1.35; color: var(--muted); }
.methods li.rules-item { display: block; background: transparent; border: 0; opacity: 1; padding: .25rem 2px; }
.rules h3 { margin: .375rem 2px .25rem; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.12em; color: var(--muted); font-weight: 700; }
.rules ul { margin: 0; padding-left: 1.2em; font-size: 0.84rem; color: var(--ink); }
.rules li { margin-bottom: 2px; }

/* ---------- sheets ---------- */
/* The sheet rises from the foot (or sits in the middle of a wide screen)
   within the space the system bars leave, and is never taller than it. */
.sheet-backdrop {
  --sheet-gap: min(1rem, 5vw);
  position: fixed; inset: 0; z-index: 20;
  background: rgba(6, 8, 16, 0.7);
  display: grid; grid-template-rows: minmax(0, 1fr); align-items: end; justify-items: center;
  padding: max(var(--sheet-gap), env(safe-area-inset-top, 0px)) max(var(--sheet-gap), env(safe-area-inset-right, 0px))
    max(var(--sheet-gap), env(safe-area-inset-bottom, 0px)) max(var(--sheet-gap), env(safe-area-inset-left, 0px));
}
@media (min-width: 640px) { .sheet-backdrop { align-items: center; } }
@media (prefers-reduced-motion: no-preference) {
  .sheet-backdrop { animation: fadeIn 200ms ease-out; }
  .sheet { animation: sheetUp 320ms cubic-bezier(0.2, 0.8, 0.2, 1); }
}
.sheet {
  width: 100%; max-height: 100%; overflow: auto; container-type: inline-size;
  background: var(--manila); color: var(--ink);
  border: 1px solid var(--manila-edge); border-radius: 14px; padding: min(1.25rem, 6.5vw);
}
.sheet h2 { margin: 0 0 .625rem; font: 500 max(min(1.55rem, 1.55 * 1.25 * var(--base-root)), 1.25rem)/1.15 var(--display); }
.sheet p { margin: 0 0 .75rem; }
.sheet ol { margin: 0 0 .75rem; padding-left: 1.2em; }
.sheet li { margin-bottom: .3125rem; }
.sheet .row { display: grid; gap: .5rem; margin-top: .5rem; }
/* A sheet, or a step of How to play, in two columns (Task 10; fix round 1,
   I4): only where one column would run past a readable measure (two
   columns of half the measure, less half the gap, need the measure's
   width) and the whole of it then fits the sheet without scrolling, since a
   sheet in columns that scrolls is read down, up and down again: dom.js
   flowColumns marks it data-cols="2" only then. Each block is kept whole
   and a heading with what follows it; a head of its own (a dossier's, the
   case file) and the figures (their row of four rearranges by the sheet's
   width) run across both. Settings lays its sections out as cards, as many
   to a row as fit, and the Layout sheet its four layouts two by two, never
   in columns. */
:is(#sheet-body, .closed-body, .ob-page)[data-cols="2"] { columns: calc((var(--measure) - 1.5em) / 2) 2; column-gap: 1.5em; }
:is(#sheet-body, .closed-body, .ob-page)[data-cols="2"] > * { break-inside: avoid; }
:is(#sheet-body, .closed-body)[data-cols="2"] > :is(h2, h3, .label) { break-after: avoid; }
#sheet-body[data-cols="2"] > :is(.dossier-head, .stats), .closed-body[data-cols="2"] > :is(.closed-file, .stats), .ob-page[data-cols="2"] > h2 { column-span: all; }
.sheet-backdrop[data-kind="settings"] #sheet-body { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 18rem), 1fr)); column-gap: 1.75rem; align-items: start; }
.sheet-backdrop[data-kind="settings"] #sheet-body > * { min-width: 0; }
.sheet-backdrop[data-kind="settings"] #sheet-body > :not(.set-block) { grid-column: 1 / -1; }
.sheet-backdrop[data-kind="settings"] .set-block { container-type: inline-size; }
/* Two buttons side by side, one above the other where their labels don't fit. */
.sheet .row.two { grid-template-columns: repeat(auto-fit, minmax(min(100%, 9rem), 1fr)); }
/* The side spacing gives way on a very narrow screen, so a long word in a
   label ("Keep investigating") stays whole at the largest text. */
.btn {
  min-height: 2.75rem; padding: .5rem min(.625rem, 2.5vw); border-radius: 8px; cursor: pointer;
  border: 1px solid var(--manila-edge); background: var(--card); color: var(--ink);
  font-weight: 700; font-size: 0.9rem;
}
.btn-primary { background: var(--ink); border-color: var(--ink); color: var(--manila); }
/* A link that looks like a button (the web page's store links). */
a.btn { display: flex; align-items: center; justify-content: center; text-align: center; text-decoration: none; }
/* The web page's message when today's case can't load (scripts/build-web.js). */
.web-error { max-width: 30rem; margin: 0 auto; padding: max(2rem, env(safe-area-inset-top, 0px)) max(1rem, env(safe-area-inset-right, 0px)) max(2rem, env(safe-area-inset-bottom, 0px)) max(1rem, env(safe-area-inset-left, 0px)); color: var(--manila); }
.web-error h1 { font: 500 1.75rem/1.2 var(--display); }
/* Shown only if the game couldn't start (app.js): the whole screen, over
   anything a part had opened, with a way to try again. The auto margins
   centre it and still let a tall one scroll from its top. */
.start-failed {
  position: fixed; inset: 0; z-index: 30; overflow: hidden auto;
  display: flex; flex-direction: column; align-items: flex-start; gap: 1rem;
  padding: max(2rem, env(safe-area-inset-top, 0px)) max(var(--page-edge), env(safe-area-inset-right, 0px)) max(2rem, env(safe-area-inset-bottom, 0px)) max(var(--page-edge), env(safe-area-inset-left, 0px));
  background: var(--noir); color: var(--manila);
}
.start-failed > :first-child { margin-top: auto; }
.start-failed > :last-child { margin-bottom: auto; }
.start-failed h1 { font: 500 1.75rem/1.2 var(--display); }
.start-failed h1 em { color: var(--brass); }
.start-failed p { max-width: var(--measure); margin: 0; }
.sheet .opt { display: flex; justify-content: space-between; align-items: center; gap: .625rem; text-align: left; }
.sheet .opt small { font-weight: 500; opacity: 0.75; font-size: 0.76rem; text-align: right; }
/* The store sheet: each plan of the Detective's Pass with its price and any
   trial, and the policy links, each a full-size tap target. */
.sheet .store-plan { flex-wrap: wrap; row-gap: .125rem; }
.sheet .store-plan small { opacity: 0.9; }
/* A product already owned, or one the store can't sell right now: flat, with
   a dashed edge, so it doesn't read as something to press. Its words keep
   their full contrast. */
.sheet [data-buy]:disabled { background: transparent; border-color: var(--manila-edge); border-style: dashed; color: var(--ink); cursor: default; }
.sheet .store-links a, .sheet .about-links a { display: inline-flex; align-items: center; min-height: 2.75rem; color: var(--ink); }
.stamp {
  display: inline-block; margin-bottom: .75rem; padding: 2px .75rem;
  border: 2.5px solid var(--stamp); color: var(--stamp); border-radius: 4px;
  font: 800 0.9rem var(--sans); letter-spacing: 0.2em; text-transform: uppercase; transform: rotate(-3deg);
}
/* Four figures in a row, or two by two where the labels wouldn't fit. */
.stats { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: .375rem; margin: .75rem 0; text-align: center; }
@container (max-width: 23.5rem) { .stats { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@container (max-width: 11rem) { .stats { grid-template-columns: minmax(0, 1fr); } }
.stats div { border: 1px solid var(--manila-edge); background: var(--card); border-radius: 8px; padding: .5rem .25rem; }
.stats b { display: block; font: 400 1.15rem var(--type); font-variant-numeric: tabular-nums; }
.stats span { font-size: 0.75rem; color: var(--muted); text-transform: uppercase; letter-spacing: 0.06em; }
.note { font-size: 0.88rem; color: var(--muted); }
.note.warn { color: var(--stamp); }
/* A wide table scrolls sideways in its .table-scroll, under an edge fade. */
.table-scroll { max-width: 100%; overflow-x: auto; overscroll-behavior-x: contain; margin: .25rem 0 .75rem; }
.table-scroll > table { margin: 0; }
.example { width: 100%; border-collapse: separate; border-spacing: 3px; margin: .25rem 0 .75rem; font-size: 0.75rem; }
.example td, .example th { text-align: center; padding: .25rem 0; border-radius: 5px; }
.example th { color: var(--muted); font: 400 0.75rem var(--type); }
.example td { background: var(--room); color: var(--room-ink); font-weight: 800; }
.example td.empty { background: rgba(43, 36, 23, 0.08); }

/* ---------- the board: a private eye's desk after midnight ---------- */
.tab.tab-noir { background: #2a262e; color: var(--noir-muted); border-color: #3a3440; }
.tab.tab-noir[aria-selected="true"] { background: var(--noir); color: var(--noir-ink); }
.page.noir { background: var(--noir); color: var(--noir-ink); }
.page.noir > * { position: relative; }

/* ---------- casebook: the start screen ---------- */
.casebook {
  position: fixed; inset: 0; z-index: 8; overflow-y: auto;
  background: var(--noir);
  background-image:
    radial-gradient(80% 45% at 80% 0%, var(--lamp), transparent 70%),
    repeating-linear-gradient(-14deg, rgba(255, 226, 170, 0.035) 0 16px, transparent 16px 40px);
  color: var(--noir-ink);
}
/* The view's side spacing, --inner-left and --inner-right: the larger of
   the spacing and a bar at that side. The view spans the screen (Task 10);
   the header uses them too, to run edge to edge. */
.casebook-inner {
  --inner-pad: var(--page-edge);
  --inner-left: max(var(--inner-pad), env(safe-area-inset-left, 0px));
  --inner-right: max(var(--inner-pad), env(safe-area-inset-right, 0px));
  padding: max(1.375rem, env(safe-area-inset-top, 0px)) var(--inner-right) max(1.25rem, env(safe-area-inset-bottom, 0px)) var(--inner-left);
  display: flex; flex-direction: column; gap: .75rem;
  container-type: inline-size;
}
.cb-featured { display: grid; gap: .5rem; }
/* A wide screen lays the casebook's cards out in columns (Task 10): the
   featured cases side by side, the case lists as many to a row as fit. */
#cb-featured { grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); align-items: start; }
/* The four levels in a row, or two by two where they don't fit. */
.cb-levels { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: .25rem; padding: .25rem; border-radius: 10px; background: var(--noir-2); border: 1px solid var(--noir-line); }
@container (max-width: 20rem) { .cb-levels { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
/* One to a row where two would break "Medium" (280px at the largest text). */
@container (max-width: 8.5rem) { .cb-levels { grid-template-columns: minmax(0, 1fr); } }
.cb-levels button {
  min-height: 2.75rem; padding: 0 .25rem; border: 0; border-radius: 7px; background: transparent; cursor: pointer;
  color: var(--noir-muted); font-weight: 700; font-size: 0.85rem;
}
.cb-levels button[aria-selected="true"] { background: var(--noir-ink); color: var(--noir); }
.cb-level-note { margin: -.25rem 2px 0; font-size: 0.8rem; color: var(--noir-muted); }
.cb-cases { display: grid; gap: .5rem; }
/* A link-styled button: underlined text with a full-height tap target. */
.cb-link { display: inline-flex; align-items: center; min-height: 2.75rem; background: none; border: 0; padding: 0 2px; cursor: pointer; color: var(--brass); font-weight: 700; font-size: 0.9rem; text-align: left; text-decoration: underline; text-underline-offset: 3px; }

/* ---------- motion ---------- */
@keyframes tagDrop {
  from { transform: translateY(-14px) rotate(-9deg) scale(1.12); opacity: 0; }
  60% { transform: translateY(2px) rotate(2deg) scale(0.98); opacity: 1; }
  to { transform: none; }
}
@keyframes slam {
  from { transform: scale(3.2) rotate(-14deg); opacity: 0; }
  55% { transform: scale(0.92) rotate(-3deg); opacity: 1; }
  72% { transform: scale(1.05) rotate(-3deg); }
  to { transform: scale(1) rotate(-3deg); opacity: 1; }
}
@keyframes shake { 0%, 100% { transform: none; } 30% { transform: translate(2px, -1px); } 60% { transform: translate(-2px, 1px); } }
@keyframes drawString { from { stroke-dashoffset: 600; } to { stroke-dashoffset: 0; } }
/* fadeUp moves with `translate` so it composes with each item's own rotation or hover lift */
@keyframes fadeUp { from { opacity: 0; translate: 0 10px; } to { opacity: 1; translate: none; } }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
@keyframes sheetUp { from { transform: translateY(100%); } to { transform: none; } }
@keyframes pulseRing { from { box-shadow: 0 0 0 0 rgba(242, 194, 48, 0.6); } to { box-shadow: 0 0 0 12px rgba(242, 194, 48, 0); } }
@keyframes caret { 50% { opacity: 0; } }
@keyframes sweep { from { transform: rotate(0); } to { transform: rotate(360deg); } }
@keyframes sweepHour { from { transform: rotate(0); } to { transform: rotate(30deg); } }
@keyframes flicker { 0%, 92%, 95%, 100% { opacity: 1; } 93% { opacity: 0.72; } 97% { opacity: 0.85; } }
@keyframes blinds { 0%, 100% { transform: translateX(0); } 50% { transform: translateX(14px); } }
/* staggered entrances: set --i (and optionally --d) on each item */
@media (prefers-reduced-motion: no-preference) {
  .fade-up { animation: fadeUp 420ms ease-out both; animation-delay: calc(var(--d, 0ms) + var(--i, 0) * 60ms); }
}

.btn-brass {
  max-width: 100%; min-height: 3.25rem; padding: 0 min(1.375rem, 5vw); border: 0; border-radius: 10px; cursor: pointer;
  background: var(--brass); color: #1a1405; font-weight: 800; font-size: 1rem; letter-spacing: 0.02em;
}

/* ---------- splash: a living noir title sequence ---------- */
/* The title and buttons sit at the foot of the scene; on a short screen or
   with large text they scroll over it rather than shrinking. */
.splash {
  position: fixed; inset: 0; z-index: 14; overflow: hidden auto; isolation: isolate;
  display: flex; flex-direction: column; background: var(--noir); color: var(--noir-ink);
}
.splash-art { position: fixed; inset: 0 0 15%; overflow: hidden; pointer-events: none; }
.splash-art img { display: block; width: 100%; height: 100%; object-fit: cover; object-position: 53% 38%; }
.splash-art::after {
  content: ''; position: absolute; inset: 0;
  background: linear-gradient(180deg, rgba(20,18,23,.15) 25%, rgba(20,18,23,.25) 45%, var(--noir) 100%),
    radial-gradient(ellipse at 50% 35%, transparent 20%, rgba(20,18,23,.55) 100%);
}
.splash-mist { position: absolute; width: 160%; height: 45%; left: -30%; opacity: .45; }
.splash-mist-back {
  top: 33%; background: radial-gradient(ellipse at 22% 60%, var(--mist) 0%, transparent 48%), radial-gradient(ellipse at 74% 70%, var(--mist) 0%, transparent 44%);
}
.splash-mist-front {
  top: 53%; background: radial-gradient(ellipse at 64% 50%, var(--mist) 0%, transparent 52%), radial-gradient(ellipse at 14% 65%, var(--mist) 0%, transparent 40%);
}
.splash-rain { position: absolute; inset: -540px -60px; background-image: url('../assets/rain.svg'); }
.splash-rain-back { --rain-travel: 360px; background-size: 240px 360px; opacity: .15; }
.splash-rain-front { --rain-travel: 540px; background-size: 360px 540px; opacity: .24; }
/* The scene runs under the system bars; the title, the buttons and Pause
   motion keep clear of them. The title's top spacing shows only when it
   reaches the top (large text, a short screen). */
.splash-title {
  position: relative; display: flex; flex-direction: column; align-items: center; margin: auto 0 0; text-align: center;
  padding: max(1rem, env(safe-area-inset-top, 0px)) max(1rem, env(safe-area-inset-right, 0px)) 1.75rem max(1rem, env(safe-area-inset-left, 0px));
}
.splash-icon { width: 4.25rem; height: 4.25rem; border-radius: 50%; box-shadow: 0 5px 24px rgba(0,0,0,.5); }
.splash h1 { margin: 1rem 0 0; font: 500 clamp(2.25rem, min(12vw, 9svh), 4.4rem)/.95 var(--display); letter-spacing: -.025em; text-shadow: 0 3px 24px var(--noir); }
.splash h1 span, .splash h1 em { display: block; }
.splash h1 em { color: var(--brass); margin-top: .25rem; }
.splash-title p { margin: 1.125rem 0 0; font: 400 .85rem var(--type); color: var(--noir-ink); }
.splash-actions {
  position: relative; width: min(100%, 30rem); margin: 0 auto; display: grid; gap: .5rem;
  padding: 0 max(1.5rem, env(safe-area-inset-right, 0px)) max(1.5rem, env(safe-area-inset-bottom, 0px)) max(1.5rem, env(safe-area-inset-left, 0px));
}
.splash-actions .cb-link { min-height: 2.75rem; }
.splash-motion {
  position: absolute; z-index: 1; top: max(1rem, env(safe-area-inset-top, 0px)); right: max(1rem, env(safe-area-inset-right, 0px));
  min-height: 2.75rem; padding: .5rem .875rem; border: 1px solid var(--noir-line); border-radius: 24px;
  background: rgba(20,18,23,.72); color: var(--noir-ink); font: .75rem var(--type); cursor: pointer;
}
@media (orientation: landscape) and (max-height: 540px) {
  .splash-art { inset: 0; }
  .splash-title { width: 55%; padding-bottom: .75rem; }
  .splash-icon { display: none; }
  .splash-actions { width: 55%; margin-left: 0; }
  .splash-motion { right: max(1rem, env(safe-area-inset-right, 0px)); }
}
@media (prefers-reduced-motion: no-preference) {
  .splash:not([hidden]):not(.motion-paused) .splash-art img { animation: manorDrift 22s ease-in-out infinite alternate; }
  .splash:not([hidden]):not(.motion-paused) .splash-mist-back { animation: mistDrift 17s ease-in-out infinite alternate; }
  .splash:not([hidden]):not(.motion-paused) .splash-mist-front { animation: mistDrift 12s -5s ease-in-out infinite alternate-reverse; }
  .splash:not([hidden]):not(.motion-paused) .splash-rain-back { animation: rainFall 1.4s linear infinite; }
  .splash:not([hidden]):not(.motion-paused) .splash-rain-front { animation: rainFall .85s linear infinite; }
  .splash:not([hidden]):not(.motion-paused) .splash-icon { animation: titleDevelop 1000ms 150ms ease-out both; }
  .splash:not([hidden]):not(.motion-paused) h1 span { animation: titleDevelop 1400ms 300ms ease-out both; }
  .splash:not([hidden]):not(.motion-paused) h1 em { animation: titleDevelop 1400ms 650ms ease-out both; }
  .splash:not([hidden]):not(.motion-paused) .splash-title p { animation: titleDevelop 1200ms 1100ms ease-out both; }
  @keyframes manorDrift { from { transform: scale(1.025) translateX(-.5%); } to { transform: scale(1.12) translateX(.5%); } }
  @keyframes mistDrift { from { transform: translateX(-8%); opacity: .25; } to { transform: translateX(8%); opacity: .55; } }
  @keyframes rainFall { from { transform: translate(0, 0); } to { transform: translateY(var(--rain-travel)); } }
  @keyframes titleDevelop { from { opacity: 0; transform: translateY(12px); filter: blur(3px); } to { opacity: 1; transform: none; filter: blur(0); } }
}
@media (prefers-reduced-motion: reduce) { .splash-motion, .splash-rain, .splash-mist { display: none; } }

/* ---------- how to play ---------- */
.onboard {
  position: fixed; inset: 0; z-index: 16;
  display: flex; flex-direction: column;
  background: var(--noir);
  background-image: radial-gradient(90% 55% at 15% 0%, var(--lamp), transparent 70%);
  color: var(--noir-ink);
  /* the whole screen, clear of the system bars on every edge (Task 10) */
  padding: max(1.25rem, env(safe-area-inset-top, 0px)) max(var(--page-edge), env(safe-area-inset-right, 0px))
    max(1.5rem, env(safe-area-inset-bottom, 0px)) max(var(--page-edge), env(safe-area-inset-left, 0px));
  container: onboard / size;
}
@media (prefers-reduced-motion: no-preference) { .onboard { animation: fadeIn 300ms ease-out; } }
/* Inside the screen's safe edges: where the screen is tall enough, the card
   takes the room between its head and its foot and scrolls itself; on a
   short one (under 24rem: a phone on its side, a large text size), the
   card is as tall as its step and the head, the card and the foot scroll
   together, never under a system bar, with room inside for a focus ring
   (fix round 1, I4). */
.ob-scroll { flex: 1; min-height: 0; display: flex; flex-direction: column; }
@container onboard (max-height: 24rem) {
  .ob-scroll { overflow-y: auto; overscroll-behavior-y: contain; margin-inline: -.375rem; padding: .375rem; }
  .ob-scroll > .ob-card { flex: none; overflow: visible; }
}
.ob-head { display: flex; justify-content: space-between; align-items: center; gap: .75rem; padding: 0 .25rem; }
.ob-step { font: 500 0.75rem var(--sans); letter-spacing: 0.12em; text-transform: uppercase; color: var(--noir-muted); }
.ob-skip { flex: none; border: 0; background: none; color: var(--brass); font-weight: 700; cursor: pointer; min-height: 2.75rem; min-width: 2.75rem; }
.ob-card {
  position: relative; flex: 1; min-height: 0; margin-top: .875rem; overflow: auto;
  background: var(--manila); color: var(--ink);
  border: 1px solid var(--manila-edge); border-radius: 12px; padding: min(1.375rem, 6vw) min(1.25rem, 6vw);
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.45);
  perspective: 1400px;
  container: ob / inline-size;
}
.ob-card h2 { margin: 0 0 .625rem; font: 500 max(min(1.7rem, 1.7 * 1.25 * var(--base-root)), 1.25rem)/1.1 var(--display); }
.ob-card p { margin: 0 0 .75rem; }
.ob-card .note { font-size: 0.9rem; }
.ob-card .example { border-spacing: 4px; font-size: 0.78rem; }
.ob-card .example td { padding: .5rem 0; }
.ob-card .example tr > th:first-child { text-align: right; font: 700 0.82rem var(--sans); color: var(--ink); }
.ob-card .example td.empty { color: var(--ink); box-shadow: 0 0 0 2px var(--highlight); }
.ob-card .sample-card {
  display: grid; grid-template-columns: 4.5rem minmax(0, 1fr); align-items: center; gap: .5rem .75rem; padding: .625rem;
  border-radius: var(--paper-radius); transform: rotate(-1deg); background: var(--card); box-shadow: var(--paper-shadow);
}
.ob-card .sample-card .said { font: .8rem/1.4 var(--type); }
/* narrow, the statement goes under the portrait, as on the testimony page */
@container ob (max-width: 14rem) { .ob-card .sample-card .said { grid-column: 1 / -1; } }
.ob-card .sample-note {
  margin: .625rem 0 .875rem; padding: .625rem .75rem; border-radius: 2px; transform: rotate(0.8deg);
  background: #efe3a8; font: 400 0.88rem/1.35 var(--type); box-shadow: 0 5px 12px rgba(43, 36, 23, 0.25);
}
.ob-card .stamp { margin-top: .625rem; }
.ob-page.fold-right { transform-origin: right center; }
/* (A wide step in two columns: with the sheets, above.) */
@media (prefers-reduced-motion: no-preference) {
  .ob-page.fold-right { animation: fold-right 320ms cubic-bezier(0.2, 0.7, 0.2, 1); }
  .ob-card .example td.empty { animation: pulseRing 1.4s infinite; }
  .ob-card .stamp { animation: slam 520ms 250ms cubic-bezier(0.2, 0.7, 0.3, 1) both; }
}
.ob-foot { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: .75rem; margin-top: 1rem; }
.ob-dots { display: flex; gap: .375rem; }
.ob-dots span { width: .5rem; height: .5rem; border-radius: 50%; background: rgba(236, 228, 209, 0.25); transition: background 0.2s; }
.ob-dots span.on { background: var(--brass); }
.ob-foot .btn-brass { min-height: 3rem; }

/* ---------- casebook: rank strip ---------- */
.rank {
  display: flex; align-items: center; gap: .75rem; padding: .625rem .75rem; border-radius: 10px;
  background: var(--noir-2); border: 1px solid var(--noir-line);
}
.badge {
  flex: none; width: 2.5rem; height: 2.5rem; border-radius: 50%; display: grid; place-items: center;
  border: 1.5px solid var(--brass); color: var(--brass); font: 700 1rem var(--display);
}
.rank-body { flex: 1; min-width: 0; }
/* The rank's name and the streak share a line, the streak wrapping under the
   name when they can't. */
.rank-top { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: baseline; gap: 0 .5rem; }
.rank-top b { font-size: 0.95rem; }
.rank-top span { font: 400 0.75rem var(--type); color: var(--noir-muted); }
.rank-bar { margin-top: .375rem; height: .25rem; border-radius: 2px; background: rgba(236, 228, 209, 0.1); overflow: hidden; }
.rank-bar i { display: block; height: 100%; background: var(--brass); transition: width 0.8s; }
.rank-next { margin-top: .25rem; font-size: 0.75rem; color: var(--noir-muted); }

/* ---------- casebook: case folders ---------- */
.cb-featured { gap: .875rem; }
.case-folder {
  position: relative; display: block; width: 100%; padding: 0; border: 0; background: none;
  text-align: left; cursor: pointer; color: var(--ink);
  transition: transform 0.18s ease-out;
}
.case-folder:hover, .case-folder:active { transform: translateY(-6px); }
.cb-featured .cb-link { justify-self: start; }
.cb-all { align-self: flex-start; }

/* ---------- casebook: every case for one level ---------- */
.caselist { z-index: 9; }
.cl-head { display: grid; gap: 2px; }
.cl-back { justify-self: start; padding-left: 0; }
.cl-head h2 { margin: 0; font: 500 max(min(2rem, 2 * 1.25 * var(--base-root)), 1.25rem)/1.1 var(--display); letter-spacing: -0.01em; }
.cl-head .cb-level-note { margin: .25rem 0 0; }
.cl-list { display: grid; gap: .875rem; }
.cl-more { height: 1px; }

/* ---------- evidence: corkboard ---------- */
/* Photos, then notes, then the clipping and the pager. A pin sits in the top
   margin of each paper (clues.js corkNodes), so every paper's top padding is
   at least a pin's height, and the gaps above each row leave room for the
   pins' tap targets. */
.cork {
  position: relative; flex: 1; min-height: 0; margin: 0 -.25rem -.25rem;
  display: flex; flex-direction: column; overflow: hidden; border-radius: 6px;
  padding: 1.25rem min(.75rem, 3vw) min(.75rem, 3vw);
  container: cork / inline-size;
  background: #9a7447;
  background-image:
    radial-gradient(circle at 20% 30%, rgba(60, 38, 16, 0.35) 0 2px, transparent 3px),
    radial-gradient(circle at 70% 60%, rgba(60, 38, 16, 0.3) 0 1.5px, transparent 2.5px),
    radial-gradient(circle at 45% 80%, rgba(255, 230, 190, 0.12) 0 2px, transparent 3px);
  background-size: 23px 19px, 17px 21px, 29px 27px;
  box-shadow: inset 0 0 0 6px #6b4a2a, inset 0 0 30px rgba(0, 0, 0, 0.4);
}
/* The photos: one row while every name plate fits (a plate needs a photo
   2.85rem wide, 3.1rem with the print's border), or two even rows (two by
   two for four suspects), then two to a row. clues.js sets data-n, the number
   of suspects. */
.cork-photos { flex: none; display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: 1rem .125rem; justify-items: center; }
@container cork (max-width: 12.775rem) { .cork-photos[data-n="4"] { grid-auto-flow: row; grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@container cork (max-width: 16rem) { .cork-photos[data-n="5"] { grid-auto-flow: row; grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@container cork (max-width: 19.225rem) { .cork-photos[data-n="6"] { grid-auto-flow: row; grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@container cork (max-width: 9.55rem) { .cork-photos[data-n] { grid-auto-flow: row; grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.polaroid {
  /* Packed in a row, a tilted photo leans into its neighbour at the foot,
     where the plate is; the tilt is kept small so no plate is covered. */
  --tilted: calc(var(--rot, 0deg) * var(--tilt, .5));
  width: min(100%, 4.5rem); min-width: 0; padding: .75rem .125rem .5rem; border: 0; cursor: pointer;
  background: #f4efe2; color: var(--ink); box-shadow: 0 4px 10px rgba(0, 0, 0, 0.4);
  transform: rotate(var(--tilted)); transform-origin: 50% 0;
}
.polaroid .mug { height: 4.125rem; }
/* The tip and Cut all share a row. Cut all keeps its place even while
   there is no string to cut, and clues.js sets the tip's height to its tallest
   message, so a tap never moves the notes under a finger. */
.cork-bar { flex: none; display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: .25rem .5rem; min-height: 2.75rem; margin: .75rem 0 .25rem; position: relative; z-index: 4; }
#cork-tip { flex: 1 1 0; min-width: min(100%, 8rem); display: flex; align-items: center; text-wrap: balance; }
#cork-tip:empty { visibility: hidden; }
.cork-chip {
  padding: 2px .5rem; border-radius: 3px; background: rgba(20, 18, 23, 0.72); color: var(--noir-ink);
  font: 400 0.75rem var(--type);
}
.cork-cut { flex: none; min-height: 2.75rem; border: 0; cursor: pointer; color: #e0605a; font: 700 0.75rem var(--sans); letter-spacing: 0.06em; text-transform: uppercase; }
.cork-cut.is-idle { visibility: hidden; }
/* Notes two to a row, or one where two won't fit (clues.js layoutCork reads
   the column count). */
.cork-notes {
  flex: 1; min-height: 0; list-style: none; margin: 0; padding: 1rem 0 0;
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem .625rem; align-content: start; justify-items: center;
}
@container cork (max-width: 15rem) { .cork-notes { grid-template-columns: minmax(0, 1fr); } }
/* Scroll mode: the board grows to hold every note, and the page scrolls. */
.page.scrolls .cork, .page.scrolls .cork-notes { flex: none; }
.cork-notes li { width: 100%; max-width: 9.25rem; }
.note-card {
  position: relative; z-index: 1; display: block; width: 100%; min-height: 5rem; padding: .875rem .625rem .625rem; border: 0; cursor: pointer;
  text-align: left; color: var(--ink); background: #efe3a8;
  font: 400 0.8rem/1.35 var(--type);
  box-shadow: 0 6px 14px rgba(0, 0, 0, 0.45);
  transform: rotate(var(--rot, 0deg)); transform-origin: 50% 0;
}
.cork-notes li:nth-child(even) .note-card { background: var(--card); }
.note-card.done { opacity: 0.5; }
.note-card.done span { text-decoration: line-through; text-decoration-color: var(--stamp); }
.note-card.hint-clue { box-shadow: 0 0 0 3px var(--highlight), 0 6px 14px rgba(0, 0, 0, 0.45); }
/* The clipping beside the pager, or above it where both don't fit. Where
   the clipping would be squeezed to a narrow column (under 8rem), the pager
   shows "2 of 3" without the word Sheet (still read out), so the clipping
   stays short and the notes keep the height. */
@container cork (max-width: 21rem) { .cork .pager-word { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } }
.cork-foot { flex: none; display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: center; gap: 1.25rem .375rem; padding-top: 1.25rem; }
.cork-clip {
  flex: 1 1 6rem; max-width: 10.625rem; padding: .75rem .5rem .5rem; text-align: center;
  background: var(--newsprint); color: #1b1712; box-shadow: 0 5px 12px rgba(0, 0, 0, 0.45);
  transform: rotate(1.2deg); transform-origin: 50% 0;
}
.cork-clip span { display: block; font: 700 0.75rem var(--sans); letter-spacing: 0.08em; text-transform: uppercase; border-bottom: 1px solid #1b1712; padding-bottom: 2px; }
.cork-clip b { display: block; margin-top: 3px; font: 800 0.8rem var(--display); text-transform: uppercase; }
.cork .pager { flex: none; position: relative; z-index: 4; padding: 3px .375rem; border-radius: 1.75rem; background: rgba(20, 18, 23, 0.72); color: var(--noir-ink); }
.cork-strings { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; z-index: 2; }
.cork-strings path { fill: none; stroke-linecap: round; }
.cork-strings .shadow { stroke: rgba(0, 0, 0, 0.35); stroke-width: 2.4; }
.cork-strings .string { stroke: var(--stamp); stroke-width: 1.9; }
.cork-strings .draft { fill: none; stroke: var(--highlight); stroke-width: 2; stroke-dasharray: 4 4; }
.cork-pins { position: absolute; inset: 0; pointer-events: none; z-index: 3; }
.pin {
  position: absolute; width: 2.75rem; height: 2.75rem; margin: -1.375rem 0 0 -1.375rem; padding: 0; border: 0;
  background: none; cursor: pointer; pointer-events: auto; display: grid; place-items: center;
}
.pin i {
  width: .75rem; height: .75rem; border-radius: 50%; background: #8e2a20;
  box-shadow: 0 1px 3px rgba(0, 0, 0, .4);
}
.pin.used i { background: var(--stamp); }
.pin.start i { background: var(--highlight); }
@media (prefers-reduced-motion: no-preference) {
  .pin.start i { animation: pulseRing 1s infinite; }
  .cork-strings .drawn { stroke-dasharray: 600; animation: drawString 500ms ease-out both; }
  .cork .polaroid, .cork .note-card { animation: fadeUp 360ms ease-out both; animation-delay: var(--d, 0ms); }
}

/* ---------- suspect dossier ---------- */
.dossier-head { display: flex; flex-wrap: wrap; gap: .875rem; }
.dossier-head > div { flex: 1 1 9rem; min-width: 0; }
.kicker { display: block; font: 700 0.75rem/1.4 var(--sans); letter-spacing: 0.12em; text-transform: uppercase; color: var(--stamp); }
.sheet .dossier-head h2 { margin: 2px 0 .375rem; }
.sheet .dossier-head p { margin: 0; font: 400 0.82rem/1.4 var(--type); }
.sheet .quote {
  margin: .875rem 0 0; padding: .5rem .6875rem; border-radius: 4px; border: 1px solid var(--manila-edge);
  background: var(--card); font: 400 0.9rem/1.35 var(--type);
}
.sheet h3.label { margin: .875rem 0 .375rem; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.12em; color: var(--muted); }
/* The whereabouts strip: an hour per column, scrolling sideways (with an
   edge fade) where the hours don't fit side by side. */
.where { display: grid; grid-template-columns: repeat(var(--n), minmax(max-content, 1fr)); gap: .25rem; overflow-x: auto; overscroll-behavior-x: contain; }
.where div { display: grid; gap: 3px; text-align: center; }
.where span { font: 400 0.75rem var(--type); color: var(--muted); }
.where span.crime-hour { color: var(--stamp); }
.where b { padding: .5rem 0; border-radius: 5px; background: var(--room, rgba(43, 36, 23, 0.08)); color: var(--room-ink); font-size: 0.78rem; font-weight: 800; }
.where b.empty { color: var(--muted); }

/* ---------- accuse: confirm the suspect the board names ---------- */
.accuse-suspect { display: flex; flex-wrap: wrap; gap: .875rem; align-items: center; margin: .625rem 0 .25rem; }
.accuse-suspect > div { flex: 1 1 10rem; min-width: 0; }
/* The full-length figure, its plate at its feet. A suspect cleared by a
   wrong accusation fades from the photograph; the plate stays legible. */
.accuse-suspect .mug { --mug-frame: rgba(236, 228, 209, 0.2); width: auto; height: clamp(9.375rem, 30vh, 16.25rem); aspect-ratio: 2 / 3; border-radius: 3px; }
.accuse-suspect .figure { background: var(--newsprint); }
.accuse-suspect.is-wrong .figure { opacity: 0.45; filter: grayscale(1); }
.sheet .accuse-suspect p { margin: 0 0 .625rem; }
.sheet .accuse-suspect p:last-child { margin-bottom: 0; }
.sheet .accuse-note { margin: .75rem 0 0; min-height: 1.3em; font-size: 0.88rem; color: var(--stamp); }
@media (prefers-reduced-motion: no-preference) {
  .accuse-note.shake { animation: shake 260ms ease-out; }
}

/* ---------- case closed ---------- */
.rank.ink { background: var(--ink); color: var(--manila); border: 0; border-radius: 8px; gap: .625rem; }
.rank.ink .badge { width: 2.125rem; height: 2.125rem; font-size: 0.9rem; }
.rank.ink .rank-top b { font-size: 0.82rem; }
.rank.ink .rank-top span { color: var(--brass); font-size: 0.75rem; }
.rank.ink .rank-bar { margin-top: .3125rem; background: rgba(234, 220, 184, 0.2); }
.rank.ink .rank-next { color: rgba(234, 220, 184, 0.7); }
@media (prefers-reduced-motion: no-preference) {
  .closed-body { animation: shake 240ms 480ms ease-out; }
  .closed-body .stamp { animation: slam 520ms 150ms cubic-bezier(0.2, 0.7, 0.3, 1) both; }
  .closed-body .stats div { animation: fadeUp 300ms ease-out both; animation-delay: calc(600ms + var(--i, 0) * 80ms); }
  .closed-body .rank { animation: fadeUp 300ms 920ms ease-out both; }
}

/* ---------- settings: Cloud Save, usage data, case history ---------- */
.set-block { padding: .75rem 0; border-top: 1px solid var(--line); }
.set-block:first-child { border-top: 0; padding-top: 0; }
.set-block p { margin: 0 0 .5rem; }
/* A link inside a sentence keeps a full-height tap target: its padding
   reaches above and below the line, and negative margins keep the line
   spacing of the paragraph. */
.set-link .cb-link { position: relative; display: inline-block; min-height: 2.75rem; padding-block: .75rem; margin-block: -.75rem; line-height: 1.25rem; vertical-align: baseline; }
/* Its focus ring is drawn around the words, not the tall tap area. */
.set-link .cb-link:focus-visible { outline: 0; box-shadow: none !important; }
.set-link .cb-link:focus-visible::after { content: ''; position: absolute; inset: .625rem -.1875rem; border: 2px solid var(--focus-ring-edge); outline: 2px solid var(--focus-ring); box-shadow: 0 0 0 4px var(--focus-ring-edge); border-radius: 2px; pointer-events: none; }
.set-link .nowrap { white-space: nowrap; }
/* Turning Cloud Save off asks first, in place of its buttons. */
.set-confirm { padding: .625rem .75rem; border-radius: 10px; background: var(--card); border: 1px solid var(--manila-edge); }
/* In a narrow Settings section (a small phone at a large text size) a
   question's card gives its side padding to the words, so none breaks
   mid-word: it keeps only a rule above and below. */
@container (max-width: 12rem) {
  .set-confirm { padding-inline: 0; border-inline: 0; border-radius: 0; background: none; }
}
/* The sync status sits beside its label, or on its own line just under it. */
.sync-head { display: flex; flex-wrap: wrap; justify-content: space-between; column-gap: .75rem; }
.sync-state { font-weight: 700; letter-spacing: 0.06em; color: var(--muted); }
.sync-state[data-state="synced"] { color: var(--good); }
.sync-state[data-state="error"] { color: var(--stamp); }
.toggle { display: flex; gap: .625rem; align-items: flex-start; min-height: 2.75rem; font-size: 0.88rem; cursor: pointer; }
.toggle input { margin-top: 3px; width: 1.125rem; height: 1.125rem; accent-color: var(--ink); flex: none; }
.cb-link.danger { color: var(--stamp); margin-top: .375rem; }
/* Settings → Delete your data: the choices in a row that wraps. */
.set-deletes { display: flex; flex-wrap: wrap; column-gap: 1.25rem; }
.set-deletes .cb-link.danger { margin-top: 0; }
/* Brass is for links on the desk. On a sheet's paper it measured 1.5:1, so a
   link there is in ink, still underlined. */
.sheet .cb-link:not(.danger) { color: var(--ink); }
/* Display: Text size and Case layout. Each group wraps rather than shrinks,
   and evenly, by the sheet's width (.sheet is the container): the three
   sizes in a row where each gets 5.5rem, else one to a row; the four
   layouts two by two where each gets 9.5rem, else one to a row. The choice
   in use is filled. */
.set-block .set-sub { margin: .75rem 0 .375rem; font-weight: 700; font-size: .88rem; }
.set-block .set-sub:first-of-type { margin-top: 0; }
.set-choices, .set-choices.set-layouts { display: grid; grid-template-columns: minmax(0, 1fr); gap: .5rem; margin-bottom: .5rem; }
@container (min-width: 17.5rem) { .set-choices { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@container (min-width: 19.5rem) { .set-choices.set-layouts { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.set-choice[aria-pressed="true"] { background: var(--ink); border-color: var(--ink); color: var(--manila); }

/* The manor gives way to a lamplit investigation desk. */
body { background-image: radial-gradient(110% 60% at 20% 0%, var(--lamp), transparent 70%); }
.casebook { background-image: linear-gradient(180deg, rgba(20,18,23,.7), var(--noir) 440px), url('../assets/manor-night.webp'); background-size: auto, cover; background-position: center, center top; background-attachment: scroll, fixed; }
.sheet-backdrop { background: rgba(10,9,12,.82); }
.cork .polaroid { border-radius: 1px; }
.file-transfer { position: fixed; z-index: 19; pointer-events: none; padding: 1.25rem; border: 1px solid var(--manila-edge); border-radius: 3px; background: var(--manila); color: var(--ink); box-shadow: 0 24px 50px rgba(0,0,0,.55); transform-origin: top center; overflow: hidden; }
.file-transfer span { display: block; border: 1px solid var(--manila-edge); padding: .5rem; background: var(--card); font: .78rem/1.4 var(--type); }
@media (prefers-reduced-motion: no-preference) {
  .casebook:not([hidden]) .case-folder { animation: archiveFile 650ms cubic-bezier(.2,.75,.2,1) both; animation-delay: min(var(--d, 0ms), 420ms); }
  .folder.file-arriving { animation: deskFile 850ms cubic-bezier(.2,.7,.2,1) both; transform-origin: center top; }
  .folder.file-arriving .crime { animation: newsDrop 900ms ease-out both; }
  .page:not([hidden]) .polaroid { animation: photoPin 750ms cubic-bezier(.2,.7,.2,1) both; animation-delay: var(--d, 0ms); }
  .page:not([hidden]) .note-card { animation: paperLay 550ms ease-out both; animation-delay: var(--d, 0ms); }
  .testimony button.done { animation: statementFiled 250ms ease-out; }
  .sheet-backdrop[data-kind='dossier']:not([hidden]) .sheet { animation: dossierLay 600ms cubic-bezier(.2,.75,.2,1) both; }
  .sheet-backdrop[data-kind='dossier']:not([hidden]) .portrait { animation: photoDevelop 1100ms ease-out both; }
  .sheet-backdrop[data-kind='dossier']:not([hidden]) .quote { animation: paperLay 550ms 160ms ease-out both; }
  /* The suspect develops into view; one wrongly named fades to grey and
     stays grey (an animation's end state outlasts the rules above). */
  .sheet-backdrop[data-kind^='accuse']:not([hidden]) .accuse-suspect:not(.is-wrong) .figure { animation: photoDevelop 900ms ease-out both; }
  .sheet-backdrop[data-kind^='accuse']:not([hidden]) .accuse-suspect.is-wrong .figure { animation: figureCleared 700ms ease-out both; }
  .sheet-backdrop[data-kind='closed']:not([hidden]) .sheet { animation: dossierLay 500ms ease-out both; }
  @keyframes archiveFile { from { opacity: 0; translate: 0 32px; rotate: -2deg; } to { opacity: 1; translate: 0; rotate: 0deg; } }
  @keyframes deskFile { from { opacity: .4; transform: perspective(1200px) rotateX(9deg) translateY(28px); } to { opacity: 1; transform: none; } }
  @keyframes newsDrop { from { opacity: 0; translate: 0 -20px; rotate: 3deg; } 65% { opacity: 1; translate: 0 2px; rotate: -.5deg; } to { opacity: 1; translate: 0; rotate: 0deg; } }
  @keyframes photoPin { from { opacity: 0; transform: translateY(-18px) rotate(calc(var(--tilted, 0deg) - 9deg)); } 65% { opacity: 1; transform: translateY(0) rotate(calc(var(--tilted, 0deg) + 3deg)); } to { opacity: 1; transform: rotate(var(--tilted, 0deg)); } }
  @keyframes paperLay { from { opacity: 0; translate: 12px 10px; rotate: 2deg; } to { opacity: 1; translate: 0; rotate: 0deg; } }
  @keyframes dossierLay { from { opacity: 0; transform: translate(24px, 50px) rotate(4deg); } to { opacity: 1; transform: none; } }
  @keyframes photoDevelop { from { filter: sepia(1) brightness(1.8); opacity: .25; } to { filter: none; opacity: 1; } }
  @keyframes statementFiled { 50% { translate: 3px 0; } }
  @keyframes figureCleared { from { filter: none; opacity: 1; } to { filter: grayscale(1); opacity: 0.45; } }
}
/* ---------- casebook and investigation: editorial hierarchy ---------- */
#casebook { background: var(--noir); container: casebook / size; }
#casebook .casebook-inner { gap: .875rem; padding-top: 0; padding-bottom: max(1.75rem, env(safe-area-inset-bottom, 0px)); }
/* The title, then Record and Menu, which move under it when they don't fit.
   The header runs the column's full width, under the status bar, and keeps
   its text clear of it. */
#casebook .cb-head {
  position: sticky; top: 0; z-index: 6; display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: .5rem .75rem;
  margin: 0 calc(-1 * var(--inner-right)) 0 calc(-1 * var(--inner-left));
  padding: max(1rem, env(safe-area-inset-top, 0px)) var(--inner-right) .875rem var(--inner-left);
  background: var(--noir); border-bottom: 1px solid var(--noir-line);
}
.cb-head h1 { margin: 0; font: 500 max(min(2rem, 2 * 1.25 * var(--base-root)), 1.25rem)/1 var(--display); letter-spacing: -0.02em; }
.cb-head h1 em { color: var(--brass); }
.cb-eyebrow { margin: 0 0 .3125rem; color: var(--brass); font: .75rem var(--type); letter-spacing: .16em; }
/* The casebook's header stays at the top as the cases scroll, unless the
   screen is short for its text: then it scrolls away with them (still
   positioned: the Menu's list hangs from it). */
@container casebook (max-height: 32rem) { #casebook .cb-head { position: relative; } }
/* Record and Menu sit beside the title, or together on a row under it,
   from the left (.cb-head spreads its two parts, and a part alone on a row
   starts it). Where even that row is too narrow, Menu drops its symbol and
   both buttons their side padding. */
.cb-head-actions { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; }
.cb-profile, .cb-menu summary { display: flex; align-items: center; justify-content: center; gap: .625rem; min-height: 2.75rem; padding: 0 .75rem; border: 1px solid var(--noir-line); background: var(--noir-2); border-radius: 5px; color: var(--noir-ink); font: .75rem var(--sans); cursor: pointer; }
@container (max-width: 9.25rem) {
  .cb-profile, .cb-menu summary { padding-inline: .5rem; }
  .cb-menu summary span { display: none; }
}
/* The Menu's list hangs from the header's right edge, just under it, so it
   stays on the screen wherever Record and Menu sit (beside the title or on a
   row under it). */
.cb-menu { position: static; }
.cb-menu summary { list-style: none; }
.cb-menu summary::-webkit-details-marker { display: none; }
.cb-menu nav { position: absolute; right: var(--inner-right); top: 100%; width: min(14rem, calc(100% - var(--inner-left) - var(--inner-right))); display: grid; padding: .5rem; background: var(--noir-2); border: 1px solid var(--noir-line); border-radius: 6px; box-shadow: 0 12px 30px rgba(0,0,0,.5); }
.cb-menu nav button { min-height: 3rem; padding: .625rem .75rem; border: 0; background: transparent; color: var(--noir-ink); text-align: left; cursor: pointer; font-size: .9rem; }
.cb-menu nav button:hover:not(:focus-visible) { background: var(--noir); outline: 1px solid var(--brass); }
.cb-menu nav button:focus-visible { background: var(--noir); }
/* The rank strip opens the profile: a full-height row to tap. */
#cb-rank { min-height: 2.75rem; padding: 0; background: transparent; border: 0; gap: .625rem; cursor: pointer; }
#cb-rank .badge { width: 1.75rem; height: 1.75rem; font-size: .8rem; }
#cb-rank .rank-top { flex-wrap: wrap; }
#cb-rank .rank-top b { font: .76rem var(--sans); }
#cb-rank .rank-top span { font-size: .75rem; }
#cb-rank .rank-next { display: none; }
#cb-rank .rank-bar { height: 2px; }
.cb-library-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0 .5rem; margin-top: .375rem; }
.cb-library-head h2 { margin: 0; font: 500 max(min(1.4rem, 1.4 * 1.25 * var(--base-root)), 1.25rem) var(--display); }
.cb-library-head .cb-link { font: .75rem var(--sans); text-decoration: none; }
.cb-library-note { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0 .625rem; }
.cb-library-note .cb-level-note { margin: 0; font-size: .75rem; }
.cb-library-note .cb-link { flex: none; font-size: .75rem; text-decoration: none; }
#cb-cases, .cl-list { display: grid; gap: .5rem; padding: 0; grid-template-columns: repeat(auto-fill, minmax(min(100%, 20rem), 1fr)); align-items: start; }
.casebook .case-folder { margin: 0; border: 1px solid var(--noir-line); border-radius: var(--surface-radius); overflow: hidden; background: var(--noir-2); color: var(--noir-ink); display: grid; grid-template-columns: 3rem minmax(0,1fr) 1.25rem; gap: .75rem; padding: .875rem; align-items: center; box-shadow: var(--surface-shadow); }
.casebook .case-folder:hover { transform: none; border-color: var(--brass); }
.case-emblem { position: relative; overflow: hidden; display: grid; place-items: center; width: 3rem; height: 3.75rem; border: 1px solid var(--noir-line); border-radius: var(--paper-radius); color: var(--brass); background: var(--noir); }
.case-emblem .ui-icon { width: 1.75rem; height: 1.75rem; stroke-width: 1; }
.case-emblem img { width: 100%; height: 100%; position: absolute; inset: 0; object-fit: cover; object-position: 66% center; }
.case-copy { display: grid; gap: .25rem; min-width: 0; }
.case-kicker { font: .75rem/1.4 var(--type); color: var(--brass); letter-spacing: .06em; text-transform: uppercase; }
.casebook .case-folder .f-label { display: block; padding: 0; border: 0; background: none; color: var(--noir-ink); font: 500 1.14rem/1.14 var(--display); overflow-wrap: anywhere; }
.case-location { font: .77rem/1.4 var(--sans); color: var(--noir-muted); }
.case-state { font: .75rem/1.4 var(--type); color: var(--noir-muted); }
.case-state.is-closed, .case-state.is-second { color: var(--brass); }
.case-arrow { color: var(--brass); font-size: 1.2rem; }
.casebook .case-feature { position: relative; isolation: isolate; grid-template-columns: 1fr; padding: min(1.375rem, 6vw); min-height: 14.25rem; background: var(--noir-2); border-color: var(--noir-line); }
.case-scene { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: 65% center; z-index: -2; }
.case-feature::before { content: ''; position: absolute; inset: 0; z-index: -1; background: linear-gradient(90deg, rgba(20,18,23,.94), rgba(20,18,23,.73) 47%, rgba(20,18,23,.24)), linear-gradient(0deg, rgba(20,18,23,.82), transparent 65%); }
.case-feature .case-emblem { position: absolute; right: 1.125rem; top: 1.125rem; width: 2.375rem; height: 2.875rem; background: rgba(20,18,23,.65); }
.case-feature .case-kicker { padding-right: 2.75rem; }
.casebook .case-feature .f-label { margin-block: .25rem; padding-right: 2.1875rem; font-size: max(min(clamp(1.65rem, 7vw, 2rem), 1.65 * 1.25 * var(--base-root)), 1.25rem); max-width: 20rem; }
.case-feature .case-location { color: var(--noir-ink); font: .77rem var(--type); }
.case-feature .case-state { color: var(--desk-ink); }
.case-cast { display: flex; flex-wrap: wrap; align-items: center; row-gap: .25rem; margin: .625rem 0 .5rem; }
.case-cast .portrait { width: 1.5625rem; height: 2rem; margin-right: -3px; border: 1px solid var(--noir); border-radius: 2px; }
.case-cast > span { margin-left: .75rem; color: var(--desk-ink); font: .75rem var(--sans); }
.case-feature .case-cta { display: flex; align-items: center; justify-content: space-between; gap: .5rem; border-top: 1px solid var(--noir-line); padding-top: .75rem; margin-top: 2px; font: 700 .77rem var(--sans); color: var(--brass); }
.case-feature .case-arrow { display: none; }
#cb-featured:has(.case-feature + .case-feature) .case-feature + .case-feature { min-height: 9.875rem; padding-block: 1rem; }
#cb-featured:has(.case-feature + .case-feature) .case-feature + .case-feature .case-cast { display: none; }
.case-row .case-emblem { width: 3.75rem; height: 4.25rem; }
.case-row .case-emblem .ui-icon { position: absolute; right: 2px; bottom: 2px; width: 1.25rem; height: 1.25rem; padding: 2px; background: var(--noir); border-radius: 2px; }
.casebook .case-row { grid-template-columns: 3.75rem minmax(0,1fr) 1rem; gap: .75rem; }
/* Narrow for its text: the room picture leads the card as a banner, and a
   featured case's emblem joins the text instead of sitting beside it. The
   arrow (or a locked case's lock) moves onto the picture's corner and the
   card's side spacing gives way, so the victim's name gets the full width
   ("Fairweather", the longest, fits whole at 280px and the largest text). */
@container (max-width: 16rem) {
  .casebook .case-row { grid-template-columns: minmax(0, 1fr); padding: min(.875rem, 4vw); }
  /* The arrow's chip at the picture's top right, the room's at its bottom
     right: their right edges line up (both .25rem inside the picture's
     1px border), with at least .375rem between them. */
  .casebook .case-row .case-emblem { grid-area: 1 / 1 / 2 / -1; width: 100%; height: 4.125rem; }
  .casebook .case-row .case-emblem .ui-icon { right: .25rem; bottom: .25rem; }
  .casebook .case-row .case-arrow {
    position: relative; z-index: 1; grid-area: 1 / 1; justify-self: end; align-self: start;
    display: grid; place-items: center; min-width: 1.75rem; min-height: 1.75rem; margin: calc(.25rem + 1px) calc(.25rem + 1px) 0 0;
    border-radius: var(--paper-radius); background: rgba(20, 18, 23, .72);
  }
  .case-feature .case-emblem { position: static; }
  .case-feature .case-kicker, .casebook .case-feature .f-label { padding-right: 0; }
}
/* Narrower still (280px at the largest text), a featured card's spacing
   gives way too: its name is larger (never under 1.25rem), and
   "Fairweather" must fit whole. */
@container (max-width: 8.5rem) { .casebook .case-feature { padding: min(1.375rem, 3vw); } }
.case-progress { display: block; height: 3px; background: var(--noir-line); margin-block: .25rem; border-radius: 2px; overflow: hidden; }
.case-progress i { display: block; height: 100%; background: var(--brass); }
.record-note { margin-top: 1rem !important; }
.where-room { display: grid; place-items: center; }
.where-room .ui-icon { width: 1.375rem; height: 1.375rem; }
.tutorial-room-key { font-size: .75rem; }
/* Statements sit on the same charcoal desk as the board. */
#page-testimony { background: var(--noir); color: var(--noir-ink); }
#page-testimony .page-head { padding-bottom: .625rem; border-bottom: 1px solid var(--noir-line); }
#page-testimony .page-head h2 { font-style: normal; }
#page-testimony .page-tip { color: var(--noir-muted); font: .75rem var(--type); letter-spacing: .03em; }
#page-testimony .page-tip.liar { color: var(--brass); }
#page-testimony .pager { color: var(--noir-muted); }
/* A statement: the speaker's portrait, named on its plate (lettering about
   the size the card's name heading had), beside what they said; on a narrow
   page the statement goes under the portrait. How to play's sample card is
   drawn the same way. */
.testimony button { grid-template-columns: 4.5rem minmax(0, 1fr); align-items: center; padding: .625rem; gap: .5rem .75rem; background: var(--card); border: 0; border-radius: var(--paper-radius); box-shadow: var(--paper-shadow); }
.testimony .mug, .ob-card .sample-card .mug { width: 4.5rem; height: 5.625rem; }
.testimony .said { font: .8rem/1.4 var(--type); }
.testimony button.done { opacity: .58; }
@container page (max-width: 14rem) { .testimony .said { grid-column: 1 / -1; } }
/* A dossier is a photograph, a statement and a working whereabouts strip. */
.sheet-backdrop[data-kind='dossier'] .sheet { --dossier-pad: min(1.125rem, 6vw); padding: 0 var(--dossier-pad) var(--dossier-pad); overflow: auto; }
.dossier-head { margin: 0 calc(-1 * var(--dossier-pad)); padding: 1.25rem var(--dossier-pad); background: var(--noir-2); color: var(--noir-ink); align-items: center; border-bottom: 3px solid var(--brass); }
/* the photograph, printed with a border, its plate on it; the name stays the heading */
.dossier-head .mug { width: 5.625rem; height: 7rem; transform: rotate(-3deg); border: 4px solid var(--newsprint); box-shadow: 0 6px 16px rgba(0,0,0,.45); }
.dossier-head .kicker { color: var(--brass); font: .75rem/1.4 var(--type); letter-spacing: .06em; }
.sheet .dossier-head h2 { margin-block: .5rem; font-size: max(min(1.9rem, 1.9 * 1.25 * var(--base-root)), 1.25rem); }
.sheet .dossier-head p { color: var(--noir-muted); font: .78rem/1.35 var(--sans); }
.sheet .quote { border: 0; border-left: 2px solid var(--stamp); padding: .75rem; border-radius: 0; background: var(--card); font: .82rem/1.5 var(--type); }
/* The accusation: one full-length figure, lit against the noir sheet, and
   the question. 'accuse-ended' is the telling that ended on a wrong one. */
.sheet-backdrop[data-kind^='accuse'] .sheet { background: var(--noir-2); color: var(--noir-ink); border-color: var(--noir-line); }
.sheet-backdrop[data-kind^='accuse'] .sheet h2 { font-size: max(min(1.9rem, 1.9 * 1.25 * var(--base-root)), 1.25rem); }
.accuse-brief { font: .8rem/1.5 var(--sans); color: var(--noir-muted); }
.accuse-brief .kicker { color: var(--brass); font: .75rem var(--type); }
.accuse-suspect p, .accuse-clash { font: .92rem/1.45 var(--sans); }
.sheet .accuse-suspect .accuse-ask { font: 500 1.3rem/1.2 var(--display); color: var(--noir-ink); }
.sheet-backdrop[data-kind^='accuse'] .btn { color: var(--noir-ink); border-color: var(--noir-line); background: transparent; }
.sheet-backdrop[data-kind^='accuse'] .btn-primary { background: var(--brass); border-color: var(--brass); color: var(--noir); }
.sheet-backdrop[data-kind^='accuse'] .accuse-note { color: var(--brass); font-size: .76rem; min-height: 2.6em; }
.sheet-backdrop[data-kind^='accuse'] .accuse-suspect .accuse-note { min-height: 0; font-size: .92rem; line-height: 1.45; }
/* The resolved file closes before its final stamp and verdict settle. */
.closed-file { position: relative; isolation: isolate; min-height: 6.125rem; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .625rem; margin: -.25rem -.25rem 1.25rem; padding: 1.125rem; background: var(--manila); border: 1px solid var(--manila-edge); border-radius: var(--paper-radius); box-shadow: var(--paper-shadow); perspective: 600px; }
.closure-flap { position: absolute; inset: 0; z-index: -1; transform-origin: top; background: var(--manila); border-bottom: 1px solid var(--manila-edge); }
/* Wrapped at a large text size, the file number and the stamp centre their
   lines in the centred file. */
.closure-number { font: .75rem var(--type); letter-spacing: .12em; color: var(--muted); text-align: center; }
.closed-file .stamp { margin: 0; font-size: 1.15rem; text-align: center; }
.closed-verdict { display: flex; gap: .875rem; align-items: center; margin-bottom: .875rem; }
.closed-verdict .mug { width: 4rem; height: 5rem; border: 3px solid var(--card); transform: rotate(-3deg); }
.closed-verdict .kicker { font-size: .75rem; }
.closed-verdict h2 { margin: .3125rem 0 0; }
/* A sheet too narrow for the portrait, the gap and the kicker's longest word
   ("CONCLUSIVE") side by side, about 10.5rem (large text on a phone): the
   portrait goes above the words, which get the full width, and so does the
   rank card's badge. The file and its stamp take narrower side padding, so
   the stamp's longest line ("CLOSED") stays on the paper at its full size. */
@container (max-width: 11rem) {
  .closed-verdict { flex-direction: column; align-items: flex-start; }
  .rank.ink { flex-direction: column; align-items: flex-start; }
  .rank.ink .rank-body { align-self: stretch; }
  .closed-file { padding-inline: .5rem; }
  .closed-file .stamp { padding-inline: .25rem; }
}
.closed-body > p { font: .84rem/1.4 var(--sans); }
.closed-body > .note { font-size: .75rem; }
/* Result rows: the shorthand above resets the weight, and .sheet p the margin. */
.closed-body > .ch-result, .closed-body > .clean-result { font-weight: 700; margin-bottom: .375rem; }
.closed-body .closed-next { margin-top: 1rem; }
/* The offer of Volume 1 and the Pass, now and then, under the next case
   (Catalog.storeAsk): set apart from the case's own result. */
.closed-body .closed-offer { margin-top: .75rem; padding: .75rem; border: 1px solid var(--manila-edge); border-radius: 8px; }
.closed-body .closed-offer p { margin: 0; font: .84rem/1.4 var(--sans); }
.closed-body [data-act='next'] { min-height: 2.875rem; }
@media (prefers-reduced-motion: no-preference) {
  .closed-body { animation: none; }
  .closed-file .closure-flap { animation: fileSeal 650ms cubic-bezier(.2,.7,.2,1) both; }
  .closed-file .stamp { animation: slam 450ms 550ms cubic-bezier(.2,.7,.3,1) both; }
  .closed-verdict { animation: fadeUp 450ms 700ms ease-out both; }
  @keyframes fileSeal { from { transform: rotateX(-100deg); } to { transform: rotateX(0); } }
}
@media (prefers-reduced-motion: reduce) {
  .rank-bar i { transition: none; }
}
/* ---------- unified materials ---------- */
.sheet, .ob-card { border-radius: var(--surface-radius); box-shadow: var(--surface-shadow); }
.polaroid, .note-card, .cork-clip { box-shadow: var(--paper-shadow); border-radius: var(--paper-radius); }
.note-card, .cork-notes li:nth-child(even) .note-card, .cork-clip { background: var(--card); }
.cork { background: var(--fabric); border: 1px solid var(--noir-line); box-shadow: inset 0 0 0 4px var(--noir-2); border-radius: var(--surface-radius); }
#page-evidence { background: var(--noir); color: var(--noir-ink); }
#page-evidence .page-head h2, #page-methods .page-head h2 { font-style: normal; }
#page-evidence .page-tip { color: var(--noir-muted); font-size: .75rem; letter-spacing: .03em; }

/* ---------- detective profile (specs/001-detective-profile-ranks) ---------- */
.profile { z-index: 9; }
.pf-head { display: grid; gap: .5rem; }
/* Badge beside the rank, or above it where the line would be too narrow. */
.pf-rank { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: .875rem; align-items: center; padding: .875rem; border-radius: var(--surface-radius); background: var(--noir-2); border: 1px solid var(--noir-line); box-shadow: var(--paper-shadow); }
@container (max-width: 17rem) { .pf-rank { grid-template-columns: minmax(0, 1fr); justify-items: start; } }
.pf-rank .badge { width: 4rem; height: 4rem; font-size: 1.5rem; }
.pf-rank .kicker { color: var(--brass); }
.pf-rank .rank-body { min-width: 0; }
.pf-rank h2 { margin: 2px 0 0; font: 500 max(min(1.7rem, 1.7 * 1.25 * var(--base-root)), 1.25rem)/1.05 var(--display); }
.pf-rank .pf-points { margin: .25rem 0 0; font: 400 0.85rem var(--type); color: var(--noir-muted); }
.pf-rank .rank-bar { margin-top: .5rem; }
.pf-rank .rank-next { font-size: 0.78rem; }
/* The detective name (the leaderboard name) and its Change name button. */
.pf-name { display: flex; flex-wrap: wrap; align-items: center; column-gap: .75rem; margin-top: 2px; font: 400 0.9rem var(--type); color: var(--noir-ink); }
.pf-name .pf-tag, .pf-name .pf-hidden { color: var(--noir-muted); }
.pf-name .cb-link { font-size: 0.85rem; }
/* One row of tabs; where the five don't fit, three and two, and with large
   text a row that scrolls sideways (with an edge fade) rather than shrinking
   the labels. */
.pf-tabs { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(max-content, 1fr); overflow-x: auto; overscroll-behavior-x: contain; scrollbar-width: none; gap: 2px; padding: .25rem; border-radius: var(--surface-radius); background: var(--noir-2); border: 1px solid var(--noir-line); }
.pf-tabs::-webkit-scrollbar { display: none; }
@container (max-width: 21rem) and (min-width: 15rem) {
  #pf-tabs { grid-auto-flow: row; grid-template-columns: repeat(6, minmax(0, 1fr)); }
  #pf-tabs button { grid-column: span 2; }
  #pf-tabs button:nth-child(n + 4) { grid-column: span 3; }
}
.pf-tabs button {
  min-width: 2.75rem; min-height: 2.75rem; padding-inline: .3125rem; border: 0; border-radius: var(--surface-radius); background: none; cursor: pointer;
  color: var(--noir-muted); font: 700 0.78rem var(--sans);
}
.pf-tabs button[aria-selected="true"] { background: var(--noir-ink); color: var(--noir); }
/* The panel lays its blocks out in a grid (Task 10): a heading, the
   streaks, the figures and the tables across it; a list across it, its
   cards as many to a row as fit (a list of one card takes one cell). Each
   list is a container, so its cards rearrange by their own width. Rows are
   as tall as what they hold (max-content): a table that scrolls sideways
   never gets squeezed. */
.pf-panel { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 18rem), 1fr)); grid-auto-rows: max-content; align-content: start; gap: .625rem; min-height: 0; overflow: hidden; container-type: inline-size; }
.pf-panel > * { grid-column: 1 / -1; min-width: 0; }
.pf-panel > :is(ul, ol):has(> li:only-child) { grid-column: auto; }
.pf-panel > :is(ul, ol) { container-type: inline-size; }
.pf-panel > :is(.ladder, .ch-list, .looks, .pf-recent):not(:has(> li:only-child)) { grid-template-columns: repeat(auto-fill, minmax(min(100%, 18rem), 1fr)); align-items: start; }
/* Words in a sheet, a How to play card or the profile keep a readable
   measure even where their block is wide (a note across a wide panel
   included: it stays one column, as long as the measure). */
.sheet :is(p, li, small, .reward), .ob-card p, .pf-panel :is(p, .reward) { max-width: var(--measure); }
.pf-panel h3 { margin: .25rem 0 0; font: 700 0.75rem var(--sans); letter-spacing: 0.12em; text-transform: uppercase; color: var(--brass); }
.pf-panel .stats { margin: 0; }
.pf-panel .stats b { font-size: 1.05rem; color: var(--ink); }
.pf-panel .note { color: var(--noir-muted); margin: 0; }
.pf-empty { padding: min(1.125rem, 5vw); border: 1px dashed var(--noir-line); border-radius: var(--surface-radius); text-align: center; color: var(--noir-muted); }
.pf-empty b { display: block; margin-bottom: .25rem; font: 500 1.15rem var(--display); color: var(--noir-ink); }
.pf-levels { width: 100%; border-collapse: collapse; font-size: 0.86rem; }
.pf-levels th, .pf-levels td { padding: .4375rem .25rem; text-align: left; vertical-align: bottom; border-bottom: 1px solid var(--noir-line); }
.pf-levels thead th { font: 700 0.75rem var(--sans); letter-spacing: 0.04em; text-transform: uppercase; color: var(--noir-muted); }
.pf-levels td { font-family: var(--type); font-variant-numeric: tabular-nums; }
.pf-recent { list-style: none; margin: 0; padding: 0; display: grid; gap: .375rem; }
.pf-recent li { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: .625rem; align-items: baseline; padding: .5rem .625rem; border-radius: var(--surface-radius); background: var(--noir-2); border: 1px solid var(--noir-line); font-size: 0.85rem; }
.pf-recent b { font: 400 0.85rem var(--type); color: var(--brass); }
/* Narrow, the case number and its time share the first line, the details
   the next; when even the number and time don't fit side by side, the time
   wraps under the number ("No. 3009" is never broken). */
@container (max-width: 18rem) {
  .pf-recent li { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: baseline; gap: .25rem .625rem; }
  .pf-recent li > span:first-of-type { order: 1; flex-basis: 100%; }
}
.pf-recent .out { color: var(--noir-muted); }
.pf-recent .ch-met { color: var(--brass); font-weight: 700; }
.pf-streaks { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 8rem), 1fr)); gap: .5rem; }
.pf-streaks div { padding: .625rem .75rem; border-radius: var(--surface-radius); background: var(--noir-2); border: 1px solid var(--noir-line); }
.pf-streaks b { display: block; font: 500 1.4rem/1.1 var(--display); color: var(--noir-ink); }
.pf-streaks span { font-size: 0.75rem; color: var(--noir-muted); text-transform: uppercase; letter-spacing: 0.06em; }
.badge { position: relative; }
.badge .badge-art { position: absolute; inset: 0; width: 100%; height: 100%; }
.badge .badge-letter { font: inherit; }
.set-link { margin: 0; }

/* ---------- ladder ---------- */
.ladder { list-style: none; margin: 0; padding: 0; display: grid; gap: .5rem; }
.ld-row { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: .75rem; align-items: start; padding: .625rem .75rem; border-radius: var(--surface-radius); background: var(--noir-2); border: 1px solid var(--noir-line); box-shadow: var(--paper-shadow); }
.ld-row .badge { width: 2.5rem; height: 2.5rem; font-size: 0.95rem; }
.ld-row.is-current { border-color: var(--brass); box-shadow: inset 0 0 0 1px var(--brass); }
.ld-row.is-upcoming .ld-top b { color: var(--noir-muted); }
.ld-row.is-upcoming .badge { filter: grayscale(0.85) brightness(0.8); }
.ld-body { min-width: 0; display: grid; gap: .3125rem; }
.ld-top { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; gap: 0 .5rem; }
.ld-top b { font: 500 1.02rem/1.15 var(--display); }
.ld-here { font: 700 0.75rem var(--sans); letter-spacing: 0.1em; text-transform: uppercase; color: var(--brass); font-style: normal; }
.ld-mark { color: var(--brass); font-weight: 800; }
.ld-lock .ui-icon, .lock-note .ui-icon { width: .875rem; height: .875rem; vertical-align: -2px; }
.ld-lock { color: var(--noir-muted); }
/* (Each as tall as what it holds: a short one beside a tall one isn't
   drawn as an empty box.) */
.ld-needs, .ld-rewards, .promo-rewards { display: flex; flex-wrap: wrap; align-items: flex-start; gap: .25rem; }
.chip { padding: 2px .4375rem; background: none; border-radius: 999px; border: 1px solid var(--noir-line); font: 400 0.75rem/1.5 var(--type); color: var(--noir-muted); }
.chip.met { color: var(--noir-ink); border-color: rgba(214, 165, 77, 0.45); }
.reward { display: inline-flex; flex-wrap: wrap; align-items: baseline; gap: 0 .3125rem; padding: 3px .5rem; border-radius: 6px; background: rgba(214, 165, 77, 0.12); border: 1px solid rgba(214, 165, 77, 0.35); color: var(--noir-ink); font: 700 0.75rem/1.3 var(--sans); }
.reward small { font: 700 0.75rem var(--sans); letter-spacing: 0.08em; text-transform: uppercase; color: var(--brass); }
button.reward { cursor: pointer; }
button.reward:disabled { cursor: default; opacity: 0.8; }

/* ---------- locked levels, challenges and looks ---------- */
.lock-note { font: 700 0.75rem/1.4 var(--sans); color: var(--noir-muted); }
.cb-level-lock[hidden] { display: none; }
/* The lock, the words, then Ladder. Where the words would get under 10rem,
   Ladder moves under them (to the right); narrower still, the words go under
   the lock and take the full width, so no word is broken. */
.lock-banner { display: flex; flex-wrap: wrap; gap: .375rem .625rem; align-items: center; padding: .625rem .75rem; border-radius: 10px; background: var(--noir-2); border: 1px dashed rgba(214, 165, 77, 0.5); }
.lock-banner > .ui-icon { flex: none; width: 1.375rem; height: 1.375rem; color: var(--brass); }
.lock-banner > div { flex: 1 1 10rem; min-width: 0; }
.lock-banner > .cb-link { flex: none; margin-left: auto; }
.lock-banner b { display: block; font: 500 1rem var(--display); }
.lock-banner span { font-size: 0.78rem; color: var(--noir-muted); }
.cb-levels button .ui-icon { width: .75rem; height: .75rem; margin-right: .25rem; vertical-align: -1px; }

/* ---------- challenges ---------- */
.ch-list { list-style: none; margin: 0; padding: 0; display: grid; gap: .5rem; }
.ch-card { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: .75rem; padding: .75rem; border-radius: var(--surface-radius); background: var(--noir-2); border: 1px solid var(--noir-line); box-shadow: var(--paper-shadow); }
.ch-card > div { min-width: 0; }
/* Narrow for the text: a ladder row's badge, or a card's icon, goes above. */
@container (max-width: 16rem) { .ld-row, .ch-card { grid-template-columns: minmax(0, 1fr); } }
.ch-card p { margin: .25rem 0 0; font-size: 0.86rem; }
.ch-card .cb-link { font-size: 0.84rem; }
.ch-card.is-locked .ch-icon { color: var(--noir-muted); }
.ch-icon { display: grid; place-items: center; width: 2.5rem; height: 2.5rem; border-radius: 50%; border: 1px solid rgba(214, 165, 77, 0.45); color: var(--brass); }
.ch-icon .ui-icon { width: 1.375rem; height: 1.375rem; }
.ch-result, .clean-result { display: flex; align-items: center; gap: .375rem; margin: 0 0 .375rem; font-weight: 700; color: var(--muted); }
.ch-result.met, .clean-result { color: #1f5a35; } /* darker than --good: 4.5:1 on both case files */
.ch-result .ui-icon, .clean-result .ui-icon { width: 1rem; height: 1rem; }

/* ---------- promotions (on the manila sheets) ---------- */
.promoted { display: grid; gap: .625rem; margin: .25rem 0 .75rem; padding: .75rem; border-radius: var(--surface-radius); background: var(--ink); color: var(--manila); }
.promo { display: grid; grid-template-columns: auto minmax(0, 1fr); gap: .75rem; align-items: center; }
.promo .badge { width: 3.25rem; height: 3.25rem; font-size: 1.2rem; }
.promo b { display: block; margin: 2px 0 .375rem; font: 500 max(min(1.3rem, 1.3 * 1.25 * var(--base-root)), 1.25rem)/1.1 var(--display); color: var(--manila); }
.promo .promo-rule { margin: 0 0 .5rem; font: 0.84rem/1.4 var(--sans); color: var(--manila); }
/* A narrow sheet (large text, a small phone): the medal or badge above the words, which get the full width,
   with room for the kicker's longest word ("COMMENDATION"), hyphenated if it still can't fit (dom.js gives
   that word a soft hyphen, for browsers without a hyphenation dictionary). A name with a word too long for
   even that ("Investigator" at the largest text on the smallest phone) gets soft hyphens at its syllables
   from dom.js (fitNames), and only then: the browser never hyphenates a name itself, since that would break
   a word that fits whole on the next line. */
@container (max-width: 15rem) {
  .promo { grid-template-columns: minmax(0, 1fr); gap: .5rem; }
  .promoted { padding: .5rem; }
  .promo .kicker { letter-spacing: .04em; hyphens: auto; }
}
.promo .kicker { color: var(--brass); }
.promoted .reward { color: var(--manila); }
.promoted .cb-link { justify-self: start; color: var(--brass); }

/* ---------- looks ---------- */
/* Swatch, name, then In use, Use or how to earn it; where that crowds the
   name, it moves under the name. */
.looks { list-style: none; margin: 0; padding: 0; display: grid; gap: .375rem; }
.look { display: grid; grid-template-columns: 3.5rem minmax(0, 1fr) auto; gap: .375rem .625rem; align-items: center; padding: .5rem .625rem; border-radius: var(--surface-radius); background: var(--noir-2); border: 1px solid var(--noir-line); box-shadow: var(--paper-shadow); }
.look > :nth-child(3) { justify-self: end; }
@container (max-width: 22rem) {
  .look { grid-template-columns: auto minmax(0, 1fr); }
  .look > .look-preview { grid-row: span 2; }
  .look > :nth-child(3) { grid-column: 2; justify-self: start; }
}
.look.in-use { border-color: var(--brass); }
.look.is-locked .look-name { color: var(--noir-muted); }
.look-name { min-width: 0; font-weight: 700; font-size: 0.9rem; overflow-wrap: break-word; }
.look-use { min-height: 2.75rem; padding: .25rem .875rem; }
.look-preview { width: 3.5rem; height: 2.75rem; padding: 0; border-radius: 6px; border: 1px solid var(--noir-line); background: var(--noir); color: inherit; display: grid; place-items: center; overflow: hidden; }
.look .lock-note { max-width: 9rem; }

@media (prefers-reduced-motion: no-preference) {
  .promo { animation: fadeUp 400ms ease-out both; }
}

/* The detective profile and the leaderboards are lists that scroll
   (specs/007-honours-board-lists, www/js/ui/lists.js): the whole view
   scrolls. The way back (.list-bar) and the tabs (.list-tabs) stick at the
   top, and the leaderboard's own line (.list-foot) at the foot, each only
   where lists.js measured that together they take no more than a third of
   the screen (data-stick, their heights in --stick-*). They are opaque and
   run edge to edge, and the bar and the foot carry the system bars' insets,
   so the list passes under them; the bar keeps room under the way back
   for its focus ring, which the tabs would otherwise cover once they
   stick. While the list goes on below the screen
   (data-more-y), a fade with an arrow sits at its foot (.list-more), never
   under the navigation bar: it lies only over items that scrolling brings
   into view, and taps pass through it. */
.list-view { overflow-y: auto; scroll-padding-block: var(--stick-top, 0px) var(--stick-foot, 0px); }
.list-view > .casebook-inner { min-height: 100%; gap: .625rem; padding-top: 0; padding-bottom: max(.625rem, env(safe-area-inset-bottom, 0px)); }
.list-bar, .list-tabs, .list-foot {
  margin-inline: calc(-1 * var(--inner-left)) calc(-1 * var(--inner-right));
  padding-inline: var(--inner-left) var(--inner-right);
  background: var(--noir);
}
.list-bar { display: flex; padding-top: max(.625rem, env(safe-area-inset-top, 0px)); padding-bottom: .5rem; }
.list-tabs { padding-bottom: .3125rem; }
.list-view[data-stick~="top"] .list-bar { position: sticky; top: 0; z-index: 2; }
.list-view[data-stick~="top"] .list-tabs { position: sticky; top: var(--stick-bar, 0px); z-index: 2; }
.list-foot { margin-top: auto; padding-top: .5rem; padding-bottom: max(.625rem, env(safe-area-inset-bottom, 0px)); }
.list-view[data-stick~="foot"] .list-foot { position: sticky; bottom: 0; z-index: 2; }
.list-more { position: sticky; bottom: max(var(--stick-foot, 0px), env(safe-area-inset-bottom, 0px)); z-index: 1; height: 0; pointer-events: none; opacity: 0; }
.list-more::before { content: ''; position: absolute; bottom: 0; left: calc(-1 * var(--inner-left)); right: calc(-1 * var(--inner-right)); height: 2.5rem; background: linear-gradient(transparent, var(--noir)); }
.list-more .ui-icon { position: absolute; bottom: .25rem; left: 50%; width: 1.5rem; height: 1.5rem; margin-left: -.75rem; color: var(--noir-muted); }
.list-view[data-more-y~="bottom"] .list-more { opacity: 1; }
@media (prefers-reduced-motion: no-preference) { .list-more { transition: opacity 150ms ease-out; } }
/* A list view's panel grows with its content (nothing in it is clipped, a
   focus ring included), and fills the rest of a view its list doesn't, so
   a swipe below a short list still changes tab (lists.js). A block of
   Stats (.pf-block) and a group of Honours (.hn-group) keep the panel's
   spacing, and their lists its columns. */
.list-view .pf-panel { overflow: visible; flex-grow: 1; }
.pf-block, .hn-group { display: grid; grid-template-columns: minmax(0, 1fr); gap: .625rem; min-width: 0; }
.pf-block > :is(ul, ol), .hn-group > :is(ul, ol) { container-type: inline-size; }
.pf-block > .pf-recent, .hn-group > .ch-list { grid-template-columns: repeat(auto-fill, minmax(min(100%, 18rem), 1fr)); align-items: start; }
.hn-group h3 .hn-count { color: var(--noir-muted); }

/* Tablet accessibility: scale the working document, not just its outer frame.
   The tablet root is already 20 to 30px, so the app's scale stops at 1.5 here. */
@media (min-width: 600px) and (min-height: 600px) {
  :root { --base-root: clamp(20px, min(3.2vw, 3.2vh), 30px); font-size: calc(var(--base-root) * min(var(--ui-scale, 1), 1.5)); }
  body { display: block; font-size: 1rem; }
  /* A board square's floor stays a tap target, 44px at Standard growing
     with the text (ruling R3a), not 2.75 times the larger tablet root. */
  .board-frame { --sq-min: calc(44px * var(--ui-scale, 1)); }
  /* Keep clear of the system bars: iOS, and Android WebView 140+ (Capacitor
     passes the insets through), draw the page under the status bar and the
     navigation or home bar. */
  .app {
    padding: max(.6rem, env(safe-area-inset-top, 0px)) max(.6rem, env(safe-area-inset-right, 0px)) max(.6rem, env(safe-area-inset-bottom, 0px)) max(.6rem, env(safe-area-inset-left, 0px));
  }
  :root { --page-edge: .8rem; }
  .casebook-inner { gap: .7rem; }
  .top { gap: .25rem .6rem; }
  .brand h1 { font-size: max(min(1.4rem, 1.4 * 1.25 * var(--base-root)), 1.25rem); }
  .top-actions { gap: .4rem; }
  .top-actions .icon-btn { font-size: .85rem; padding-inline: .6rem; }
  .top-actions .icon-btn.round { padding: 0; }
  .brand-line .timer { font-size: .85rem; }
  .tabs { gap: .2rem; }
  .tab { font-size: .9rem; padding: .5rem; }
  .ui-icon { width: 1.1em; height: 1.1em; }
  .page { --page-pad: .65rem; }
  .page-head { margin-bottom: .6rem; }
  .crime { gap: .6rem; }
  .briefing-copy { padding: .65rem .65rem .65rem 0; }
  .crime h3 { font-size: max(min(1.55rem, 1.55 * 1.25 * var(--base-root)), 1.25rem); }
  .briefing-copy p { font-size: .85rem; }
  .board-section-label { font-size: .8rem; margin-top: .2rem; }
  .status { font-size: .8rem; }
  .pad { gap: .35rem; margin-top: .45rem; }
  .room-btn { padding: .4rem; gap: .45rem; font-size: .9rem; }
  .room-btn i { width: 1.6rem; height: 1.6rem; }
  .tools { margin-top: .4rem; gap: .3rem; }
  .tool { font-size: .85rem; }
  .casebook .case-folder { padding: .8rem; gap: .7rem; }
  .casebook .case-row { grid-template-columns: 4rem minmax(0,1fr) 1rem; }
  .case-emblem, .case-row .case-emblem { width: 4rem; height: 4.5rem; }
  .casebook .case-feature { min-height: 12rem; padding: 1rem; }
  .casebook .case-feature .f-label { max-width: none; padding-right: 1.5rem; font-size: max(min(2rem, 2 * 1.25 * var(--base-root)), 1.25rem); }
  .case-cast .portrait { width: 1.7rem; height: 2rem; }
  .case-cast { gap: .3rem; }
  .case-kicker, .case-state, .case-cta, .case-location { font-size: .8rem; }
  .cb-profile, .cb-menu summary, .cb-levels button { font-size: .85rem; }
  #cb-rank .badge { width: 2rem; height: 2rem; }
  #cb-rank .rank-top span, .cb-library-note .cb-level-note, .cb-library-note .cb-link { font-size: .78rem; }
  .clues { gap: .65rem; }
  .testimony button { padding: .75rem; gap: .3rem .7rem; min-height: clamp(6rem, 15vh, 9rem); align-content: center; }
  .testimony .said { font-size: 1.08rem; line-height: 1.5; }
  .cork { padding: 1.25rem .7rem .7rem; }
  .polaroid { --tilt: .25; }
  .cork-chip, .cork-cut { font-size: .8rem; }
  .cork-notes li { max-width: none; }
  .note-card { min-height: 5rem; padding: .8rem .7rem; font-size: 1rem; line-height: 1.45; }
  .cork-clip { flex-basis: 12rem; padding: .75rem .55rem .55rem; }
  .cork-clip b { font-size: 1rem; }
  .pager { font-size: .8rem; }
  .sheet { padding: 1rem; }
  .sheet .btn { font-size: .9rem; }
  .list-view > .casebook-inner { gap: .5rem; }
  .pf-rank .badge { width: 3rem; height: 3rem; }
  .look { grid-template-columns: 3rem minmax(0,1fr) auto; padding: .4rem; gap: .5rem; }
  .look-preview { width: 3rem; }
  .look .lock-note { max-width: 8rem; }
  .ld-row .badge { width: 2rem; height: 2rem; }
}

/* The board and the clues on screen together: Split, Board first, the
   drawer and side by side share these. The folder becomes a grid: the
   board's page, the tabs under a brass rule, then the clue page. Each layout
   sets its own below (Board first a single column instead); side by side
   adds its columns. */
.split-workspace .folder { display: grid; grid-template-rows: auto auto minmax(25rem, 1fr); overflow-y: auto; perspective: none; margin-top: .375rem; background: var(--noir); }
.split-workspace .page { position: relative; inset: auto; min-width: 0; min-height: 0; }
.split-workspace #page-board { grid-row: 1; }
.split-workspace #case-tabs { grid-row: 2; margin: 0; border-top: 2px solid var(--brass); padding-top: .5rem; }
.split-workspace #page-evidence, .split-workspace #page-testimony, .split-workspace #page-methods { grid-row: 3; }
.split-workspace .cork-clip { display: none; }
.split-workspace .cork-bar { margin: .3125rem 0; }
.split-workspace .page-head { margin-bottom: .25rem; }
.split-workspace .page-head h2 { font-size: 1rem; }
.split-workspace .testimony button { min-height: 2.75rem; }
@media (min-width: 600px) and (min-height: 600px) {
  .split-workspace .crime { grid-template-columns: 3.2rem minmax(0,1fr); }
  .split-workspace .briefing-copy { padding: .25rem; }
  .split-workspace .briefing-copy::before { width: 2.5rem; height: 2.5rem; }
  /* the clipping never stacks here, so the tab stays in its top corner */
  .split-workspace #briefing-toggle[aria-expanded="true"] { top: var(--page-pad); border-radius: 0 var(--paper-radius) 0 .625rem; }
  .split-workspace .crime h3 { font-size: 1.1rem; }
  .split-workspace .crime .paper { display: none; }
  .split-workspace .note-card { min-height: 4rem; font-size: .85rem; padding: .75rem .5rem .5rem; }
  .split-workspace .testimony .said { font-size: .9rem; }
  .split-workspace .testimony button { padding: .4rem; }
}

/* ---------- Split (spec 005 R3.2): the board above the clues ----------
   The folder is the case area: the board's page on top, as tall as layout.js
   makes it (--split-pane, the handle's place), then the handle and the tabs
   in one row under the brass rule, then the clue page, which scrolls in its
   own pane and shows every clue (no pager). The room pad and the tools stay
   with the board (R3.3). Given less than the board's height at its smallest
   squares, the frame keeps the hour row and two rows of squares
   (--board-least) and the board pans inside it; with less still, the
   board's page scrolls.
   Where it can't give the board its least frame and one whole clue, or the
   case view flows, Split stacks (layout.js data-stacked, decided from the case
   view's own height): no handle, and the board's page, the tabs and the
   clue page, each as tall as its content, scroll as one, as a flowing view
   does. These rules apply only while it doesn't stack. */
.app[data-layout="split"]:not([data-stacked]) .folder { grid-template-columns: auto minmax(0, 1fr); grid-template-rows: var(--split-pane, auto) auto minmax(0, 1fr); overflow: hidden; }
.app[data-layout="split"]:not([data-stacked]) #page-board { grid-column: 1 / -1; overflow-y: auto; overscroll-behavior-y: contain; }
.app[data-layout="split"]:not([data-stacked]) .board-frame { --frame-min: var(--board-least); }
.app[data-layout="split"]:not([data-stacked]) #case-tabs { grid-column: 2; }
.app[data-layout="split"]:not([data-stacked]) #page-testimony, .app[data-layout="split"]:not([data-stacked]) #page-evidence,
.app[data-layout="split"]:not([data-stacked]) #page-methods { grid-column: 1 / -1; overflow-y: auto; overscroll-behavior-y: contain; }
.app[data-stacked] > .folder { --flow: 1; flex: none; grid-template-rows: auto; overflow: visible; }
.app[data-stacked] #page-board, .app[data-stacked] #page-testimony, .app[data-stacked] #page-evidence,
.app[data-stacked] #page-methods { overflow: visible; }
/* Room for both on a phone (Split, and the drawer, whose board shares the
   area with it): the clipping keeps its photo, the victim and where and
   when, without its masthead line (the case number is in the header) or its
   instruction. A clue page under its tabs (Split, the drawer, Board first)
   leaves its name to its tab (still a heading for a screen reader). No text
   gets smaller. */
.app[data-layout="split"] .crime, .app[data-layout="drawer"] .crime { grid-template-columns: 3.5rem minmax(0, 1fr); gap: .625rem; }
.app[data-layout="split"] .crime .paper, .app[data-layout="drawer"] .crime .paper { display: none; }
.app[data-layout="split"] .page-head h2, .app[data-layout="drawer"] .page-head h2,
.app[data-layout="board_first"] .page-head h2 { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
/* Too narrow for the photo beside the words (where One page sets the photo
   above them), Split and the drawer leave the photo out: the words say where
   and when. The masthead line comes back, beside the fold tab, so the
   victim's name has the clipping's whole width below it. */
@container page (max-width: 16rem) {
  .app[data-layout="split"] .crime, .app[data-layout="drawer"] .crime { grid-template-columns: minmax(0, 1fr); }
  .app[data-layout="split"] .briefing-visual, .app[data-layout="drawer"] .briefing-visual { display: none; }
  .app[data-layout="split"] .crime .paper, .app[data-layout="drawer"] .crime .paper { display: flex; }
  .app[data-layout="split"] #briefing-toggle[aria-expanded="true"],
  .app[data-layout="drawer"] #briefing-toggle[aria-expanded="true"] { top: var(--page-pad); border-radius: 0 var(--paper-radius) 0 .625rem; }
}
/* The handle: a tab-shaped grip at the start of the tab row, a tap target
   on the brass rule. Dragged up or down (layout.js keeps the pointer), tapped
   to give the clues more room and back, or stepped by the arrow keys. Where
   it has no room to move (data-fixed), the grip isn't drawn. */
.split-handle { display: none; min-width: 2.75rem; min-height: 2.75rem; touch-action: none; }
.app[data-layout="split"]:not([data-stacked]) .split-handle {
  display: grid; place-items: end center; grid-column: 1; grid-row: 2; align-self: stretch;
  margin: 0; padding: .5rem 0 0 .25rem;
  border-top: 2px solid var(--brass); color: var(--brass); cursor: row-resize;
}
.split-grip {
  display: grid; place-items: center; width: 2.5rem; height: 2.75rem;
  border: 1px solid #3a3440; border-bottom: 0; border-radius: 5px 5px 0 0; background: #2a262e;
}
.split-grip .ui-icon { width: 1.375rem; height: 1.375rem; stroke-width: 2; }
.split-handle:active .split-grip { background: var(--noir-2); }
.split-handle[data-fixed] { pointer-events: none; }
.split-handle[data-fixed] .split-grip { visibility: hidden; }
.split-handle:focus-visible { outline-offset: -4px; box-shadow: inset 0 0 0 6px var(--focus-ring-edge) !important; }

/* ---------- Board first (spec 005 R3.4): one long page ----------
   The board's page at its natural height, the board as wide as the page
   (its squares sized by the width), but never taller than the case area;
   then the tabs under the brass rule, and the clue page with every clue.
   The folder scrolls them, so the header stays (layout.js caseScroller), and
   the jump button floats at the foot (layout.js updateJump). The folder is the
   case area, a size container, so the board's frame can be capped at its
   height (100cqh); when the case view flows, the view scrolls instead
   (flow mode, above) and the folder grows with the page, so it is no size
   container then (the rule after this block). */
/* One column in which nothing shrinks: the board's page, the tabs and the
   clue page each as tall as all they hold (the long page scrolls, not a
   page; the tab row, a sideways scroller, would otherwise be given no
   height), the clue page filling any room left below. */
.app[data-layout="board_first"] .folder { display: flex; flex-direction: column; container: case / size; }
.app[data-layout="board_first"] .folder > * { flex: none; }
.app[data-layout="board_first"] .folder > .page:not(#page-board) { flex: 1 0 auto; }
.app[data-layout="board_first"] #page-board, .app[data-layout="board_first"] #page-testimony,
.app[data-layout="board_first"] #page-evidence, .app[data-layout="board_first"] #page-methods { overflow: visible; }
/* (The frame keeps its height: the folder's grid sizes the board's row at
   the page's least, which a frame that could shrink would take.) */
.app[data-layout="board_first"] .board-frame {
  flex: none;
  --sq-w: max(var(--sq-min), (100cqw - var(--n, 4) * var(--gap) - var(--ring)) / (var(--n, 4) + 12 / 11));
  --frame-h: min(calc(var(--hour-h) + var(--n, 4) * (var(--sq-w) + var(--gap)) + var(--ring)), 100cqh);
}
@container app (max-height: 32rem) {
  .app[data-layout="board_first"] > .folder { container-type: normal; }
}

/* ---------- the Clue drawer (spec 005 R3.4) ----------
   The board's page fills the case area above a drawer of clues that rises
   from the foot: its strip (the grip, then the tabs with each page's name
   and count) at peek; at half and full, the clue page raised under it,
   scrolling in its own pane with every clue (--drawer-pane, layout.js
   applyDrawer). The board's page shrinks above the drawer, never under it:
   at half it keeps the board's least frame (the hour row and two rows,
   panning) with the briefing, the pad and the tools whole (ruling T8-3);
   full gives the clues the whole area and hides the board. The drawer runs
   under the bottom bar: the case view gives its bottom spacing to the
   folder, whose last row is that spacing (the bar's height, or .625rem
   with none). The clue page reaches into it, so its own background runs
   under the bar while its padding keeps the clues clear of it; at peek the
   folder's own (the strip's) shows there. Where the case area can't hold
   the board's least page and the strip, or the case view flows, the drawer
   stacks as Split does (layout.js data-stacked); these rules apply only while
   it doesn't. It glides between its states only when motion is welcome. */
.app[data-layout="drawer"]:not([data-stacked]) { padding-bottom: 0; }
.app[data-layout="drawer"]:not([data-stacked]) .folder {
  grid-template-columns: auto minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) auto var(--drawer-pane, 0px) max(.625rem, env(safe-area-inset-bottom, 0px));
  overflow: hidden; border-bottom-width: 0; border-radius: 0 5px 0 0;
}
.app[data-layout="drawer"]:not([data-stacked]) #page-board { grid-column: 1 / -1; overflow-y: auto; overscroll-behavior-y: contain; }
.app[data-layout="drawer"]:not([data-stacked]) .board-frame { --frame-min: var(--board-least); }
.app[data-layout="drawer"]:not([data-stacked]) #case-tabs { grid-column: 2; touch-action: pan-x pinch-zoom; }
.app[data-layout="drawer"]:not([data-stacked]) #page-testimony, .app[data-layout="drawer"]:not([data-stacked]) #page-evidence, .app[data-layout="drawer"]:not([data-stacked]) #page-methods {
  grid-column: 1 / -1; grid-row: 3 / 5; overflow-y: auto; overscroll-behavior-y: contain;
  padding-bottom: max(.625rem, env(safe-area-inset-bottom, 0px));
}
/* At peek the clue page waits below the strip; full hides the board. */
.app[data-layout="drawer"][data-drawer="peek"]:not([data-stacked]) #page-testimony, .app[data-layout="drawer"][data-drawer="peek"]:not([data-stacked]) #page-evidence,
.app[data-layout="drawer"][data-drawer="peek"]:not([data-stacked]) #page-methods, .app[data-layout="drawer"][data-drawer="full"]:not([data-stacked]) #page-board { visibility: hidden; }
/* The grip: a tab-shaped handle at the start of the strip, on the brass
   rule, as Split's is. */
.drawer-grip { display: none; min-width: 2.75rem; min-height: 2.75rem; padding: 0; border: 0; background: none; color: var(--brass); cursor: row-resize; touch-action: none; }
.app[data-layout="drawer"]:not([data-stacked]) .drawer-grip {
  display: grid; place-items: end center; grid-column: 1; grid-row: 2; align-self: stretch;
  margin: 0; padding: .5rem 0 0 .25rem; border-top: 2px solid var(--brass); background: var(--noir);
}
.drawer-grip:active .split-grip { background: var(--noir-2); }
.drawer-grip:focus-visible { outline-offset: -4px; box-shadow: inset 0 0 0 6px var(--focus-ring-edge) !important; }
@media (prefers-reduced-motion: no-preference) {
  .app[data-layout="drawer"]:not([data-stacked]):not(.drawer-dragging) .folder { transition: grid-template-rows 220ms cubic-bezier(.2, .7, .2, 1); }
}

/* ---------- the Layout sheet ----------
   Each layout as a button: a little phone drawn in CSS (never an image),
   then its name and a line about it. The layout in use is pressed. */
.layout-options { display: grid; gap: .5rem; grid-template-columns: repeat(auto-fit, minmax(min(100%, 16rem), 1fr)); }
.sheet .layout-opt { justify-content: flex-start; gap: .875rem; width: 100%; padding: .625rem .75rem; }
.layout-opt[aria-pressed="true"] { border-color: var(--ink); box-shadow: inset 0 0 0 1px var(--ink); }
.layout-words { display: grid; gap: 2px; min-width: 0; }
.layout-words b { font-size: .95rem; }
/* The layout in use says so in words, not just by its border. */
.layout-inuse { justify-self: start; margin-top: .25rem; padding: 1px .5rem; border-radius: .5rem; background: var(--ink); color: var(--manila); font: 700 .75rem/1.4 var(--sans); letter-spacing: .06em; text-transform: uppercase; }
.sheet .layout-opt small { text-align: left; font-size: .8rem; line-height: 1.35; }
/* Split's line where it would stack at this size (review R7g). */
.sheet .layout-opt .layout-stacks { color: var(--stamp); font-weight: 700; }
/* Too narrow for the phone beside the words (12.5rem: the padding, the
   phone, the gap and the longest word), the phone goes above them. */
@container (max-width: 12.5rem) { .sheet .layout-opt { flex-direction: column; align-items: flex-start; } }
/* The phone: the board a dark panel ruled into squares, the clues ruled
   paper, the tabs and the handle brass. */
.layout-diagram {
  --d-inset: .1875rem;
  position: relative; flex: none; width: 2.75rem; height: 4.25rem; overflow: hidden;
  border: 2px solid var(--ink); border-radius: .5rem; background: var(--card);
}
.layout-diagram i { position: absolute; left: var(--d-inset); right: var(--d-inset); display: block; }
.layout-diagram i:first-child {
  border-radius: 2px; background-color: var(--noir);
  background-image: linear-gradient(to right, var(--noir-line) 1px, transparent 1px), linear-gradient(to bottom, var(--noir-line) 1px, transparent 1px);
  background-size: .375rem .375rem;
}
.layout-diagram[data-layout] i:last-child { background: repeating-linear-gradient(to bottom, transparent 0 .25rem, var(--muted) .25rem calc(.25rem + 1px)); }
/* One page at a time: the tabs, then one page (the board) under them. */
.layout-diagram[data-layout="pages"] i:nth-child(1) { top: .75rem; bottom: var(--d-inset); }
.layout-diagram[data-layout="pages"] i:nth-child(2) { top: var(--d-inset); height: .4375rem; background: repeating-linear-gradient(to right, var(--brass) 0 .5rem, transparent .5rem .625rem); }
.layout-diagram[data-layout="pages"] i:nth-child(3) { display: none; }
/* Split: the board, the handle, the clues. */
.layout-diagram[data-layout="split"] i:nth-child(1) { top: var(--d-inset); height: 2rem; }
.layout-diagram[data-layout="split"] i:nth-child(2) { top: 2.375rem; height: 3px; left: 0; right: 0; background: var(--brass); }
.layout-diagram[data-layout="split"] i:nth-child(2)::after { content: ''; position: absolute; left: .25rem; top: -.1875rem; width: .5rem; height: .5625rem; border-radius: 2px; background: var(--brass); }
.layout-diagram[data-layout="split"] i:nth-child(3) { top: 2.75rem; bottom: var(--d-inset); }
/* Board first: the board across, the room pad, then clues running on past
   the foot (the page scrolls). */
.layout-diagram[data-layout="board_first"] i:nth-child(1) { top: var(--d-inset); height: 2rem; }
.layout-diagram[data-layout="board_first"] i:nth-child(2) { top: 2.3125rem; height: .5rem; background: repeating-linear-gradient(to right, var(--brass) 0 .5625rem, transparent .5625rem .75rem); }
.layout-diagram[data-layout="board_first"] i:nth-child(3) { top: 3rem; bottom: -1rem; }
/* Clue drawer: the board fills the phone, and a drawer of clues rises from
   the foot with a grip on its edge. */
.layout-diagram[data-layout="drawer"] i:nth-child(1) { top: var(--d-inset); bottom: var(--d-inset); }
.layout-diagram[data-layout="drawer"] i:nth-child(2) { bottom: 0; left: 0; right: 0; height: 1.5rem; border-radius: .3125rem .3125rem 0 0; background: var(--card); box-shadow: 0 -1px 0 var(--muted); }
.layout-diagram[data-layout="drawer"] i:nth-child(3) { bottom: .25rem; height: .875rem; left: .3125rem; right: .3125rem; }
.layout-diagram[data-layout="drawer"] i:nth-child(2)::after { content: ''; position: absolute; top: .1875rem; left: 50%; width: .75rem; height: 2px; margin-left: -.375rem; border-radius: 1px; background: var(--muted); }

/* ---------- tablet on its side: the board beside the reference pages ----------
   Stacked, the board would get no height. layout.js adds .side-by-side (with
   .split-workspace) while (orientation: landscape) and (min-width: 900px) and
   (min-height: 560px) matches: keep that query and sideBySideQuery the same.
   Left: the board page, with the tools. Right: the reference tabs and page,
   then the room pad, which layout.js moves into .side-controls. */
/* Each column is about half the screen wide, so the tablet type size also
   follows the width (e.g. 21.5px at 1024 wide instead of 24.6px). */
@media (orientation: landscape) and (min-width: 900px) and (min-height: 600px) {
  :root { --base-root: clamp(18px, min(2.1vw, 3.2vh), 30px); font-size: calc(var(--base-root) * min(var(--ui-scale, 1), 1.5)); }
}
.split-workspace.side-by-side .folder { grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); grid-template-rows: auto minmax(0, 1fr) auto; column-gap: .5rem; overflow: hidden; }
.split-workspace.side-by-side #page-board { grid-column: 1; grid-row: 1 / -1; }
.split-workspace.side-by-side #case-tabs { grid-column: 2; grid-row: 1; border-top: 0; padding: .5rem .25rem 0 .375rem; }
.split-workspace.side-by-side #page-testimony,
.split-workspace.side-by-side #page-evidence,
.split-workspace.side-by-side #page-methods { grid-column: 2; grid-row: 2; }
.side-controls { grid-column: 2; grid-row: 3; min-width: 0; padding: 0 .65rem .65rem; background: var(--noir); container: side / inline-size; }
/* The reference column is half a screen wide and short: the room pad sits
   under the reference pages, as few rows as fit (below), and the reference
   pages are set tighter (text never below the readable minimum). */
.split-workspace.side-by-side .tab { padding: .3rem; font-size: .8rem; }
.split-workspace.side-by-side .pad { grid-template-columns: none; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: .25rem; }
/* The room pad by the column's width:
   - 25rem and up: the six rooms in one row, symbol above name (the longest
     name piece, Conserva-, is 3.5rem);
   - 18.1rem to 25rem: three to a row (two rows), each room a single row,
     symbol beside name (5.9rem a room);
   - 12.35rem to 18.1rem: three to a row, symbol above name;
   - under 12.35rem: two to a row (three rows), symbol above name. */
.split-workspace.side-by-side .room-btn { flex-direction: column; justify-content: center; gap: 2px; padding: .3rem .15rem; font-size: .75rem; text-align: center; }
@container side (max-width: 25rem) {
  .split-workspace.side-by-side .pad { grid-auto-flow: row; grid-template-columns: repeat(3, minmax(0, 1fr)); }
  .split-workspace.side-by-side .room-btn { flex-direction: row; justify-content: flex-start; gap: .3125rem; padding: .3125rem; text-align: left; }
}
@container side (max-width: 18.1rem) {
  .split-workspace.side-by-side .room-btn { flex-direction: column; justify-content: center; gap: 2px; padding: .3rem .15rem; text-align: center; }
}
@container side (max-width: 12.35rem) { .split-workspace.side-by-side .pad { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.split-workspace.side-by-side .room-btn i { width: 1.3rem; height: 1.3rem; }
.split-workspace.side-by-side .testimony .said { font-size: .75rem; line-height: 1.35; }
.split-workspace.side-by-side .note-card { min-height: 4rem; font-size: .75rem; line-height: 1.25; padding: .75rem .5rem .4rem; }
/* The list already shows the needed methods first and the tip counts them. */
.split-workspace.side-by-side .methods-lead { display: none; }
.split-workspace.side-by-side .methods li { padding: .3125rem .5rem; }
.split-workspace.side-by-side .methods b { font-size: .8rem; }
.split-workspace.side-by-side .methods p { font-size: .75rem; }
.split-workspace.side-by-side .rules ul { font-size: .75rem; }

/* ---------- closed cases in the lists ---------- */
/* The player's Case closed stamp, as on the report (gold or red). */
.casebook .case-folder { position: relative; }
.casebook .case-stamp {
  color: var(--stamp-lit); border-color: var(--stamp-lit);
  position: absolute; z-index: 2; right: 1.75rem; top: 50%; margin: 0; padding: 1px .5rem;
  font-size: .75rem; letter-spacing: .12em; white-space: nowrap; pointer-events: none;
  background: rgba(20, 18, 23, .6); transform: translateY(-50%) rotate(-8deg);
}
.casebook .case-feature .case-stamp { top: 4rem; right: 1.125rem; font-size: .8rem; transform: rotate(-8deg); }
.case-closed .case-emblem img { filter: grayscale(.6) brightness(.8); }
.cb-all-bottom { justify-self: center; font: .78rem var(--sans); text-decoration: none; }
.cb-empty { margin: .25rem 0; font: .78rem var(--sans); color: var(--noir-muted); }
.cl-head .cb-library-note { margin-top: .25rem; }
.cl-head .cb-library-note .cb-level-note { margin: 0; }
.ld-points { margin: 0 0 .5rem; }

/* ---------- service stars and commendations ---------- */
/* Service stars sit under the player's own badge (five, then a count). */
.badge-stars { position: absolute; left: 50%; bottom: -0.6em; transform: translateX(-50%); font: 700 max(0.75rem, 0.42em)/1 var(--sans); color: var(--brass); white-space: nowrap; text-shadow: 0 1px 2px rgba(0, 0, 0, 0.8); pointer-events: none; }
/* Honours: commendation cards reuse the challenge cards; earned medals take their tier's colour. */
:root { --medal-bronze: #c08453; --medal-silver: #c9ced4; --medal-gold: #e2b84f; }
.hn-tier-1 { --medal: var(--medal-bronze); }
.hn-tier-2 { --medal: var(--medal-silver); }
.hn-tier-3 { --medal: var(--medal-gold); }
.ch-icon[class*="hn-tier-"] { border: 2px solid var(--medal); color: var(--medal); }
.hn-card .ld-top span { margin-left: auto; }
.hn-card .ld-needs { margin-top: .375rem; }
/* A long label wraps as a rounded box rather than a circle, and a word too long for it breaks. */
.hn-card .chip { border-radius: .875rem; overflow-wrap: anywhere; }
/* A date or case number stays in one piece (profile.js honourWhen) while it fits: "Case No. 3059 ·" needs 7.75rem plus the card's padding. */
.hn-keep { white-space: nowrap; }
@container (max-width: 9.5rem) { .hn-keep { white-space: normal; } }
/* A look's name likewise: "Starred Laurel frame ↗", the longest, needs 11rem plus the card's padding. */
.hn-look { white-space: nowrap; }
@container (max-width: 12.75rem) { .hn-look { white-space: normal; } }
/* Progress toward the next tier: brighter than the chip's muted text (7.6:1 on the card). */
.hn-progress { color: var(--brass); }
.hn-stars { display: flex; align-items: center; gap: .375rem; color: var(--brass); }
.hn-stars .ui-icon { width: 1rem; height: 1rem; flex: none; }

/* ---------- Leaderboards (specs/004-leaderboards) ----------
   The screen is a list view like the profile (specs/007): every row of the
   board in one list that scrolls, in one column at any width (a ranked list
   read across columns hides its order), and the player's own line in the
   foot. Each row and card is a .look-scene carrying that player's looks.
   .look-scene (cosmetics.css, loaded after this file) is also a preview box with
   its own size, padding and a 116px badge, so a row repeats the class to win
   over it without shrinking anything else. The wider column gap leaves room for
   a badge frame's outline (up to 10px) between the number, badge and name. */
/* The foot carries the bottom inset, so the view's own spacing there goes.
   With no line to show, the foot keeps only the inset. */
.leaderboard.list-view > .casebook-inner { padding-bottom: 0; }
.list-foot:has(> .lb-you:empty) { padding-top: 0; }
.lb-head h2 { margin: 0; font: 400 max(min(1.6rem, 1.6 * 1.25 * var(--base-root)), 1.25rem)/1.1 var(--display); color: var(--noir-ink); }
.lb-head .note { margin: 2px 0 0; color: var(--noir-muted); }
#lb-panel > .lb-rows { grid-column: 1 / -1; list-style: none; margin: 0; padding: 0; display: grid; gap: .625rem; }
.lb-open.look-scene { width: 100%; min-height: 4rem; display: grid; grid-template-columns: 2em 2.75rem minmax(0, 1fr) auto; align-items: center; justify-items: stretch; gap: .625rem .875rem; padding: .5rem .625rem; border: 1px solid var(--noir-line); border-radius: 8px; background: var(--noir-2); color: var(--noir-ink); font: inherit; text-align: left; cursor: pointer; }
.lb-row.is-you .lb-open { border-color: var(--brass); }
.lb-pos { font: 400 0.9rem var(--type); color: var(--noir-muted); text-align: right; }
.lb-open.look-scene > .lb-badge { width: 2.75rem; height: 2.75rem; font-size: 1rem; }
.lb-who { display: grid; gap: .25rem; min-width: 0; }
.lb-name { margin: 0; font: var(--ink-face); font-size: 1rem; line-height: 1.3; color: var(--note-ink); overflow-wrap: anywhere; }
.lb-name small { display: block; font: 700 0.75rem var(--sans); letter-spacing: 0.08em; text-transform: uppercase; color: var(--brass); }
.lb-tag { font: 400 0.75rem var(--type); color: var(--noir-muted); }
.lb-honours { display: flex; flex-wrap: wrap; align-items: center; gap: .375rem; }
.lb-medal { display: inline-flex; align-items: center; justify-content: center; min-width: 1.375rem; height: 1.375rem; padding: 0 .3125rem; border-radius: 999px; background: var(--medal); color: var(--noir); font: 700 0.75rem var(--sans); }
.lb-laurel { display: inline-flex; align-items: center; gap: 3px; color: var(--medal-gold); font: 700 0.75rem var(--sans); }
.lb-laurel .ui-icon { width: 1.125rem; height: 1.125rem; }
.lb-points { font: 400 1rem var(--type); color: var(--noir-ink); }
/* A narrow board (a small phone, large text), measured on the board's own
   list (a container): the score goes under the name, and narrower still,
   the name under the badge across the row, with less spacing at its sides,
   so the longest name (Names) keeps the width it needs and nothing runs off
   the row. */
@container (max-width: 21rem) {
  .lb-open.look-scene { grid-template-columns: 2em 2.75rem minmax(0, 1fr); grid-template-areas: "pos badge who" "pos badge points"; row-gap: .25rem; }
  .lb-open.look-scene > .lb-pos { grid-area: pos; }
  .lb-open.look-scene > .lb-badge { grid-area: badge; }
  .lb-open.look-scene > .lb-who { grid-area: who; }
  .lb-open.look-scene > .lb-points { grid-area: points; }
}
@container (max-width: 15rem) {
  .lb-open.look-scene { grid-template-columns: 2.75rem minmax(0, 1fr); grid-template-areas: "badge pos" "badge points" "who who"; column-gap: .625rem; padding-inline: .375rem; }
  .lb-open.look-scene > .lb-pos { text-align: left; }
}
.lb-you { padding: .5rem .625rem; border: 1px solid var(--brass); border-radius: 8px; background: var(--noir-2); color: var(--noir-ink); }
.lb-you:empty { display: none; }
.lb-you p { margin: 0; }
/* The player's line when their row is in the list: a press brings the row
   into view. The button fills the line, which keeps its look. */
.lb-you:has(> .lb-find) { padding: 0; }
button.lb-find { display: block; width: 100%; min-height: 2.75rem; margin: 0; padding: .5rem .625rem; border: 0; border-radius: 7px; background: none; color: inherit; font: inherit; text-align: left; cursor: pointer; }

/* ---------- a player's card ---------- */
/* The card is a .look-scene, whose rules in cosmetics.css (loaded after this
   file) give a preview box its own size, padding, centring and a 68px seal, so
   the card repeats the class to win over them, as the leaderboard rows do. */
.lb-card.look-scene { place-items: stretch; min-height: 0; display: grid; gap: .75rem; padding: .875rem; border-radius: 10px; background-color: var(--noir); background-image: var(--desk-detail); color: var(--noir-ink); }
.lb-card-head { display: grid; grid-template-columns: 4rem minmax(0, 1fr); align-items: center; gap: .75rem; }
.lb-card-head > div { display: grid; gap: .25rem; min-width: 0; }
.lb-card-badge { width: 4rem; height: 4rem; font-size: 1.5rem; }
.lb-card .kicker { color: var(--brass); }
.lb-card .lb-name { margin: 0; }
.lb-card-file { display: grid; gap: .625rem; padding: .75rem; border-radius: 6px; background-color: var(--manila); background-image: var(--file-detail); color: var(--ink); }
.lb-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 6rem), 1fr)); gap: .5rem; }
.lb-stats div { display: grid; gap: 2px; }
.lb-stats b { font: 400 1.2rem var(--type); }
.lb-stats span { font: 700 0.75rem var(--sans); letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); }
.lb-held { list-style: none; margin: 0; padding: 0; display: grid; gap: .25rem; font: 400 0.85rem var(--sans); }
.lb-held li { display: flex; align-items: center; gap: .375rem; margin: 0; }
.lb-held .lb-medal { min-width: .75rem; height: .75rem; padding: 0; }
.lb-marks { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: .625rem; }
.lb-marks .stamp { margin: 0; }
/* The seal is a .closed-file so the seal looks apply to it, but here it is only
   the wax: no folder behind it, and in line with the stamp, not pinned to a corner. */
.lb-card.look-scene .lb-seal { position: relative; flex: none; width: 3.5rem; height: 3.5rem; min-height: 0; margin: 0; padding: 0; border: 0; border-radius: 0; background: none; box-shadow: none; display: block; }
.lb-card.look-scene .lb-seal .closed-seal { position: absolute; inset: 0; right: auto; bottom: auto; width: 3.5rem; height: 3.5rem; animation: none; }

/* ---------- joining, naming and hiding ---------- */
/* minmax(0, 1fr) and a full-width select: the longest option ("A sealed name · Earn a secret commendation") must not widen the sheet. */
.lb-pick { display: grid; grid-template-columns: minmax(0, 1fr); gap: .25rem; margin-bottom: .625rem; }
.lb-pick span { font: 700 0.75rem var(--sans); letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }
.lb-pick select { width: 100%; min-width: 0; box-sizing: border-box; min-height: 2.75rem; padding: .5rem; border: 1px solid var(--line); border-radius: 6px; background: var(--card); color: var(--ink); font: 400 1rem var(--sans); }
.lb-preview { margin: .375rem 0; }
/* The visibility switch is the main control of its section: a full-height row to tap, and room under the buttons for the note. */
.toggle.lb-toggle { min-height: 2.75rem; align-items: center; }
.toggle.lb-toggle input { margin-top: 0; }
.set-block .row + .note { margin-top: .625rem; }
/* The notice that Cloud Save is on: its closing line, under the button. */
#sheet[data-kind="sync-on"] .row + .note { margin-top: .75rem; }
