/* Face Jam prototype — component and screen styles.
 *
 * Colour, radius and shadow values here come from tokens.css — no colour
 * literal belongs in this file, and tests/tokens.test.js fails the suite if
 * one appears. Type and spacing mostly come from tokens too, but component
 * dimensions are deliberately literal (the 40px small button, the 24px
 * swatch, the 1.5px divider): they are spec component definitions, not points
 * on the spacing scale. tokens.css explains why, and no test enforces that
 * half — use judgement, and prefer a token when the value is scale-derived.
 *
 * Component vocabulary follows style_guide/files/. Two names collided with the
 * prototype's own and were resolved in favour of the design system:
 *   .tool-btn  is now the toolbar's ICON tool (dot/marker/eraser). The Undo /
 *              Clear / End-game text buttons became .btn variants.
 *   .swatch    is now the spec's 24px chip with a dot-below selection, not the
 *              old 28px chip with an outline ring.
 */

* {
  box-sizing: border-box;
}

body {
  margin: 0;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: var(--color-app-shell);
  font-family: var(--font-display);
  color: var(--color-content-primary);
  font-size: var(--text-body-size);
  line-height: var(--text-body-lh);
}

/* Fluid (D-041): full window height, full width up to the ergonomic cap,
   then centered by the body's flexbox on wider screens. */
#phone-frame {
  width: 100vw;
  max-width: var(--app-max-width);
  height: 100dvh;
  background: var(--color-background);
  overflow: hidden;
  position: relative;
}

/* ...except on the draw screen, which takes the whole window (D-080). The
   2026-10 desktop mock is an uncapped 1440-wide layout: its top-bar buttons
   sit in the window's far corners and its carousel runs off the window's
   edges, not a centred column's. The cap stays everywhere else — every other
   screen is a column of text and controls that D-041's ergonomic width is
   right for. Only this screen's content is a drawing that wants the room.
   No media query: below the cap this changes nothing. */
#phone-frame:has(.draw-screen) {
  max-width: none;
}

#app {
  height: 100%;
  display: flex;
  flex-direction: column;
  padding: env(safe-area-inset-top) env(safe-area-inset-right)
    env(safe-area-inset-bottom) env(safe-area-inset-left);
}

.screen {
  flex: 1;
  display: flex;
  flex-direction: column;
  padding: var(--space-md);
  overflow-y: auto;
}

/* Type/Title is the default for a heading that doesn't name a role. The
   :not([class]) matters: a bare `.screen h1` (0,1,1) outranks a single class
   like `.landing-title` (0,1,0), so without it every screen-specific heading
   role is silently overridden by this default. Naming a class opts out. */
.screen h1:not([class]) {
  font-size: var(--text-title-size);
  line-height: var(--text-title-lh);
  letter-spacing: var(--text-title-ls);
  font-weight: 700;
  margin: 0 0 var(--space-sm);
}

/* ===================== Button =====================
   Spec grid: type (primary | secondary) x size (standard ~52px | small 40px)
   x shape (label pill | icon | icon-label). D-068 flattened it: every button
   is the warm grey surface with a black label and no shadow, label or icon.
   The one exception is .accent — landing's Play and the gallery's Play again
   (D-092) — which keeps the red fill and the hard teal shadow/graphic offset. D-078 added the icon-label shape; no
   screen uses it yet. */
.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  border: none;
  font-family: var(--font-display);
  font-weight: 700;
  border-radius: var(--radius-pill);
  box-shadow: none;
  color: var(--color-content-primary);
  background: var(--color-ui-primary);
  transition: transform var(--press-dur) ease-out,
    box-shadow var(--press-dur) ease-out, background-color var(--press-dur) ease-out;
}

/* .btn's own display beats the UA sheet's [hidden] rule, so hiding a button
   by attribute (Submit, until every card is drawn on) needs saying here. */
.btn[hidden] {
  display: none;
}

.btn.standard {
  padding: var(--space-sm) var(--space-md);
  font-size: var(--text-body-size);
  line-height: var(--text-body-lh);
}

.btn.small {
  padding: 8px var(--space-sm);
  height: 40px;
  font-size: 14px;
}

.btn.icon {
  width: 52px;
  height: 52px;
  padding: 0;
}

.btn.icon.small {
  width: 40px;
  height: 40px;
}

.btn.icon svg {
  width: 24px;
  height: 24px;
  fill: currentColor;
}

/* D-078's third shape: a leading icon with a label, for when the glyph alone
   is too thin a cue. The glyph is 20px, not the icon button's 24px: it has to
   stay inside the label's 24px line box so an icon-label button is exactly as
   tall as the label-only button beside it. (24px happens to fit here too, but
   it is the whole of the line box, and it overflows Figma's 20px text box —
   20 leaves headroom in both.)

   Leading padding drops to space/sm so the glyph isn't over-inset against the
   label's space/md; .small already pads 16px both sides, so only .standard
   needs the override. The glyph never shrinks — a long label wraps or the row
   wraps first (see .action-row). */
.btn.icon-label svg {
  width: 20px;
  height: 20px;
  flex: none;
  fill: currentColor;
}

.btn.standard.icon-label {
  padding-left: var(--space-sm);
}

.btn.primary {
  background: var(--color-ui-primary);
}

.btn.secondary {
  background: var(--color-ui-primary);
}

.btn.accent {
  background: var(--color-ui-accent);
  color: var(--color-content-on-dark);
  box-shadow: var(--shadow-graphic);
}

/* D-086: the draw screen's Submit is the accent colour without Shadow/Graphic.
   It shares a row with Undo rather than carrying a screen on its own, and the
   Figma frame draws it flat; landing's Play stays the one hero button. */
.btn.accent.flat {
  box-shadow: none;
}

/* D-055 press state. A flat button sinks a fixed 2px and darkens; the hero
   sinks by exactly its shadow offset and the shadow collapses, so it lands
   flush on the page. :active covers keyboard activation too (Space held). */
