/* Undercover — a playing card laid on a felt table, one phone passed around.
   The art direction lives in design/ ; "Undercover Prototype.dc.html" is the
   reference. Four motifs carry the identity:
     - the lozenge: a square turned 45deg, never a border-radius, never a glyph;
     - stacked inset shadows that draw the three edges of a playing card;
     - a diagonal hatch, on the felt and on the back of the card, nothing else;
     - elements duplicated at 180deg for the players sitting opposite.
   Every colour comes from the custom properties below (CONTRAT §9). */

/* Jost, self-hosted rather than fetched from Google Fonts.
   The game is meant to work with no signal at all, and a third-party <link> is
   a request that fails offline. The face is not decorative either: the size of
   the secret word is derived from the advance of the real Jost 600 (CONTRAT
   §9), so falling back would resize it.

   ONE file, and that is not a shortcut: Google serves Jost as a VARIABLE font
   and hands out the same woff2 for 400, 500 and 600. So the weight is declared
   as the range the stylesheet actually uses, and nothing wider.

   unicode-range is Google's `latin` subset, copied verbatim. It is what the
   page already used, so nothing changes: the only two characters of the
   interface outside it are the vote tick (U+2713) and the arrow (U+2192), and
   both fell back to a system face before this and still do. */
@font-face {
  font-family: "Jost";
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url("../fonts/jost-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304,
    U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

:root {
  /* --- surfaces ------------------------------------------------------- */
  --bg: #c9c4b6; /* the felt, outside the card */
  --surface: #fbf9f4; /* the card itself, and every field at rest */
  --surface-fade: rgba(251, 249, 244, 0); /* same ivory, alpha 0, for gradients */
  --surface-2: #f4efe4; /* pressed surfaces: hold zone, active step buttons */
  --surface-picked: #f7eeee; /* the chosen vote option */
  --scrim: rgba(18, 16, 14, 0.5); /* the veil under the abandon dialog */

  /* --- ink, strongest to faintest ------------------------------------- */
  --text: #12100e;
  --text-2: #5e574e;
  --text-dim: #7a736a;
  --text-faint: #a9a197;
  --text-ghost: #c0b9ae; /* a dead player's name in the peek layer */
  --text-off: #c9c0ae; /* the glyph of a disabled step button */

  /* --- accent and roles ------------------------------------------------ */
  --accent: #b0182b; /* undercover, danger, focus: one accent, three jobs */
  --civil: #17624a;
  --undercover: #b0182b;
  --white: #2f2a24;
  --danger: #b0182b;

  /* --- hairlines, edges, ornaments ------------------------------------- */
  --rule: #ebe3d4; /* row separators, printed fillet of the card */
  --edge: #e4dccb; /* the edge of the card, of every field and outline */
  --ornament: #d5ccbb; /* the lozenge of a separator, the upside-down rank */
  --ornament-2: #c9bfa9; /* ornament axes, the rule under the secret word */
  --ring-face: #e8cdca; /* rose ring, card face up */
  --ring-back: #edd6d4; /* rose ring, card face down */

  /* --- textures and shadows -------------------------------------------- */
  --hatch-light: rgba(255, 255, 255, 0.16);
  --hatch-dark: rgba(0, 0, 0, 0.05);
  --hatch-back: rgba(176, 24, 43, 0.16);
  --shadow-card: rgba(0, 0, 0, 0.18);
  --shadow-lift: rgba(60, 45, 30, 0.3);

  /* --- one family, three weights: 400, 500, 600 ------------------------- */
  --font: "Jost", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;

  /* --- the one motion vocabulary --------------------------------------- */
  --ease-settle: cubic-bezier(0.22, 0.9, 0.28, 1); /* anything being laid down */
  --ease-door: cubic-bezier(0.45, 0, 0.2, 1); /* the back of the card turning */
  --ease-focus: cubic-bezier(0.3, 0, 0.2, 1); /* the word coming into focus */
  --ease-confetti: cubic-bezier(0.2, 0.7, 0.3, 1);

  color-scheme: light;
}

/* Role colour as a single inherited property: the modifier is put on the
   screen root as well as on the label, so the corner lozenges, the frame and
   the name all read the same value without ever naming a colour twice. */
.role--civil {
  --role: var(--civil);
}

.role--undercover {
  --role: var(--undercover);
}

.role--white {
  --role: var(--white);
}

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

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

/* ------------------------------------------------------------ the felt */

body {
  margin: 0;
  height: 100dvh;
  padding: calc(9px + env(safe-area-inset-top)) 9px calc(9px + env(safe-area-inset-bottom));
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: var(--bg);
  /* Period 4px, stroke 1px. White at 16 % climbing, black at 5 % falling:
     the asymmetry reads as felt rather than as graph paper. */
  background-image:
    repeating-linear-gradient(45deg, var(--hatch-light) 0 1px, transparent 1px 4px),
    repeating-linear-gradient(-45deg, var(--hatch-dark) 0 1px, transparent 1px 4px);
  color: var(--text);
  font-family: var(--font);
  font-size: 17px;
  line-height: 1.4;
  -webkit-font-smoothing: antialiased;
  overscroll-behavior: none;
}

/* ------------------------------------------------------------ the card */

/* Three edges, not one: the ivory margin between two sand fillets is the only
   thing that tells a playing card from a white rectangle. */
#app {
  position: relative;
  width: 100%;
  max-width: 396px;
  height: 100%;
  max-height: 882px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border-radius: 16px;
  background: var(--surface);
  box-shadow:
    0 1px 3px var(--shadow-card),
    inset 0 0 0 1px var(--edge),
    inset 0 0 0 7px var(--surface),
    inset 0 0 0 8px var(--rule);
}

.noscript {
  padding: 1.5rem;
}

/* ---------------------------------------------------------------- motion */

/* The card being laid on the table. Every screen is built from scratch, so
   every screen is dealt: the 1.2deg is what makes it read as a card and not
   as a panel appearing. */
@keyframes cardSettle {
  from {
    opacity: 0;
    transform: translateY(10px) rotate(1.2deg);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* A hand of cards being spread. */
@keyframes rowIn {
  from {
    opacity: 0;
    transform: translateY(9px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* The word coming into focus. Opacity is never touched: the word is fully
   opaque on the frame it enters the document. Only sharpness changes, timed
   on the 190 ms of the door, so it reads as an object entering the field. */
@keyframes wordFocus {
  from {
    filter: blur(16px);
  }
  60% {
    filter: blur(3px);
  }
  to {
    filter: blur(0);
  }
}

/* The two corner indices arrive after the card is down, and offset from each
   other: that lag is what makes the symmetry legible. Opacity only — the
   second copy carries a 180deg transform it must not lose. */
@keyframes markIn {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes peekIn {
  from {
    opacity: 0;
    transform: scale(0.97);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

/* The 70 % plateau is what makes it read as twelve objects flying then
   vanishing, instead of a soft fade. */
@keyframes confettiFly {
  from {
    transform: translate(0, 0) rotate(45deg) scale(1);
    opacity: 1;
  }
  70% {
    opacity: 1;
  }
  to {
    transform: translate(var(--dx), var(--dy)) rotate(var(--rot)) scale(0.35);
    opacity: 0;
  }
}

/* Every animation is declared `both`, so switching them off leaves each
   element at its end state: opaque, unshifted, sharp. Nothing disappears.
   The door of the card is a transition, not an animation, so it is turned
   into a plain fade here — the direction promised "nothing turns". */
@media (prefers-reduced-motion: reduce) {
  * {
    animation: none !important;
  }

  .hold-zone__door {
    transition: opacity 120ms linear !important;
    transform: none !important;
  }
}

/* ---------------------------------------------------------------- layout */

.screen {
  flex: 1 1 auto;
  min-width: 0;
  min-height: 0;
  position: relative;
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 22px 20px 8px;
  animation: cardSettle 260ms var(--ease-settle) both;
}

.screen--reveal,
.screen--elimination {
  gap: 14px;
  padding: 26px 22px 8px;
}

.screen--whiteGuess {
  gap: 14px;
  padding: 24px 22px 8px;
}

/* No abandon bar on these two, so they carry their own bottom padding. */
.screen--over {
  padding-bottom: 20px;
}

.screen--resume {
  gap: 14px;
  padding: 26px 22px 36px;
}

.screen--fallback {
  padding-bottom: 20px;
}

.spacer {
  flex: 1 1 auto;
  min-height: 0;
}

/* --------------------------------------------------------- the lozenge */

/* The signature mark. Always a square turned 45deg with a flat fill —
   never a border-radius, never a glyph, never an SVG. */
.rule-mark {
  display: flex;
  align-items: center;
  gap: 10px;
}

.rule-mark::before,
.rule-mark::after {
  content: "";
  flex: 1;
  height: 1px;
  background: var(--rule);
}

.rule-mark__diamond {
  width: 6px;
  height: 6px;
  flex: none;
  background: var(--ornament);
  transform: rotate(45deg);
}

.rule-mark--wide .rule-mark__diamond {
  width: 7px;
  height: 7px;
}

/* Corner lozenges always come in a diagonal pair, never on all four corners:
   a playing card turned 180deg is identical to itself. */
.corner {
  position: absolute;
  width: 11px;
  height: 11px;
  background: var(--role, var(--text));
  transform: rotate(45deg);
  pointer-events: none;
}

.corner--a {
  top: 20px;
  left: 20px;
}

.corner--b {
  bottom: 20px;
  right: 20px;
}

.screen--whiteGuess .corner {
  background: var(--white);
}

.screen--over .corner {
  width: 10px;
  height: 10px;
}

.screen--over .corner--a {
  top: 14px;
  left: 18px;
}

.screen--over .corner--b {
  bottom: 14px;
  right: 18px;
}

/* The resume screen takes the most neutral colour in the system: nothing
   here should catch the eye of someone walking past the table. */
.screen--resume .corner {
  width: 10px;
  height: 10px;
  background: var(--ornament-2);
}

.screen--resume .corner--a {
  top: 15px;
  left: 20px;
}

.screen--resume .corner--b {
  bottom: 15px;
  right: 20px;
}

/* --------------------------------------------------- the ornament axis */

/* Three screens are sparse (vote, whiteGuess, resume). A column of lozenges
   on a hairline holds the vertical symmetry, reads the same upside down, and
   fills the void without saying anything. It absorbs the free space and is
   clipped, so it can never push a button off the screen. */
.axis {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
}

/* Wide enough for the diagonal of the largest lozenge: a 26px square turned
   45deg spans ~37px, and `overflow: hidden` — which is there to crop the
   column vertically — would otherwise shave its two side corners off. */
.axis__column {
  width: 44px;
  height: 100%;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-between;
}

.axis__line {
  flex: 1 1 auto;
  min-height: 0;
  width: 1px;
  background: var(--ornament-2);
}

.axis__mark {
  flex: none;
  width: 12px;
  height: 12px;
  margin: 4px 0;
  background: var(--ornament-2);
  transform: rotate(45deg);
}

.axis__mark--accent {
  background: var(--accent);
}

.axis__mark--big {
  width: 20px;
  height: 20px;
  margin: 8px 0;
}

/* The only hollow lozenge in the system: the word Mr. White has not got. */
.axis__mark--hollow {
  width: 26px;
  height: 26px;
  margin: 10px 0;
  background: none;
  border: 1px solid var(--text-dim);
}

.axis__column--vote {
  max-height: 210px;
  opacity: 0.32;
}

.axis__column--guess {
  max-height: 180px;
  opacity: 0.3;
}

.axis__column--resume {
  max-height: 160px;
  opacity: 0.28;
}

.axis__column--resume .axis__mark {
  width: 14px;
  height: 14px;
  margin: 5px 0;
}

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

.title {
  margin: 0;
  font: 600 40px/1 var(--font);
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

.subtitle {
  margin: 0;
  font: 400 14px/1.4 var(--font);
  letter-spacing: 0.06em;
  color: var(--text-dim);
}

.hint {
  margin: 0;
  font: 400 15px/1.5 var(--font);
  letter-spacing: 0.02em;
  color: var(--text-dim);
}

/* The round marker, printed at both edges so the table reads it from either
   side. The second copy is the same node turned 180deg (see .flipped). */
.eyebrow {
  margin: 0;
  font: 500 12px/1 var(--font);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--accent);
}

.headline {
  margin: 0;
  font: 500 30px/1.05 var(--font);
  letter-spacing: -0.01em;
  text-align: center;
}

.flipped {
  transform: rotate(180deg);
}

/* The header of the screens read from the table: a marker at each edge, and
   whatever sits between them stays centred. */
.table-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.player-name {
  display: block;
  margin-top: 6px;
  font: 600 46px/1.05 var(--font);
  letter-spacing: -0.015em;
  overflow-wrap: anywhere;
}

/* The secret word never splits: `nowrap` makes a break impossible and the
   size shrinks with the word's length so it still fits.

   The available width is the real one, measured down the box chain — body
   padding 9px, screen padding 22px, hold zone padding 20px — so 102px in
   total off a card that never exceeds 414px. The divisor is an upper bound
   on the width of a Jost 600 word of `--chars` characters at letter-spacing
   -0.03em, in em: the 300 words of the bank all sit under 0.44n + 1.30, and
   0.47n + 1.38 leaves ~9 % of headroom for a fallback face.
   `--chars` is the character count, set by the reveal screen. */
.word {
  --word-avail: calc(min(100vw - 18px, 396px) - 84px);
  margin: 0;
  font-size: min(
    clamp(48px, calc(0.22 * min(100vw, 414px)), 112px),
    calc(var(--word-avail) / (var(--chars, 8) * 0.47 + 1.38))
  );
  font-weight: 600;
  line-height: 1;
  letter-spacing: -0.03em;
  white-space: nowrap;
  animation: wordFocus 210ms var(--ease-focus) both;
}

.word-rule {
  width: 44px;
  height: 1px;
  background: var(--ornament-2);
}

.secret-role {
  margin: 0;
  font: 600 42px/1.05 var(--font);
  letter-spacing: -0.02em;
  color: var(--white);
  text-align: center;
  animation: wordFocus 210ms var(--ease-focus) both;
}

.secret-note {
  margin: 0;
  font: 400 15px/1.5 var(--font);
  letter-spacing: 0.02em;
  color: var(--text-dim);
  text-align: center;
  animation: wordFocus 210ms var(--ease-focus) both;
}

.warning {
  margin: 0;
  font: 400 13px/1.4 var(--font);
  letter-spacing: 0.03em;
  color: var(--text-faint);
  text-align: center;
}

/* -------------------------------------------------------------- controls */

button {
  font: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transition:
    transform 120ms var(--ease-settle),
    box-shadow 140ms ease,
    background-color 140ms ease,
    color 140ms ease;
}

button:disabled {
  cursor: default;
}

button:not(:disabled):hover {
  transform: translateY(-1px);
}

button:not(:disabled):active {
  transform: translateY(2px) scale(0.985);
}

:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  width: 100%;
  min-height: 64px;
  padding: 8px 16px;
  border: 0;
  border-radius: 9px;
  background: var(--text);
  color: var(--surface);
  font: 500 17px/1 var(--font);
  letter-spacing: 0.05em;
  text-align: center;
}

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

.btn--danger {
  background: var(--danger);
}

.btn--ghost {
  min-height: 52px;
  border: 1px solid var(--edge);
  background: transparent;
  color: var(--text-2);
  font: 400 15px/1 var(--font);
  letter-spacing: 0.04em;
}

.screen--resume .btn--ghost {
  min-height: 56px;
  font-size: 16px;
}

.btn--large {
  min-height: 64px;
}

.btn--small {
  width: auto;
  min-height: 44px;
  padding: 8px 16px;
  border-radius: 8px;
  font: 500 15px/1 var(--font);
  letter-spacing: normal;
}

.btn--ghost.btn--small {
  font-weight: 400;
}

/* 52 x 52 is under the 48px floor on neither side, and the glyph is U+2212
   MINUS SIGN, not a hyphen. */
.btn--icon {
  width: 52px;
  min-width: 52px;
  min-height: 52px;
  padding: 0;
  border-radius: 8px;
  background: var(--surface-2);
  box-shadow: inset 0 0 0 1px var(--edge);
  color: var(--text);
  font: 400 22px/1 var(--font);
  letter-spacing: normal;
}

/* A disabled call to action ABANDONS its fill instead of dimming it: dark
   text on a muddy background is unreadable at arm's length. */
.btn:disabled {
  border: 1px solid var(--edge);
  background: transparent;
  color: var(--text-faint);
}

.btn--icon:disabled {
  border: 0;
  background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--rule);
  color: var(--text-off);
}

.linkish {
  min-height: 44px;
  padding: 8px 12px;
  background: none;
  border: 0;
  color: var(--text-faint);
  font: 400 13px/1 var(--font);
  letter-spacing: 0.04em;
  text-decoration: underline;
  cursor: pointer;
}

.input {
  width: 100%;
  min-height: 52px;
  padding: 10px 14px;
  background: var(--surface);
  color: var(--text);
  border: 0;
  border-radius: 8px;
  box-shadow: inset 0 0 0 1px var(--edge);
  font: 400 17px/1 var(--font);
  transition: box-shadow 140ms ease;
}

/* The one field of the whole game that is a ceremony rather than a form. */
.input--guess {
  min-height: 60px;
  padding: 12px 16px;
  border-radius: 9px;
  font: 500 22px/1 var(--font);
  text-align: center;
}

.input::placeholder {
  color: var(--text-faint);
}

/* The ring thickens inwards, so selecting something never nudges a neighbour. */
.input:focus {
  box-shadow: inset 0 0 0 2px var(--accent);
  outline: none;
}

/* ---------------------------------------------------------------- setup */

.screen--setup {
  gap: 0;
  padding: 0;
}

.setup__body {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 24px 20px 22px;
}

.setup__head {
  text-align: center;
}

.setup__foot {
  flex: none;
  position: relative;
  padding: 0 20px 20px;
}

/* At twelve players the last field slides under a fade, never under a slab. */
.setup__foot::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  height: 28px;
  background: linear-gradient(var(--surface-fade), var(--surface));
  pointer-events: none;
}

.stepper-row {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
}

.stepper-row--second {
  margin-top: 4px;
}

.stepper-row__label {
  font: 500 17px/1 var(--font);
  letter-spacing: 0.04em;
}

.stepper {
  display: flex;
  align-items: center;
  gap: 8px;
}

.stepper__value {
  min-width: 38px;
  font: 500 24px/1 var(--font);
  font-variant-numeric: tabular-nums;
  text-align: center;
}

.preview {
  margin: 0;
  font: 400 14px/1.4 var(--font);
  letter-spacing: 0.02em;
  color: var(--text-2);
}

.preview--error {
  color: var(--accent);
  font-weight: 500;
}

.names {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.name-field {
  display: flex;
  align-items: center;
  gap: 10px;
}

.name-field__index {
  flex: 0 0 auto;
  width: 20px;
  font: 400 13px/1 var(--font);
  font-variant-numeric: tabular-nums;
  color: var(--text-faint);
  text-align: right;
}

/* The whole row is the tap target: clicking the label toggles the box. */
.switch {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 52px;
  cursor: pointer;
}

.switch__label {
  font: 400 16px/1.2 var(--font);
  letter-spacing: 0.02em;
}

.switch__input {
  flex: 0 0 auto;
  width: 26px;
  height: 26px;
  margin: 0;
  appearance: none;
  -webkit-appearance: none;
  border-radius: 5px;
  background: transparent;
  box-shadow: inset 0 0 0 1px var(--ornament-2);
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--surface);
  font: 500 15px/1 var(--font);
  cursor: pointer;
  transition:
    background-color 140ms ease,
    box-shadow 140ms ease;
}

.switch__input:checked {
  background: var(--accent);
  box-shadow: none;
}

.switch__input:checked::after {
  content: "✓";
}

/* --------------------------------------------------------------- reveal */

/* The corner index of a playing card: number, lozenge, total. It is printed
   twice, the second copy turned 180deg, like the pips of a real card. */
.card-index {
  position: absolute;
  top: 20px;
  left: 20px;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  pointer-events: none;
  animation: markIn 160ms var(--ease-settle) 80ms both;
}

.card-index--flipped {
  top: auto;
  left: auto;
  bottom: 20px;
  right: 20px;
  transform: rotate(180deg);
  animation-delay: 140ms;
}

.card-index__num {
  font: 600 15px/1 var(--font);
  letter-spacing: 0.06em;
  color: var(--accent);
}

.card-index__diamond {
  width: 9px;
  height: 9px;
  background: var(--accent);
  transform: rotate(45deg);
}

.card-index__total {
  font: 400 11px/1 var(--font);
  color: var(--text-faint);
}

.reveal__head {
  text-align: center;
  padding: 0 40px;
}

.reveal__eyebrow {
  margin: 0;
  font: 400 15px/1.3 var(--font);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-dim);
}

/* The face of the card is always mounted, with its word on it. It is the
   BACK that turns. The word is never faded, never moved, never scaled: it is
   simply uncovered. `overflow: hidden` clips the back once it passes 90deg,
   so it never paints on the ivory margin of the card. */
.hold-zone {
  flex: 1 1 auto;
  min-height: 0;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  padding: 20px;
  border-radius: 10px;
  background: var(--surface-2);
  box-shadow:
    inset 0 0 0 1px var(--accent),
    inset 0 0 0 5px var(--surface-2),
    inset 0 0 0 6px var(--ring-face);
  perspective: 1100px;
  overflow: hidden;
  text-align: center;
  cursor: pointer;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
  -webkit-touch-callout: none;
  -webkit-tap-highlight-color: transparent;
}

.hold-zone__corner {
  position: absolute;
  width: 11px;
  height: 11px;
  background: var(--accent);
  transform: rotate(45deg);
  opacity: 0;
  transition: opacity 140ms ease;
  pointer-events: none;
}

.hold-zone__corner--a {
  top: 14px;
  left: 16px;
}

.hold-zone__corner--b {
  bottom: 14px;
  right: 16px;
}

.hold-zone.is-held .hold-zone__corner {
  opacity: 1;
}

.hold-zone__hint {
  margin: 0;
  font: 400 14px/1 var(--font);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-dim);
  opacity: 0;
  transition: opacity 80ms ease;
}

.hold-zone.is-held .hold-zone__hint {
  opacity: 1;
  transition: opacity 120ms ease 70ms;
}

.hold-zone__secret {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  max-width: 100%;
}

/* The back of the card: same hatch grammar as the felt, but coarse and
   coloured — 3px on 9px, the accent in both directions, so it is identical
   turned 180deg. It is the only thing in the system that says "hidden";
   there is no padlock and no alert colour, because that is the gesture you
   actually make with a card. */
.hold-zone__door {
  position: absolute;
  inset: 0;
  padding: 20px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 10px;
  background-color: var(--surface);
  background-image:
    repeating-linear-gradient(45deg, var(--hatch-back) 0 3px, transparent 3px 9px),
    repeating-linear-gradient(-45deg, var(--hatch-back) 0 3px, transparent 3px 9px);
  box-shadow:
    inset 0 0 0 1px var(--edge),
    inset 0 0 0 5px var(--surface),
    inset 0 0 0 6px var(--ring-back);
  transform-origin: left center;
  transform: rotateY(0deg);
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
  opacity: 1;
  pointer-events: none;
  transition:
    transform 190ms var(--ease-door),
    box-shadow 190ms ease,
    opacity 40ms linear;
}

/* -112deg, not 90: past 90 the back has crossed over and
   `backface-visibility` hides it, and the extra 22deg guarantee it is gone
   whatever the browser does. Opening, the opacity waits 150 ms then drops in
   50 ms, so the back is still visible through almost all of its rotation —
   without that delay it would evaporate instead of turning. */
.hold-zone.is-held .hold-zone__door {
  transform: rotateY(-112deg);
  box-shadow: -22px 0 40px var(--shadow-lift);
  opacity: 0;
  transition:
    transform 190ms var(--ease-door),
    box-shadow 190ms ease,
    opacity 50ms linear 150ms;
}

.hold-zone__prompt {
  margin: 0;
  max-width: 15em;
  padding: 18px 22px;
  border-radius: 6px;
  background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--edge);
  font: 400 15px/1.45 var(--font);
  letter-spacing: 0.06em;
  color: var(--text-2);
}

/* ------------------------------------------------------------- describe */

/* The eye, drawn in CSS: no icon, no SVG. The amygdala shape comes from a
   four-value border-radius, rounder at the top than at the bottom. */
.peek-button {
  width: 56px;
  height: 48px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border: 0;
  background: none;
}

.eye {
  position: relative;
  width: 28px;
  height: 16px;
  border: 1.5px solid var(--ornament-2);
  border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: border-color 140ms ease;
}

.eye__pupil {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--ornament-2);
  transition:
    background-color 140ms ease,
    width 140ms ease,
    height 140ms ease;
}

/* One pixel of dilation is what makes the eye look at you. */
.peek-button.is-held .eye {
  border-color: var(--accent);
}

.peek-button.is-held .eye__pupil {
  width: 8px;
  height: 8px;
  background: var(--accent);
}

.speakers {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
}

.speaker {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 54px;
  padding: 6px 4px;
  border-bottom: 1px solid var(--rule);
  animation: rowIn 320ms var(--ease-settle) both;
  animation-delay: calc(60ms + var(--i, 0) * 45ms);
}

.speaker--first {
  min-height: 58px;
}

/* A full table stays on one screen: a name below the fold is a player
   skipped. Twelve rows of 35px fit the 449px the list is given at 390x844,
   and the ghost rank steps aside — there is no room left for a courtesy. */
.speakers--compact .speaker {
  min-height: 35px;
  padding: 1px 4px;
}

.speakers--compact .speaker--first {
  min-height: 40px;
}

.speakers--compact .speaker__name {
  font-size: 20px;
}

.speakers--compact .speaker--first .speaker__name {
  font-size: 21px;
}

.speakers--compact .speaker__ghost {
  display: none;
}

.speaker__rank {
  width: 34px;
  flex: none;
  font: 400 16px/1 var(--font);
  color: var(--text-faint);
  text-align: center;
}

.speaker--first .speaker__rank {
  width: 34px;
  height: 34px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  background: var(--accent);
  color: var(--surface);
  font-weight: 500;
}

.speakers--compact .speaker--first .speaker__rank {
  width: 30px;
  height: 30px;
}

.speaker__name {
  font: 500 25px/1.05 var(--font);
  letter-spacing: -0.01em;
  overflow-wrap: anywhere;
}

.speaker--first .speaker__name {
  font-weight: 600;
  font-size: 26px;
}

.speaker__badge {
  margin-left: auto;
  padding: 6px 10px;
  border: 1px solid var(--accent);
  border-radius: 99px;
  font: 400 11px/1 var(--font);
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--accent);
}

/* The rank a second time, upside down and paler than the original, for the
   half of the table sitting on the other side of the phone. */
.speaker__ghost {
  margin-left: auto;
  width: 34px;
  text-align: center;
  font: 400 16px/1 var(--font);
  color: var(--ornament);
  transform: rotate(180deg);
}

.rules {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 4px;
  text-align: center;
}

.rules li {
  font: 400 14px/1.5 var(--font);
  letter-spacing: 0.02em;
  color: var(--text-2);
}

/* ------------------------------------------------- the "all words" layer */

/* Held open by the eye, for someone who is out of the game. The words are in
   the document only while the button is held, exactly like the secret word.
   Two columns and tight rows, so a twelve-player table fits whole: the layer
   never scrolls and cannot be touched. */
.peek {
  position: absolute;
  inset: 0;
  z-index: 8;
  pointer-events: none;
  background: var(--surface);
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 26px 22px 22px;
  animation: peekIn 150ms var(--ease-settle) both;
}

.peek__title {
  margin: 0;
  font: 400 12px/1 var(--font);
  letter-spacing: 0.2em;
  text-transform: uppercase;
  color: var(--accent);
  text-align: center;
}

.peek__note {
  margin: 0;
  font: 400 13px/1.5 var(--font);
  letter-spacing: 0.02em;
  color: var(--text-faint);
  text-align: center;
}

.peek__list {
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
  margin: 6px 0 0;
  padding: 0;
  list-style: none;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 6px 14px;
  align-content: start;
}

.peek__item {
  display: flex;
  flex-direction: column;
  gap: 1px;
  padding: 5px 0 7px;
  border-bottom: 1px solid var(--rule);
  min-width: 0;
}

.peek__name {
  font: 400 13px/1.25 var(--font);
  letter-spacing: 0.04em;
  color: var(--text-dim);
  overflow-wrap: anywhere;
}

.peek__item--out .peek__name {
  color: var(--text-ghost);
  text-decoration: line-through;
}

.peek__word {
  font: 600 19px/1.15 var(--font);
  letter-spacing: -0.01em;
  color: var(--role, var(--text));
  overflow-wrap: anywhere;
}

.peek__item--out .peek__word {
  opacity: 0.55;
}

/* ----------------------------------------------------------------- vote */

/* The grid does not grow: it shrinks and scrolls, and the ornament axis
   takes the rest of the room. */
.vote-grid {
  flex: 0 1 auto;
  min-height: 0;
  overflow-y: auto;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 10px;
  align-content: start;
}

.vote-option {
  position: relative;
  min-height: 64px;
  padding: 8px;
  border: 0;
  border-radius: 9px;
  background: var(--surface);
  box-shadow: inset 0 0 0 1px var(--edge);
  color: var(--text);
  font: 500 21px/1.05 var(--font);
  overflow-wrap: anywhere;
}

/* Three redundant marks on the chosen option: the ring, the lozenge and the
   tick. Colour is never the only carrier. */
.vote-option[aria-pressed="true"] {
  background: var(--surface-picked);
  box-shadow: inset 0 0 0 2px var(--accent);
  color: var(--accent);
  font-weight: 600;
}

.vote-option[aria-pressed="true"]::before {
  content: "";
  position: absolute;
  top: 7px;
  left: 9px;
  width: 8px;
  height: 8px;
  background: var(--accent);
  transform: rotate(45deg);
}

.vote-option[aria-pressed="true"]::after {
  content: "✓";
  position: absolute;
  bottom: 7px;
  right: 9px;
  font: 500 14px/1 var(--font);
}

/* ---------------------------------------------------------- elimination */

.role-reveal {
  flex: 1 1 auto;
  min-height: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 20px;
  text-align: center;
}

.role-reveal__intro {
  margin: 0;
  font: 400 16px/1 var(--font);
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--text-dim);
}