.btn:active:not(:disabled) {
  transform: translateY(var(--press-travel-flat));
  background: var(--color-surface-button-pressed);
}

.btn.accent:active:not(:disabled) {
  transform: translateY(var(--press-travel-graphic));
  background: var(--color-ui-accent);
  box-shadow: 0 0 0 0 var(--color-accent-teal);
}

/* No shadow to collapse, so it takes the flat press and keeps its colour. */
.btn.accent.flat:active:not(:disabled) {
  transform: translateY(var(--press-travel-flat));
  background: var(--color-ui-accent);
  box-shadow: none;
}

@media (prefers-reduced-motion: reduce) {
  .btn {
    transition: none;
  }
}

.btn:disabled {
  background: var(--color-disabled);
  box-shadow: none;
  cursor: not-allowed;
}

.btn:focus-visible,
.tool-btn:focus-visible,
.swatch:focus-visible {
  outline: 3px solid var(--color-ui-primary);
  outline-offset: 3px;
}

/* ===================== Toolbar (D-085) =====================
   No pill: the three Tool Buttons, a hairline divider and the colour swatches
   sit straight on the page ground. The swatches take the rest of the row and
   space themselves out, which is what retires the old overflow problem — the
   pill used to measure a fixed 412px against a 390px phone and clip its last
   swatch, and a row that spreads cannot.

   Figma insets the row 8px from the frame and the toolbar another 8px inside
   it (nodes 148:362 and 148:424), putting the first tool 16px from the
   window. With no pill to draw, that inner padding has nothing to show, so
   the screen's own 16px stands in for both and the row stays inside it —
   bleeding it out instead would make .draw-screen scroll, which D-056 says it
   never does. The row stops growing at the frame's 402px, so the desktop
   layout keeps the phone's toolbar rather than stretching one across the
   column (node 148:708). Full screen puts the padding back, as a cream strip
   around the toolbar. */
.toolbar-row {
  width: 100%;
  max-width: 402px;
  align-self: center;
  /* 16px from .draw-screen's gap + 16px here = the 32px Figma leaves between
     the toolbar and the Undo/Submit row, inside D-072's 16-64px range. */
  margin-bottom: var(--space-sm);
  flex: none;
  position: relative;
}

/* ================= Bundle-arrival toast (D-090.1) =================
   Rounds from 2 on open with "New cards from Player N" over the bottom
   of the stage. It hangs off the toolbar row — 8px above it, as nodes 197:529
   and 231:559 draw it — and is absolutely positioned, so it takes no height
   and the card is measured exactly as it is without it (D-090). On a phone it
   spans the row; from 430px it takes the card's width less 16px each side,
   the same threshold at which the header follows the card (D-089).

   It waits for the new cards to finish sliding into their slots, comes in
   over 400ms, holds for 3.5s, then fades for 300ms and draw.js removes it.
   It never takes a pointer, so a stroke begun under it still lands on the
   card. Under reduced motion it fades in without the rise; a fade is a change
   of opacity, not motion, so it stays. Full screen hides it without stopping
   the clock, so it does not replay on the way back out. */
.bundle-toast {
  position: absolute;
  bottom: calc(100% + var(--space-xs));
  left: 50%;
  translate: -50% 0;
  width: 100%;
  height: 48px;
  display: flex;
  align-items: center;
  padding: 0 var(--space-sm);
  border-radius: var(--radius-sm);
  background: var(--color-content-primary);
  color: var(--color-content-on-dark);
  box-shadow: var(--shadow-soft);
  font-size: var(--text-body-size);
  line-height: var(--text-body-lh);
  letter-spacing: var(--text-body-ls);
  font-weight: 400;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  pointer-events: none;
  z-index: 4; /* over the front card (3), under the page menu's header (5) */

  /* The cards' entrance is one --carousel-dur transition from render. That
     property lives on .sheets-stage, which this is not inside, so it is
     restated here — keep the three (with STEP_MS) in step. */
  --toast-wait: 260ms;
  --toast-in: 400ms;
  --toast-hold: 3500ms;
  animation: toast-in var(--toast-in) ease-out var(--toast-wait) both,
    toast-out 300ms ease-in calc(var(--toast-wait) + var(--toast-in) + var(--toast-hold)) forwards;
}

@media (min-width: 430px) {
  .bundle-toast {
    width: calc(var(--card-w) - 2 * var(--space-sm));
  }
}

.is-fullscreen .bundle-toast {
  visibility: hidden;
}

@keyframes toast-in {
  from {
    opacity: 0;
    transform: translateY(8px);
  }
}

@keyframes toast-fade-in {
  from {
    opacity: 0;
  }
}

@keyframes toast-out {
  to {
    opacity: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .bundle-toast {
    animation-name: toast-fade-in, toast-out;
  }
}

.toolbar {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
  border-radius: var(--radius-pill);
  padding: 12px 0;
  max-width: 100%;
  overflow-x: auto;
}

.toolbar-divider {
  width: 1.5px;
  height: var(--space-md);
  background: var(--color-border-subtle);
  border-radius: var(--radius-sm);
  flex: none;
}

/* The swatches take the row's remainder and spread across it, so the gap
   between them is whatever the screen's width leaves rather than a fixed
   number that has to be re-checked against the narrowest phone.

   Top-aligned, 6px down: that lands the 24px chips a little above the tool
   buttons' centre line and leaves the selected chip's dot room to hang below
   without making the row taller (Figma node 148:429). */
.swatches {
  display: flex;
  flex: 1;
  min-width: 0;
  align-items: flex-start;
  justify-content: space-between;
  padding-left: var(--space-xs);
  padding-top: 6px;
}

.tool-btn {
  width: 40px;
  height: 40px;
  flex: none;
  padding: 0;
  border: none;
  background: transparent;
  border-radius: var(--radius-pill);
  display: flex;
  align-items: center;
  justify-content: center;
}

.tool-btn.selected {
  background: var(--color-ui-accent);
}

.tool-btn svg {
  width: 24px;
  height: 24px;
}

.tool-btn:not(.selected) svg {
  fill: var(--color-content-primary);
  stroke: var(--color-content-primary);
}

.tool-btn.selected svg {
  fill: var(--color-content-on-dark);
  stroke: var(--color-content-on-dark);
}

/* The spray glyph is an outline, so it must not take the blanket fill. */
.tool-btn svg.glyph-stroked circle {
  fill: none;
}

/* ===================== Colour swatch =====================
   Selection is a small dot BELOW the chip — explicitly not a ring (spec). The
   dot is content/primary now that the toolbar has no dark pill behind it
   (D-085); the cream swatch keeps its ring so it doesn't vanish into the
   page ground. */
.swatch {
  width: 24px;
  height: 24px;
  flex: none;
  padding: 0;
  border: none;
  border-radius: 50%;
  position: relative;
}

/* An inset shadow, not a border: an absolutely positioned child is laid out
   against its parent's PADDING box, so a 1px border pulled the selected dot
   1px up and 1px nearer the chip than on the five swatches without one. A
   shadow draws the same ring without moving that edge. */
.swatch.white-swatch {
  box-shadow: inset 0 0 0 1px var(--color-border-subtle);
}

.swatch.selected::after {
  content: "";
  position: absolute;
  left: 50%;
  bottom: -6px;
  transform: translateX(-50%);
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: var(--color-content-primary);
}

/* D-048.1: inks a sheet would swallow are withheld rather than offered. */
.swatch[hidden] {
  display: none;
}

/* ===================== Form fields ===================== */
.field {
  margin-bottom: var(--space-md);
}

.field label {
  display: block;
  font-size: 14px;
  font-weight: 700;
  margin-bottom: var(--space-xs);
}

.field select,
.field input[type="range"] {
  width: 100%;
}

.field select {
  font-family: var(--font-display);
  font-size: var(--text-body-size);
  padding: var(--space-xs) var(--space-sm);
  border: 1.5px solid var(--color-content-primary);
  border-radius: var(--radius-sm);
  background: var(--color-background);
  color: var(--color-content-primary);
}

.field input[type="range"] {
  accent-color: var(--color-ui-primary);
}

.range-value {
  font-size: 14px;
  color: var(--color-content-secondary);
}

.sketch-canvas {
  border-radius: var(--radius-lg);
  touch-action: none;
}

/* Network game: setup section, lobby, waiting screens (D-028–D-031) */
.net-section {
  margin-top: var(--space-md);
  padding-top: var(--space-md);
  border-top: 1px solid var(--color-border-subtle);
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}

.net-title {
  font-size: 14px;
  font-weight: 700;
}

.net-hint {
  font-size: 14px;
  color: var(--color-content-secondary);
  margin: 0;
}

.join-row {
  display: flex;
  gap: var(--space-xs);
}

.join-row input {
  flex: 1;
  min-width: 0;
  font-family: var(--font-display);
  font-size: var(--text-body-size);
  padding: var(--space-xs) var(--space-sm);
  border: 1.5px solid var(--color-content-primary);
  border-radius: var(--radius-sm);
  background: var(--color-background);
  text-transform: uppercase;
}

.lobby-screen,
.waiting-screen {
  justify-content: center;
  align-items: center;
  text-align: center;
  gap: var(--space-sm);
}

.screen.lobby-screen h1,
.screen.waiting-screen h1 {
  margin: 0;
}

.join-qr {
  width: 180px;
  height: 180px;
  image-rendering: pixelated;
  border-radius: var(--radius-sm);
}

.code-display {
  font-size: var(--text-headline-size);
  line-height: var(--text-headline-lh);
  font-weight: 700;
  letter-spacing: 0.3em;
  padding: var(--space-xs) var(--space-xs) var(--space-xs) var(--space-md);
  background: var(--color-accent-yellow);
  border-radius: var(--radius-md);
}

/* ===================== Landing (D-052) =====================
   Centred column from the design mockup: the face art takes whatever height
   is left after the text and button, so the page fits one screen on a phone
   and doesn't sprawl on a wide window. */
.landing-screen {
  align-items: center;
  justify-content: center;
  gap: var(--space-lg);
  text-align: center;
}

/* Capped in both directions: the width cap stops it dominating a desktop
   window, the height cap keeps the button on screen on a short phone. */
.landing-art {
  display: block;
  width: 100%;
  max-width: 420px;
  flex: 0 1 auto;
  min-height: 0;
  max-height: 50dvh;
}

/* Larger windows: bigger art and extra room between it and the title. The
   50dvh cap above still applies, so a wide but short window stays on one
   screen. */
@media (min-width: 768px) {
  .landing-art {
    max-width: 540px;
    margin-bottom: var(--space-lg);
  }
}

.landing-art .ink-teal { stroke: var(--color-accent-teal); }
.landing-art .fill-black { fill: var(--color-content-primary); }
.landing-art .fill-blue { fill: var(--color-accent-blue); }
.landing-art .fill-red { fill: var(--color-accent-red); }
.landing-art .fill-pink { fill: var(--color-accent-pink); }
.landing-art .fill-orange { fill: var(--color-accent-orange); }
.landing-art .fill-yellow { fill: var(--color-accent-yellow); }

.landing-text {
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
}

/* Headline, not Display: the 2026-10 Figma flow sets "Doodle Jam" on one
   line at 42px, where Display's 68px wraps it on a phone. */
.landing-title {
  font-size: var(--text-headline-size);
  line-height: var(--text-headline-lh);
  letter-spacing: var(--text-headline-ls);
  font-weight: 700;
  margin: 0;
  text-wrap: balance;
}

.landing-body {
  font-size: var(--text-body-size);
  line-height: var(--text-body-lh);
  color: var(--color-content-secondary);
  margin: 0;
}

.landing-actions {
  display: flex;
  flex-direction: column;
  gap: var(--space-sm);
  align-items: center;
}

/* Settings, pinned to the screen's top-left corner inside its padding.
   Outlined icon button from Figma: cream fill, 1px muted border, muted gear,
   no shadow. */
.landing-screen {
  position: relative;
}

.landing-settings {
  position: absolute;
  top: var(--space-md);
  left: var(--space-md);
}

/* Past about 1050px there is room beside the 720px app column, so the
   button moves out of it, 100px in from the window's left edge. */
@media (min-width: 1050px) {
  .landing-settings {
    position: fixed;
    left: 100px;
  }
}

.landing-settings.btn {
  background: var(--color-background);
  border: 1px solid var(--color-content-secondary);
  box-shadow: none;
}

/* The gear file is drawn in cream; using it as a mask lets it take the muted
   colour without editing the icon. */
.landing-settings .icon {
  width: 24px;
  height: 24px;
  background: var(--color-content-secondary);
  -webkit-mask: url("icons/settings.svg") center / contain no-repeat;
  mask: url("icons/settings.svg") center / contain no-repeat;
}

/* Wider side padding than the standard button, on this screen only. */
.landing-actions .btn.standard {
  padding-inline: var(--space-lg);
}

/* Vertical rhythm with the copyright line (D-102). Three auto top margins —
   on the art, the title block and the credit — split the spare height equally,
   so the title and Play stay centred between the art and the credit at every
   height, rather than following the art up as it shrinks. The top padding
   clears the gear (52px button, 24px in), so on a short screen the art shrinks
   instead of sliding under it; past 1050px the gear sits outside the column
   and the padding returns to normal. */
.landing-screen {
  padding-top: calc(var(--space-md) + 52px + var(--space-sm));
}

@media (min-width: 1050px) {
  .landing-screen {
    padding-top: var(--space-md);
  }
}

.landing-art,
.landing-text {
  margin-top: auto;
}

/* Copyright, then "Get in touch" on its own line, at the foot of landing. */
.landing-credit {
  margin-top: auto;
  display: flex;
  flex-direction: column;
  gap: var(--space-xs);
  font-size: var(--text-caption-size);
  line-height: var(--text-caption-lh);
  letter-spacing: var(--text-caption-ls);
  color: var(--color-content-secondary);
}

/* From 768px the art keeps an extra --space-lg below it; the credit gets the
   same above it so the title block stays centred between them. */
@media (min-width: 768px) {
  .landing-credit {
    padding-top: var(--space-lg);
  }
}

.landing-credit p {
  margin: 0;
}

.landing-credit a {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 2px;
}

/* Hand-off between real players' turns (D-027, see screens/draw.js) */
.handoff-screen {
  justify-content: center;
  align-items: center;
  text-align: center;
  gap: var(--space-sm);
  cursor: pointer;
}

.handoff-title {
  font-size: var(--text-headline-size);
  line-height: var(--text-headline-lh);
  letter-spacing: var(--text-headline-ls);
  font-weight: 700;
  margin: 0;
}

.handoff-hint {
  font-size: var(--text-body-size);
  color: var(--color-content-secondary);
  margin: 0;
}

/* Draw screen: the sheet carousel with one condensed action row below
   (D-056, see screens/draw.js). Nothing here scrolls — the stage takes what
   the chrome leaves and the card is measured into it. */
.draw-screen {
  /* Spacing from the 2026-10 Figma frame (D-072): at least 16px between
     every block, more where the frame groups things apart. */
  gap: var(--space-sm);
  padding: var(--space-sm) var(--space-sm) var(--space-md);
  overflow: hidden;
  min-height: 0;
  /* D-065: nothing here is text to select, and a drag that begins as a
     selection is a drag that does not draw. Scoped to this screen on purpose —
     the lobby's join code must stay selectable so it can be copied. */
  -webkit-user-select: none;
  user-select: none;

  /* The measured card, written by sizeCards(); these are only the values
     before the first measurement. They sit on the screen rather than the
     stage because the header is aligned to the card as well as the cards
     being sized by it (D-080). */
  --card-w: 210px;
  --card-h: 280px;
}

/* The header group (D-072): round counter above, then the prompt with the
   sheet chip beside it. 32px below the top bar (gap + margin) and inset 16px
   further than the rest of the screen, as in Figma. */
.draw-header {
  display: flex;
  flex-direction: column;
  gap: 8px;
  flex: none;
  margin-top: var(--space-sm);
  padding-inline: var(--space-sm);
}

.draw-title-row {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: var(--space-xs);
}

.screen .draw-header h1 {
  margin: 0;
  font-size: var(--text-title-size);
  line-height: var(--text-title-lh);
  letter-spacing: var(--text-title-ls);
  min-width: 0;
}

/* Figma's Type/Caption: bold and in text/primary on the draw screen, where
   it is a label rather than the muted hint it is elsewhere. */
.draw-header .round-label {
  font-weight: 700;
  color: var(--color-content-primary);
}

.round-label {
  font-size: var(--text-caption-size);
  line-height: var(--text-caption-lh);
  letter-spacing: var(--text-caption-ls);
  font-weight: 500;
  color: var(--color-content-secondary);
}

/* ================= Sheet carousel (D-056, D-079, D-080) =================
   The slots one sheet can be in, decided by src/carousel.js:

     wing-left   far-left      left       FRONT       right      far-right   wing-right
    (off stage) (ring 2 only) (peek)    (selected)    (peek)  (ring 2 only) (off stage)

   A card always leaves towards where it is going and arrives from the opposite
   wing, so nothing is parked at the centre and nothing fades in on top of the
   selected sheet.

   D-080 widened the strip: --ring is how many cards each side are on stage, 1
   or 2, and the wings sit one card beyond the outermost of them. At ring 2 the
   ±2 pair is the desktop mock's five cards.

   D-079 made this a plain strip. Until then the neighbours sat BEHIND the
   front card — the offset was 0.571 card-widths where cards only stop
   overlapping at (1 + scale) / 2 — and were rotated, so the carousel read as a
   leaning stack. Now each card sits beside the last with a real gap, square to
   the screen, and only the front card is raised. */
.sheets-stage {
  flex: 1;
  position: relative;
  /* clip, not hidden: the front card's shadow has to escape vertically or the
     stage chops the elevation off (D-079). `clip` also means no scroll
     container, so the off-stage wings can never be scrolled into view. */
  overflow-x: clip;
  overflow-y: visible;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 0;
  background: var(--color-background);
  /* One finger draws; navigation is a tap, never a drag (D-060), so the
     browser must not claim the gesture for panning. */
  touch-action: none;

  /* Gap between one card and the next. CARD_GAP in src/carousel.js mirrors it
     and sizeCards() reads this value back, so the number lives here only. */
  --card-gap: var(--space-sm);

  /* How many cards each side of the front one are on stage. --ring-max is the
     layout's ceiling; sizeCards() clamps it (full screen has no neighbours at
     all) and writes the result back as --ring, which is what the positions
     below are built from. */
  --ring-max: 1;
  --ring: 1;

  /* An off-centre card is the same card, uniformly smaller, centred on the
     front card's axis — so it is shorter as well as narrower (D-079). 0.792 is
     the desktop mock's 336x503 over its 424x635 front card. The mobile mock
     draws 0.967, near enough to no difference at all; the brief asked for
     off-centre cards to be visibly shorter, so the desktop value is used at
     every width. */
  --peek-scale: 0.792;

  /* Half of the front card, the gap, then half of the neighbour: the first
     offset at which the two do not overlap. Derived rather than tabulated, so
     it survives any change to --peek-scale or the card's shape. */
  --peek-x: calc((var(--card-w) * (1 + var(--peek-scale))) / 2 + var(--card-gap));

  /* One more card-and-gap out: the ±2 pair, on stage at ring 2 (D-080). */
  --far-x: calc(var(--peek-x) + var(--card-w) * var(--peek-scale) + var(--card-gap));

  /* And one beyond the outermost card that IS on stage, so the wings clear the
     strip at either ring. They are transparent, so this only has to put a card
     far enough away that it is clear of the stage before it fades. */
  --wing-x: calc(
    var(--peek-x) + var(--ring) * (var(--card-w) * var(--peek-scale) + var(--card-gap))
  );

  /* Settled by the 2026-09-06 playtest. Keep --carousel-dur in step with
     STEP_MS in screens/draw.js, which schedules multi-sheet jumps against it,
     and with .bundle-toast's --toast-wait, which waits out the cards' entrance.
     D-061: there is deliberately no prefers-reduced-motion variant — the
     animation is how the carousel explains where a sheet went, and removing it
     brings back the "magic pop" the 2026-09-05 playtest rejected. If that ever
     needs revisiting, the alternative is a SHORTER motion, not none. */
  --carousel-dur: 260ms;
  --carousel-ease: cubic-bezier(0.22, 0.61, 0.36, 1);
}

/* A zero-size anchor at the middle of the stage; cards position against it. */
.sheets-track {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 0;
  height: 0;
}

/* The stage cancels the screen's side padding at every width, so the carousel
   bleeds to the edge of the app frame instead of stopping 16px short (D-079).
   The card is measured from the stage, so it widens to match.

   The frame no longer caps on this screen (D-080), so on a wide window that
   edge is the window's own. */
.draw-screen:not(.is-fullscreen) .sheets-stage {
  margin-inline: calc(-1 * var(--space-sm));
}

/* Only the front card is raised (D-079). D-048.2 gave every card shadow/soft
   because the stage ground is the app's cream and three sheet colours barely
   separate from it (yellow 1.42:1, pink 1.96:1) — so a pale neighbour now has
   no edge against the background. That is the mock as drawn and was chosen
   knowingly; a hairline on the neighbours is the fallback if it reads badly.
   The shadow is transitioned with the move, so it lifts as a card arrives
   rather than snapping on. */
.sheet-card {
  position: absolute;
  width: var(--card-w);
  height: var(--card-h);
  margin-left: calc(var(--card-w) / -2);
  margin-top: calc(var(--card-h) / -2);
  border-radius: var(--radius-md);
  box-shadow: none;
  overflow: hidden;
  transition: transform var(--carousel-dur) var(--carousel-ease),
    opacity var(--carousel-dur) var(--carousel-ease),
    box-shadow var(--carousel-dur) var(--carousel-ease);
  will-change: transform;
}

.sheet-card[data-slot="front"] {
  transform: translateX(0) scale(1);
  opacity: 1;
  z-index: 3;
  box-shadow: var(--shadow-soft);
}
.sheet-card[data-slot="left"] {
  transform: translateX(calc(-1 * var(--peek-x))) scale(var(--peek-scale));
  opacity: 1;
  z-index: 2;
  cursor: pointer;
}
.sheet-card[data-slot="right"] {
  transform: translateX(var(--peek-x)) scale(var(--peek-scale));
  opacity: 1;
  z-index: 2;
  cursor: pointer;
}
/* The ±2 pair. On stage only at ring 2 — src/carousel.js hands a card one of
   these slots only then, so these rules need no media query of their own. They
   are the same size as the ±1 pair: every off-centre card is the one uniform
   --peek-scale smaller (D-079), so the strip steps in position alone. Tapping
   one travels the two sheets it is away (D-056/D-080). */
.sheet-card[data-slot="far-left"] {
  transform: translateX(calc(-1 * var(--far-x))) scale(var(--peek-scale));
  opacity: 1;
  z-index: 2;
  cursor: pointer;
}
.sheet-card[data-slot="far-right"] {
  transform: translateX(var(--far-x)) scale(var(--peek-scale));
  opacity: 1;
  z-index: 2;
  cursor: pointer;
}

/* The wings keep the neighbour's size: a card entering the strip then changes
   only its position and its opacity, never its scale as well. */
.sheet-card[data-slot="wing-left"] {
  transform: translateX(calc(-1 * var(--wing-x))) scale(var(--peek-scale));
  opacity: 0;
  z-index: 1;
  pointer-events: none;
}
.sheet-card[data-slot="wing-right"] {
  transform: translateX(var(--wing-x)) scale(var(--peek-scale));
  opacity: 0;
  z-index: 1;
  pointer-events: none;
}

/* The card that has to cross the strip rather than step along it
   (src/carousel.js `isJump`). It takes its new position with no transition,
   so it is never seen travelling against the strip — the artefact a bundle
   too small to keep a card in both wings would otherwise show (D-080.1).
   It is held invisible for the first half of the move and then comes up,
   which is the wing's fade borrowed for a crossing that has no wing to make
   it in. The keyframes name no end state on purpose: the card comes up to
   whatever its destination slot is worth, so a crossing that does end in a
   wing stays invisible throughout. */
@keyframes card-crossing {
  from,
  60% {
    opacity: 0;
  }
}

.sheet-card.is-jumping {
  transition-property: opacity, box-shadow;
  animation: card-crossing var(--carousel-dur) var(--carousel-ease);
}

/* D-083: the card paints the sheet's own ground, set inline from the sheet.
   It is behind the canvas and never visible through it, so this is not about
   colour — it is about the card not being a transparent, rounded, composited
   clip. .sheet-card carries border-radius, overflow: hidden and
   will-change: transform around a canvas on its own layer; with nothing
   opaque behind it, a dropped frame on the corner mask paints the bare layer
   and a black wedge appears at a corner. Until D-081 the .sheet-cover div
   happened to provide that opaque rect; removing it left the card
   transparent. The canvas rounds itself too, so the corner does not rest on
   the parent's clip alone. */
.sheet-card canvas {
  border-radius: inherit;
}

/* The buffer is always the sheet's own width/height attributes — the shared
   coordinate space every device agrees on (D-029) — and the CSS box is
   whatever sizeCards() measured. The card matches the buffer's aspect, so that
   scaling stays uniform and no face is squashed (D-062). */
.sheet-card canvas {
  display: block;
  width: 100%;
  height: 100%;
  touch-action: none;
  /* D-065: the WebKit half of the same guard — Safari will happily lift a
     canvas out of the page on a long press. The `dragstart` handler in
     draw.js is what covers Gecko, where this property does not exist. */
  -webkit-user-drag: none;
}

/* Only the front sheet takes ink. On a neighbour the canvas is transparent to
   the pointer so the tap reaches the card and navigates instead (D-056). */
.sheet-card:not([data-slot="front"]) canvas {
  pointer-events: none;
}



/* ================== The desktop draw screen (D-080) ==================
   From 1280px up the screen is the 2026-10 desktop mock: the window's whole
   width, a strip of five cards running off both edges, and the wider gap
   Figma draws between them. The header's alignment to the card is the mock's
   too, but it starts lower down — see the 721px query below (D-089).

   1280 rather than D-041's 720px cap, because a five-card strip is paid for
   out of the front card: at 720 it would leave the card under half the width
   it has with one card each side, while at 1280x900 it keeps five sixths of
   it (362px against 431px) and the mock's own 1440 sits comfortably inside.
   Media queries cannot read --app-max-width, so the number is written out. */
@media (min-width: 1280px) {
  .sheets-stage {
    --card-gap: var(--space-md);
    --ring-max: 2;
  }
}

/* Figma aligns the title and its "i of M" chip to the front card's left edge —
   title frame at x=509 against a card at x=508 (node 148:687) — rather than to
   the window, so the header takes the card's width and centres with it.

   From 430px, not D-080's 1280 (D-089): the card is where the title belongs at
   every width the frames draw it on, so the only question is where the phone
   stops. 430 is the widest phone (the frames' 402 plus headroom), and below it
   the phone layout stands with the header inset 16px further than the top bar
   (D-072). */
@media (min-width: 430px) {
  .draw-screen:not(.is-fullscreen) .draw-header {
    width: var(--card-w);
    margin-inline: auto;
    padding-inline: 0;
  }
}


/* Undo, and Submit once every card carries ink (D-086). In full screen it is
   Start fresh and Undo instead — one row of standard buttons, as the frame
   draws it (D-088); Clear still lives in full screen only (D-072). Centred, so
   the row re-centres on whichever pair it is carrying. */
.action-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-xs);
  flex-wrap: wrap;
  flex: none;
  min-width: 0;
}