.role-frame {
  width: 100%;
  padding: 22px 0;
  border-top: 1px solid var(--role, var(--text));
  border-bottom: 1px solid var(--role, var(--text));
}

/* The shortest word gets the largest body, so all three roles take up the
   same optical width. */
.role-name {
  margin: 0;
  font: 600 56px/1 var(--font);
  letter-spacing: -0.025em;
  color: var(--role, var(--text));
}

.role--civil.role-name {
  font-size: 74px;
}

.role-word {
  margin: 0;
  font: 400 15px/1.5 var(--font);
  letter-spacing: 0.03em;
  color: var(--text-faint);
}

/* ------------------------------------------------------------ whiteGuess */

.white-head {
  text-align: center;
  padding: 0 12px;
}

.white-head__eyebrow {
  margin: 0;
  font: 500 13px/1.3 var(--font);
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--white);
}

.white-head__title {
  margin: 10px 0 0;
  font: 500 30px/1.1 var(--font);
  letter-spacing: -0.01em;
}

.white-head__note {
  margin: 8px 0 0;
  font: 400 14px/1.45 var(--font);
  letter-spacing: 0.02em;
  color: var(--text-dim);
}

/* ------------------------------------------------------------------ over */

.banner {
  text-align: center;
  padding-top: 4px;
}