/* Figma gives Submit a fixed 110px (node 148:559), which is wider than the
   label needs; as a minimum it holds that shape without clipping a longer
   one. Even padding, unlike .standard's 16/24, is the frame's own. */
#submit-btn {
  min-width: 110px;
  padding: var(--space-sm);
}

/* Start fresh shares the action row but belongs to full screen alone (D-072),
   so the draw screen never shows it. A class, not [hidden]: the row's members
   are chosen by which mode the screen is in, not by game state the way
   Submit's gate is. */
#start-fresh-btn {
  display: none;
}

.is-fullscreen #start-fresh-btn {
  display: inline-flex;
}

/* ===================== Draw chrome (D-069) =====================
   From the 2026-10 Figma flow: icon buttons top left and right, the round's
   prompt as the title with the "i of M" chip beside it, and Next (or ←/Next)
   along the bottom. */
.draw-screen {
  position: relative;
}

.draw-topbar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-xs);
  flex: none;
}

.draw-topbar-actions {
  display: flex;
  align-items: center;
  gap: var(--space-xs);
}

.draw-header {
  position: relative;
  z-index: 5; /* above the stage, so the chip menu can overlap the card */
}

.page-chip {
  flex: none;
  border: none;
  border-radius: var(--radius-pill);
  padding: 8px var(--space-sm);
  font-family: var(--font-display);
  font-size: var(--text-caption-size);
  line-height: var(--text-caption-lh);
  letter-spacing: var(--text-caption-ls);
  font-weight: 700;
  color: var(--color-content-primary);
}