.banner__title {
  margin: 0;
  font: 600 44px/1.02 var(--font);
  letter-spacing: -0.02em;
  color: var(--role, var(--text));
  text-wrap: balance;
}

.banner__guess {
  margin: 10px 0 0;
  font: 400 14px/1.4 var(--font);
  letter-spacing: 0.02em;
  color: var(--text-dim);
}

.result-list {
  flex: 1 1 auto;
  min-height: 0;
  overflow-y: auto;
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
}

/* Baseline, not centre: the three sizes line up on their own baseline, which
   absorbs the difference. Slower cascade than the speaker list — the game is
   over, this one is savoured. */
.result {
  display: flex;
  align-items: baseline;
  gap: 9px;
  padding: 11px 4px;
  border-bottom: 1px solid var(--rule);
  animation: rowIn 320ms var(--ease-settle) both;
  animation-delay: calc(140ms + var(--i, 0) * 55ms);
}

.result__name {
  font: 600 17px/1.2 var(--font);
  overflow-wrap: anywhere;
}

.result--out .result__name {
  font-weight: 500;
  color: var(--text-faint);
  text-decoration: line-through;
}

.result__role {
  font: 500 14px/1.2 var(--font);
  letter-spacing: 0.05em;
  color: var(--role, var(--text));
}