/* The chip is a plain button surface; open, it takes the pressed shade so it
   reads as the control its menu hangs from (D-069). */
.page-chip {
  background: var(--color-surface-button);
}

.page-chip[aria-expanded="true"] {
  background: var(--color-surface-button-pressed);
}

.page-menu button.on-dark,
.fs-btn.on-dark {
  color: var(--color-background);
}

/* Every sheet as a dot in its ground colour. The ring (an outline offset from
   the dot) is the sheet in front; the check is a sheet drawn on this turn. */
.page-menu {
  position: absolute;
  right: var(--space-sm);
  top: calc(100% + 8px);
  z-index: 10;
  display: grid;
  grid-template-columns: repeat(var(--cols, 3), 32px);
  gap: var(--space-md);
  /* 20px top / 24px bottom, per Rachel's 2026-10-15 review. */
  padding: 20px var(--space-md) var(--space-md);
  border-radius: var(--radius-md);
  background: var(--color-surface-button);
  box-shadow: var(--shadow-soft);
}

.page-menu[hidden] {
  display: none;
}

.page-menu button {
  width: 32px;
  height: 32px;
  padding: 0;
  border: none;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--color-content-primary);
}

.page-menu button[aria-current="true"] {
  outline: 2px solid var(--color-content-primary);
  outline-offset: 4px;
}

.page-menu button svg {
  width: 20px;
  height: 20px;
  fill: currentColor;
  visibility: hidden;
}

.page-menu button.drawn svg {
  visibility: visible;
}

/* Full-screen toggle, on the front card only. */
.fs-btn {
  position: absolute;
  top: var(--space-xs);
  right: var(--space-xs);
  z-index: 2;
  width: 36px;
  height: 36px;
  padding: 6px;
  border: none;
  background: transparent;
  color: var(--color-content-primary);
  display: none;
}

.sheet-card[data-slot="front"] .fs-btn {
  display: block;
}

.fs-btn svg {
  width: 24px;
  height: 24px;
  fill: currentColor;
}

.fs-btn .fs-exit,
.is-fullscreen .fs-btn .fs-enter {
  display: none;
}

.is-fullscreen .fs-btn .fs-exit {
  display: inline;
}

/* ===================== Full screen (D-070) =====================
   Same card and canvas; the chrome goes and the stage takes the room. */
.draw-screen.is-fullscreen {
  background: var(--color-content-primary);
}

.is-fullscreen .draw-topbar,
.is-fullscreen .draw-header {
  display: none;
}

/* Full screen is for working one card; the turn is submitted from the draw
   screen, where the whole bundle is in view (D-086). */
.is-fullscreen #submit-btn {
  display: none;
}