.result__word {
  margin-left: auto;
  font: 400 15px/1.2 var(--font);
  color: var(--text-dim);
  overflow-wrap: anywhere;
}

.pair-recap {
  margin: 0;
  font: 400 14px/1.5 var(--font);
  letter-spacing: 0.02em;
  color: var(--text-dim);
  text-align: center;
}

.pair-recap strong {
  font-weight: 600;
  color: var(--role, var(--text));
}

/* ---------------------------------------------------------------- resume */

.resume__head {
  text-align: center;
  padding: 0 18px;
}

.resume__title {
  margin: 0;
  font: 500 34px/1.1 var(--font);
  letter-spacing: -0.01em;
}

.resume__note {
  margin: 10px 0 0;
  font: 400 15px/1.5 var(--font);
  letter-spacing: 0.02em;
  color: var(--text-dim);
}

.panel {
  display: flex;
  flex-direction: column;
  gap: 7px;
  padding: 16px 18px;
  border: 1px solid var(--edge);
  border-radius: 10px;
}

.panel .preview {
  font: 500 17px/1.3 var(--font);
  letter-spacing: 0.02em;
  color: var(--text);
}

/* -------------------------------------------------------------- abandon */

/* The trigger is the one control of the game allowed under the 48px floor
   (CONTRAT §9): it destroys the party, so it must be a little hard to hit
   while the phone travels round the table. The reserved height keeps the
   bottom of every screen aligned. */