/* The canvas runs to the window's edges and the palette sits straight under
   it, so the screen keeps only its bottom padding; the toolbar's own 12px of
   top padding is the air between the two (node 114:3351 butts them together).
   D-072's 16-64px rhythm is the draw screen's, not this one's — here the room
   it would take goes to the card. The action row puts its 16px back. */
.draw-screen.is-fullscreen {
  padding: 0 0 var(--space-md);
  gap: 0;
}

.is-fullscreen .action-row {
  margin-top: var(--space-sm);
}

.is-fullscreen .sheets-stage {
  background: transparent;
}

/* Full-bleed, as in Figma: no rounding where the card meets the screen edge. */
.is-fullscreen .sheet-card[data-slot="front"],
.is-fullscreen .sheet-card[data-slot="front"] canvas {
  border-radius: 0;
}

/* D-088: ...except when it doesn't. The card is 3:4 (D-064), so on a window
   wider than that it stops at the height the chrome leaves and the black shows
   either side. Standing on the ground like that it is a sheet, and a square
   corner on it reads as a crop, so it rounds — at radius/md, as every card
   does since D-092.1. Whether it touches an edge is a fact about the measured card, not about
   the viewport, so `is-inset` comes from sizeCards() rather than from a media
   query (draw.js). */
.is-fullscreen.is-inset .sheet-card[data-slot="front"],
.is-fullscreen.is-inset .sheet-card[data-slot="front"] canvas {
  border-radius: var(--radius-md);
}

/* A rounded corner needs somewhere to turn: the card is as tall as the screen
   lets it be, so without this it meets the top edge and two of its corners are
   rounded against the window's own. The 16px costs the card about that much
   height and buys it back at the sides, since the shape is fixed. */
.draw-screen.is-fullscreen.is-inset {
  padding-top: var(--space-sm);
}

/* D-088: no cream strip. The palette fills the row and insets its toolbar 16px
   — the frame's 8px row padding plus the toolbar's own 8px, collapsed into one
   the way D-085 collapsed them on the draw screen; the screen has no padding
   here for them to collapse into, so the row carries it. Everything else is
   the row as the draw screen has it, 402px cap included: full screen keeps the
   phone's palette rather than stretching one across a desktop window, for the
   reason D-085 gave — six swatches spaced across 1440px are a row of six
   unrelated dots. The toolbar keeps its 12px top and bottom, which is what
   separates it from the canvas above. */
.is-fullscreen .toolbar-row {
  padding-inline: var(--space-sm);
  margin-bottom: 0;
}

/* With the strip gone the row sits on the black ground, so everything D-085
   turned dark for the cream page turns back over: the tool glyphs, the
   divider (the frame's grey, which is what --color-border holds) and the
   selected swatch's dot. The cream swatch drops its ring — it had one so it
   would not vanish into the cream page, and here it is the lightest thing on
   the screen. A selected tool is the accent pill at either ground, so it is
   not restated. */
.is-fullscreen .tool-btn:not(.selected) svg {
  fill: var(--color-content-on-dark);
  stroke: var(--color-content-on-dark);
}

.is-fullscreen .toolbar-divider {
  background: var(--color-border);
}

.is-fullscreen .swatch.selected::after {
  background: var(--color-content-on-dark);
}

.is-fullscreen .swatch.white-swatch {
  box-shadow: none;
}

.is-fullscreen .sheet-card:not([data-slot="front"]) {
  visibility: hidden;
}

/* ===================== Bundle gallery (D-071) ===================== */
.bundle-gallery {
  position: absolute;
  inset: 0;
  z-index: 20;
  display: flex;
  flex-direction: column;
  /* Same padding as .draw-screen, so the top bar does not move when the
     gallery opens over it; 32px down to the grid, like the draw header. */
  gap: var(--space-lg);
  padding: var(--space-sm) var(--space-sm) var(--space-md);
  background: var(--color-background);
  /* The grid is the one thing that scrolls; the top bar stays (D-095). */
  overflow: hidden;
}

.bundle-gallery[hidden] {
  display: none;
}

.bundle-gallery-title {
  margin: 0;
  font-size: var(--text-title-size);
  line-height: var(--text-title-lh);
  font-weight: 700;
}

/* D-095 (superseding D-082's fit-everything view): both galleries are a
   scrolling grid. The column count steps with the window — two on a phone,
   three from 600px, four from 900px — and the cards share whatever width the
   page margins leave. The margin is min(100px, 8.5vw): the two Figma frames
   (nodes 197:480 and 231:711) eyeball about 35px at 402 wide and 101 at 1189,
   and that line runs through both, holding at 100 from about 1180px.

   The grid is the scroller and spans its screen's full width — the negative
   margins undo the screen's own padding — so the scrollbar sits at the window
   edge and the margin is measured from it. Each screen says how much padding
   to undo with --gallery-undo-x/--gallery-undo-bottom. */
.gallery-grid {
  --gallery-gap: var(--space-sm);
  --gallery-margin: min(100px, 8.5vw);
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  /* Where a scrollbar takes room, take the same on the left so the margins
     stay equal. */
  scrollbar-gutter: stable both-edges;
  margin: 0 calc(-1 * var(--gallery-undo-x, 0px)) calc(-1 * var(--gallery-undo-bottom, 0px));
  padding: 0 var(--gallery-margin) var(--gallery-pad-bottom, var(--space-md));
}

.bundle-gallery .gallery-grid {
  --gallery-undo-x: var(--space-sm);
  --gallery-undo-bottom: var(--space-md);
}

/* Flex-wrap rather than a grid: a grid leaves an incomplete last row hugging
   the left, because its cards still sit in columns. Wrapping rows centre
   themselves, so two cards under three sit under the middle of them
   (D-082.1, kept). */
.gallery-rows {
  --gallery-cols: 2;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  /* Cards keep their own height rather than stretching to the row's. */
  align-items: flex-start;
  gap: var(--gallery-gap);
}

@media (min-width: 600px) {
  .gallery-rows {
    --gallery-cols: 3;
  }
}

@media (min-width: 900px) {
  .gallery-rows {
    --gallery-cols: 4;
  }
}

/* The end-of-game gallery (D-019) is the same grid on its own screen, and it
   leaves D-041's 720px column as the draw screen does, so a wide window
   reaches four columns (D-095). The headline stays at the top and Play again
   floats over the bottom of the grid; the grid's bottom padding is what lets
   its last row scroll clear of the button: the button's 52px and 6px
   shadow, its 24px inset, and 16px of air. */
#phone-frame:has(.gallery-screen) {
  max-width: none;
}

.gallery-screen {
  position: relative;
  overflow: hidden;
  gap: var(--space-md);
}

.gallery-screen #gallery-headline {
  margin: 0;
}

.gallery-screen .gallery-grid {
  --gallery-undo-x: var(--space-md);
  --gallery-undo-bottom: var(--space-md);
  --gallery-pad-bottom: calc(52px + 6px + var(--space-md) + var(--space-sm));
}

/* Play again is the standard hero button, as landing's Play (D-092). Placed
   with auto margins rather than a translate, which the press state owns. */
.gallery-screen #restart-btn {
  position: absolute;
  left: 0;
  right: 0;
  bottom: var(--space-md);
  width: max-content;
  margin-inline: auto;
}

/* Aspect follows the finished sheet's own dimensions (D-044 preset). No
   elevation here: in the gallery every card is equal, so there is no focused
   one for a shadow to single out (D-082.1). Radius/md, as every card
   (D-092.1). */
.gallery-grid canvas {
  width: calc((100% - (var(--gallery-cols) - 1) * var(--gallery-gap)) / var(--gallery-cols));
  height: auto;
  flex: none;
  border-radius: var(--radius-md);
  box-shadow: none;
  cursor: pointer;
}

/* Quit confirmation (D-032, D-075): an in-app dialog over the whole app. */
.confirm-scrim {
  position: absolute;
  inset: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-md);
  background: var(--color-scrim);
}

.confirm-dialog {
  width: 100%;
  max-width: 320px;
  padding: var(--space-md);
  border-radius: var(--radius-lg);
  background: var(--color-background);
  box-shadow: var(--shadow-soft);
  text-align: center;
}

.confirm-dialog h2 {
  margin: 0 0 8px;
  font-size: var(--text-title-size);
  line-height: var(--text-title-lh);
  font-weight: 700;
}

.confirm-dialog p {
  margin: 0 0 var(--space-md);
  color: var(--color-content-secondary);
}

.confirm-actions {
  display: flex;
  justify-content: center;
  gap: var(--space-xs);
}

.lightbox[hidden] {
  display: none;
}

/* Fixed, not absolute: the gallery sits in D-041's 720px column, and the
   scrim covers the whole window rather than stopping at the column (D-094).
   The sketch's size is set by paintEnlarged() in gallery.js. */
.lightbox {
  position: fixed;
  inset: 0;
  z-index: 40;
  background: var(--color-scrim);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: var(--space-md);
  cursor: pointer;
}

.lightbox canvas {
  flex: none;
  border-radius: var(--radius-md);
}