.abandon {
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 52px;
  padding-bottom: 10px;
}

/* A veil over the whole card, and a small card centred on it. Above the words
   layer, so nothing can be held open behind it. */
.dialog {
  position: absolute;
  inset: 0;
  z-index: 11;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 18px;
  background: var(--scrim);
  animation: markIn 140ms var(--ease-settle) both;
}

/* The same three edges as the card face of `reveal`, red fillet included: this
   panel is the only thing on screen and it is about to lose the game. */
.dialog__panel {
  width: 100%;
  max-width: 302px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 20px 18px 18px;
  border-radius: 10px;
  background: var(--surface);
  box-shadow:
    0 10px 30px var(--shadow-lift),
    inset 0 0 0 1px var(--accent),
    inset 0 0 0 5px var(--surface),
    inset 0 0 0 6px var(--ring-face);
  text-align: center;
  animation: peekIn 160ms var(--ease-settle) both;
}

/* The only red lozenges in the system outside a role: the mark says danger
   before the sentence does. */
.dialog__panel .rule-mark__diamond {
  background: var(--accent);
}

.dialog__title {
  margin: 0;
  font: 500 24px/1.15 var(--font);
  letter-spacing: -0.01em;
}

.dialog__note {
  margin: 0;
  font: 400 15px/1.45 var(--font);
  letter-spacing: 0.02em;
  color: var(--text-dim);
}

/* Smaller, narrower, and high in the panel — nowhere near the trigger at the
   bottom of the screen. Its label carries the loss on its own: the red is a
   second signal, never the only one. */
.dialog__quit {
  align-self: center;
  margin-top: 2px;
}

/* -------------------------------------------------------------- confetti */

/* A small spray of lozenges at the exact point of contact, on every press of
   a live button. A greyed-out button spawns nothing, which makes it one more
   state signal, for free. */
.confetti {
  position: absolute;
  inset: 0;
  z-index: 9;
  overflow: hidden;
  pointer-events: none;
}

.confetti__piece {
  position: absolute;
  border-radius: 1px;
  animation: confettiFly 720ms var(--ease-confetti) both;
}

.confetti__piece--0 {
  background: var(--accent);
}

.confetti__piece--1 {
  background: var(--civil);
}

.confetti__piece--2 {
  background: var(--text);
}

.confetti__piece--3 {
  background: var(--ornament-2);
}

.confetti__piece--4 {
  background: var(--ring-face);
}

/* The one place the spray is held back. Pressing the eye is a press on a live
   button, so it sprays like any other, and twelve lozenges flying over the
   secret words is noise at the exact moment they are being read.

   It has to be said here, from outside the screen: `.screen` carries a filled
   `cardSettle`, so its computed transform stays an identity matrix and it is a
   stacking context for good. No z-index inside it can ever climb above this
   layer, which is its sibling. */
.screen.is-peeking ~ .confetti {
  display: none;
}
