/* KATAPULT play client — portrait-first board.
   Phone is the design target; the desktop layout is the same board with side
   rails. Every rule here assumes touch: no hover is ever load-bearing. */

:root {
  --k-safe-top: env(safe-area-inset-top, 0px);
  --k-safe-bottom: env(safe-area-inset-bottom, 0px);
  --k-rail: 0px;
  --lane-gap: 6px;
}

* { box-sizing: border-box; }

html, body {
  margin: 0;
  height: 100%;
  overscroll-behavior: none;
  background: var(--k-bg, #12101f);
  color: var(--k-parchment, #ece2c6);
  font-family: var(--k-font-ui, system-ui, sans-serif);
  /* no double-tap zoom, no pinch, no 300ms tap delay */
  touch-action: manipulation;
  -webkit-tap-highlight-color: transparent;
}

/* 100dvh, never 100vh: the mobile URL bar must not clip the hand */
#app {
  position: relative;
  width: 100%;
  max-width: 100vw;
  overflow-x: hidden;
  height: 100dvh;
  display: flex;
  flex-direction: column;
  padding-top: var(--k-safe-top);
  padding-bottom: var(--k-safe-bottom);
  overflow: hidden;
}

button {
  font: inherit;
  color: inherit;
  background: none;
  border: 0;
  cursor: pointer;
  /* every tappable control clears the 44px target floor */
  min-height: 44px;
  min-width: 44px;
  touch-action: manipulation;
}

/* the stage is the flex child that must absorb the height at every width */
.stage { flex: 1; min-width: 0; min-height: 0; display: flex; flex-direction: column; }
.screen { display: none; flex: 1; min-width: 0; min-height: 0; flex-direction: column; }
.screen[data-active] { display: flex; }
/* the board is a grid, but only once it is the active screen */
.screen.board { display: none; }
.screen.board[data-active] { display: grid; }

/* ── HOME ─────────────────────────────────────────────────────────── */
.home {
  width: 100%;
  min-width: 0;
  justify-content: center;
  align-items: center;
  gap: 18px;
  padding: 24px 20px calc(24px + var(--k-safe-bottom));
  text-align: center;
  background:
    radial-gradient(ellipse 80% 50% at 50% 8%, rgba(201, 164, 92, .13), transparent 70%),
    var(--k-bg, #12101f);
}
.brand {
  font-family: var(--k-font-brand, Cinzel, serif);
  font-size: clamp(1.7rem, 8.6vw, 3.2rem);
  letter-spacing: .1em;
  max-width: 100%;
  color: var(--k-gold-bright, #e8c477);
  margin: 0;
}
.tagline {
  margin: 0 0 6px;
  font-size: .82rem;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--k-parchment-dim, #d8cba9);
  opacity: .8;
}
.cta {
  width: min(420px, 100%);
  padding: 16px 20px;
  font-family: var(--k-font-brand, Cinzel, serif);
  font-size: 1.05rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  border: 1px solid var(--k-gold-dim, #8a713f);
  background: linear-gradient(180deg, rgba(232, 196, 119, .16), rgba(201, 164, 92, .06));
  color: var(--k-gold-bright, #e8c477);
  transition: transform .12s var(--k-ease), background .18s var(--k-ease);
}
.cta--primary {
  background: linear-gradient(180deg, #e8c477, #c9a45c 60%, #a2803f);
  color: #241f38;
  border-color: #ffe6a8;
  font-weight: 700;
}
.cta:active { transform: translateY(1px) scale(.99); }
.cta[disabled] { opacity: .45; pointer-events: none; }
.stats {
  display: flex;
  gap: 18px;
  font-size: .78rem;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--k-parchment-dim, #d8cba9);
  opacity: .75;
}
.stats b { color: var(--k-gold-bright, #e8c477); }

/* ── GATE ─────────────────────────────────────────────────────────────
   The sign-in wall, and the only screen that is active in the markup. It is
   built to hold still: every control that will ever exist here is laid out at
   first paint, and the three modes only change VISIBILITY, never the box
   heights of anything above them.

   - "checking"  the auth session is being restored. The controls are laid out
                 but invisible, so the reveal costs zero layout shift.
   - "signin"    nobody is signed in. Google, or the email form.
   - "terms"     signed in already, but the documents moved on. No password
                 retyped to re-read a paragraph.

   Phone-first: 44px targets everywhere, 16px inputs (anything smaller makes
   iOS zoom the page on focus, which is a layout shift the user performs), and
   the whole column scrolls rather than clipping on a short viewport. */
.gate {
  position: relative;
  width: 100%;
  min-width: 0;
  align-items: center;
  /* centred by `margin:auto` on the inner column, never by justify-content:
     a centred flex column with overflow clips its own top when it grows */
  overflow-y: auto;
  overscroll-behavior: contain;
  padding: calc(20px + var(--k-safe-top)) 20px calc(24px + var(--k-safe-bottom));
  text-align: center;
  background: var(--k-bg, #12101f);
}

/* THE KEY ART. The login screen used to show a wordmark on an empty field —
   nothing about the game a person was being asked to sign up for. This is the
   real thing: the rival Realm hanging in the cloud, your five katapults loaded,
   two shots already in the air.

   It is a separate layer rather than a background on .gate because it must sit
   UNDER a scrolling column without scrolling with it, and because the veil over
   it is doing real work: art at full strength behind 12px type is a legibility
   failure, not a mood. The gradient darkens hardest exactly where the panel is,
   and the vignette keeps the frame from ending in a hard edge. */
.gate-art {
  /* FIXED, not absolute: the gate lives inside the centre stage, which is
     width-limited by the ad rails, and an absolutely-positioned art layer
     stopped at those columns — a full-screen moment with two empty gutters.
     Fixed spans the viewport. It is safe because an inactive .screen is
     `display: none`, which removes the whole subtree from rendering: the art
     cannot outlive the gate and appear over the board. */
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background-image:
    /* vignette: holds the frame together and keeps the edges from ending flat */
    radial-gradient(ellipse 128% 86% at 50% 46%, transparent 0%, rgba(9, 7, 18, .34) 58%, rgba(9, 7, 18, .82) 100%),
    /* the working scrim: near-transparent across the art, deepening into the
       page colour at the bottom so the picture ends rather than being cropped */
    linear-gradient(180deg, rgba(9, 7, 18, .46) 0%, rgba(9, 7, 18, .16) 26%, rgba(9, 7, 18, .3) 62%, rgba(9, 7, 18, .8) 88%, var(--k-bg, #12101f) 100%),
    image-set(url("/assets/art/gate-key-art.webp") type("image/webp"),
              url("/assets/art/gate-key-art.jpg") type("image/jpeg"));
  background-size: cover, cover, cover;
  background-position: center, center, center 40%;
  background-repeat: no-repeat;
}
/* the art is a first impression, not information: if it has not arrived, the
   screen is still complete. It fades in rather than snapping. */
.gate-art { animation: gate-art-in .9s var(--k-ease, ease) both; }
@keyframes gate-art-in { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .gate-art { animation: none; } }

.gate-inner { position: relative; z-index: 1; }
/* The controls need a ground of their own. Darkening the WHOLE image to make
   12px type legible is how the art got erased the first time; a soft plate
   under the column solves the same problem locally and leaves the picture
   alone. It is a blurred ellipse, not a box: a hard panel over this art would
   read as a dialog dropped on a screenshot. */
.gate-inner::before {
  content: "";
  position: absolute;
  inset: -10% -20% -12%;
  z-index: -1;
  border-radius: 50% / 22%;
  /* Softened after looking at it: at .93 in the middle this stopped being
     ground under the text and became a dark shape on the artwork, with a
     visible edge where it fell off. Legibility here is carried mostly by the
     text shadows and by the agreement box's own panel, so the plate only has to
     take the contrast down — not black the picture out. */
  background: radial-gradient(ellipse 78% 62% at 50% 50%,
    rgba(7, 6, 14, .66) 0%, rgba(7, 6, 14, .46) 46%, rgba(7, 6, 14, .16) 72%, transparent 88%);
  filter: blur(18px);
  pointer-events: none;
}

/* THE SKY. Between the still art and the controls, and nothing but atmosphere:
   no pointer events, no layout, no contribution to the reading order. */
.gate-sky {
  position: fixed;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  display: block;
  pointer-events: none;
}
.gate-sky[hidden] { display: none; }
.gate-inner {
  width: min(420px, 100%);
  margin: auto;            /* centred when it fits, scrolled when it does not */
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 10px;
}
.gate .tagline {
  margin-bottom: 2px;
  letter-spacing: .22em;
  opacity: .92;
  color: var(--k-parchment, #ece2c6);
  text-shadow: 0 1px 3px rgba(0, 0, 0, 1), 0 2px 14px rgba(0, 0, 0, .95);
}

/* THE WORDMARK, not the word typed in a serif. The SVG is a single-colour path
   set, so it is used as a mask and the gold gradient beneath it is the same one
   the cards and the CTA use — one gold in the product, not three. */
.brand--mark {
  width: min(300px, 78%);
  margin: 0 auto;
  line-height: 0;
}
.brand--mark .k-wordmark {
  display: block;
  width: 100%;
  aspect-ratio: 984 / 346;
  background: linear-gradient(178deg, #f6e3b0 8%, var(--k-gold-bright, #e8c477) 38%, var(--k-gold, #c9a45c) 72%, #9b7a3e 100%);
  -webkit-mask: url("/assets/brand/katapult-wordmark.svg") no-repeat center / contain;
  mask: url("/assets/brand/katapult-wordmark.svg") no-repeat center / contain;
  filter: drop-shadow(0 3px 14px rgba(0, 0, 0, .8)) drop-shadow(0 0 26px rgba(201, 164, 92, .28));
}
.gate-body { width: 100%; display: flex; flex-direction: column; gap: 12px; }
/* laid out, not yet offered: the geometry is already final while auth restores,
   so revealing the controls costs no layout shift. The note stays visible
   throughout — it is the one box that speaks in every mode. */
.gate[data-mode="checking"] .gate-body,
.gate[data-mode="stalled"] .gate-body { visibility: hidden; }
.gate[data-mode="checking"] .gate-note,
.gate[data-mode="stalled"] .gate-note,
.gate[data-mode="stalled"] .gate-reload { visibility: visible; }
.gate[data-mode="terms"] .gate-auth { display: none; }
.gate:not([data-mode="terms"]) .gate-continue { display: none; }
/* only ever shown by the dead-man switch in index.html */
.gate-reload { display: none; }
.gate[data-mode="stalled"] .gate-reload { display: block; }

/* the agreement, above everything it unlocks */
.agree {
  display: flex;
  align-items: flex-start;
  gap: 11px;
  min-height: 44px;
  padding: 10px 12px;
  text-align: left;
  font-size: .78rem;
  line-height: 1.45;
  color: var(--k-parchment-dim, #d8cba9);
  border: 1px solid var(--k-line, #3a3458);
  background: rgba(8, 6, 16, .62);
  backdrop-filter: blur(2px);
  cursor: pointer;
}
/* The default checkbox is a white square, which is the one bright rectangle on
   a gold-on-navy screen — it reads as a rendering fault. Drawn here instead,
   and handed back to the platform under forced colours. */
.agree input {
  appearance: none;
  -webkit-appearance: none;
  flex: 0 0 auto;
  width: 22px;
  height: 22px;
  margin: 1px 0 0;
  display: grid;
  place-items: center;
  border: 1px solid var(--k-gold-dim, #8a713f);
  border-radius: 2px;
  background: rgba(8, 6, 16, .6);
  cursor: pointer;
}
.agree input::after {
  content: "";
  width: 11px;
  height: 6px;
  border: 2px solid var(--k-gold-bright, #e8c477);
  border-top: 0;
  border-right: 0;
  transform: rotate(-45deg) translate(1px, -1px);
  opacity: 0;
}
.agree input:checked {
  border-color: var(--k-gold-bright, #e8c477);
  background: rgba(232, 196, 119, .14);
}
.agree input:checked::after { opacity: 1; }
.agree input:focus-visible { outline: 2px solid var(--k-gold-bright, #e8c477); outline-offset: 2px; }
@media (forced-colors: active) {
  .agree input { appearance: auto; }
}
.agree a { color: var(--k-gold-bright, #e8c477); text-underline-offset: 2px; }
/* asked for, not yet given: the border says which control is waiting */
.agree[data-needed] {
  border-color: var(--k-danger, #d64541);
  animation: agree-nudge .38s var(--k-ease, ease);
}
@keyframes agree-nudge {
  25% { transform: translateX(-4px); }
  75% { transform: translateX(4px); }
}

/* Reserved from first paint so a message never pushes the form down. */
.gate-note {
  margin: 0;
  font-size: .76rem;
  line-height: 1.35;
  color: var(--k-parchment-dim, #d8cba9);
}
/* Two lines were reserved so an error filled the box instead of pushing the
   form down. With the form gone there is nothing below to push, and an empty
   reservation is just a hole between the agreement and the only button. It is
   still held open while the session is being checked, because that is the one
   moment the box has something to say and the controls are not yet drawn. */
.gate-note:empty { display: none; }
.gate[data-mode="checking"] .gate-note,
.gate[data-mode="stalled"] .gate-note { display: block; min-height: 2.8em; }
.gate-note[data-tone="bad"] { color: var(--k-danger, #d64541); }
.gate-note[data-tone="good"] { color: var(--k-gold-bright, #e8c477); }

.gate-auth { display: flex; flex-direction: column; gap: 12px; }
/* ACCEPT AND CONTINUE is the longest label left on this screen and it must stay
   on one line down to a 320px phone: the type scales with the viewport instead
   of wrapping the primary action in half. */
.gate .cta {
  width: 100%;
  padding: 16px 14px;
  font-size: clamp(.9rem, 4.4vw, 1.05rem);
}

/* LAUNCH INTO BATTLE — the call to action, and a heading rather than a button
   label. Google's branding guidelines fix both the wording and the colours of
   the button below, so the words that belong to us live above it, with the
   gold rules from the landing art. */
.gate-cta {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 2px 0 0;
  font-family: var(--k-font-brand, Cinzel, serif);
  font-size: clamp(.82rem, 3.9vw, 1rem);
  letter-spacing: .22em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--k-gold-bright, #e8c477);
  text-shadow: 0 2px 12px rgba(0, 0, 0, .85);
}
.gate-cta::before, .gate-cta::after {
  content: "";
  flex: 1;
  height: 1px;
  background: linear-gradient(90deg, transparent, var(--k-gold-dim, #8a713f));
}
.gate-cta::after { background: linear-gradient(90deg, var(--k-gold-dim, #8a713f), transparent); }

/* GOOGLE'S BUTTON, TO GOOGLE'S SPEC — and deliberately not restyled.
   Their guidelines allow white, light grey or #131314 with a #8E918F hairline;
   they fix the wording to "Sign in with Google" / "Continue with Google"; and
   the four-colour mark may not be recoloured, reproportioned or crowded. A gold
   button with that mark in it is not something we are allowed to ship, so the
   dark theme is what we use — it is also the one that sits honestly on navy.
   The only liberty taken is the height: 48px instead of their 40px minimum,
   because this is the single tap the whole product depends on and 40px is under
   the touch-target floor on a phone. Scaling up is permitted; recolouring is
   not. */
.gsi {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  width: 100%;
  min-height: 48px;
  padding: 0 14px;
  border: 1px solid #8e918f;
  border-radius: 4px;
  background: #131314;
  color: #e3e3e3;
  font-family: Roboto, "Helvetica Neue", Arial, sans-serif;
  font-size: 15px;
  font-weight: 500;
  letter-spacing: .25px;
  cursor: pointer;
  transition: background .15s ease, box-shadow .15s ease;
}
.gsi:hover { background: #1c1c1d; box-shadow: 0 1px 3px rgba(0, 0, 0, .5); }
.gsi:active { background: #232324; }
.gsi:focus-visible { outline: 2px solid var(--k-gold-bright, #e8c477); outline-offset: 3px; }
.gsi:disabled { opacity: .55; cursor: default; }
.gsi-icon { flex: 0 0 auto; display: block; width: 20px; height: 20px; }
.gsi-icon svg { display: block; width: 100%; height: 100%; }
.gsi-label { white-space: nowrap; }

/* one quiet line under the only control, answering the question a login screen
   always raises and this one can actually answer well */
.gate-fine {
  margin: 0;
  font-size: .68rem;
  letter-spacing: .08em;
  color: var(--k-parchment-dim, #d8cba9);
  opacity: .72;
  text-shadow: 0 1px 6px rgba(0, 0, 0, .95);
}

.gate-links {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 6px;
}
/* a button that reads as a link, and still clears the 44px target floor */
.linky {
  padding: 10px 4px;
  font-size: .72rem;
  letter-spacing: .04em;
  color: var(--k-gold, #c9a45c);
  text-decoration: underline;
  text-underline-offset: 3px;
  background: none;
  border: 0;
}
.linky:hover, .linky:focus-visible { color: var(--k-gold-bright, #e8c477); }
.linky[hidden] { display: none; }
.gate-out { align-self: center; opacity: .75; font-size: .68rem; }

/* the whole ad surface is off while the gate is up: nobody sees a promotional
   surface — house or otherwise — before they have agreed to anything.
   `visibility` keeps the banner's reserved box, so entering home shifts nothing. */
body[data-gate] .ad,
body[data-gate] .consent {
  visibility: hidden;
  pointer-events: none;
}

/* ── BOARD ────────────────────────────────────────────────────────── */
.board {
  /* one column that can never exceed the viewport: without minmax(0,1fr) a
     crowded hand widens the whole grid and pushes lane 5 off screen */
  grid-template-columns: minmax(0, 1fr);
  /* opponent realm · lanes · battle · hud · katapults · realm · hand · hud · actions
     the battle keeps a bounded share; the slack goes to the hand, where the
     thumb lives */
  grid-template-rows: auto auto minmax(56px, 22dvh) auto auto auto 1fr auto auto;
  gap: 3px;
  padding: 4px var(--k-rail);
  min-width: 0;
  min-height: 0;
  overflow: hidden;
}

/* five lanes always fit: grid children default to min-width:auto, which is what
   pushed the fifth lane off a 390px screen */
.row {
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: var(--lane-gap);
  padding: 0 6px;
  min-width: 0;
}
.row > * { min-width: 0; }

.hud {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 2px 12px;
  font-size: .68rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--k-parchment-dim, #d8cba9);
}
.hud b { color: var(--k-gold-bright, #e8c477); }

/* the clock is always legible; it warns, then goes critical */
.clock {
  font-family: var(--k-font-brand, Cinzel, serif);
  font-size: 1.5rem;
  font-variant-numeric: tabular-nums;
  color: var(--k-gold-bright, #e8c477);
  min-width: 2.6ch;
  text-align: center;
}
.clock[data-state="warn"] { color: var(--k-element, #d9a933); }
.clock[data-state="critical"] { color: var(--k-danger, #d64541); animation: pulse .8s infinite; }
@keyframes pulse { 50% { opacity: .45; } }

/* Realm tile: architectural, not a card */
.tile {
  position: relative;
  min-width: 0;
  aspect-ratio: 1 / 0.82;
  border: 1px solid var(--k-line, #3a3458);
  background: linear-gradient(180deg, #221d3c, #14111f);
  display: grid;
  place-items: center;
  gap: 2px;
  padding: 3px;
  text-align: center;
  overflow: hidden;
}
.tile img { width: 44%; image-rendering: auto; }
.tile .name {
  font-size: clamp(6px, 2.1vw, 9px);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--k-gold, #c9a45c);
}
.tile .hp { font-size: 9px; letter-spacing: 1px; color: var(--k-hp, #cf3f3f); }
.tile .hp .lost { opacity: .22; }
.tile[data-hidden] { background: repeating-linear-gradient(135deg, #1a1730 0 5px, #15121f 5px 10px); }
.tile[data-hidden] .name { color: var(--k-line, #3a3458); }
.tile[data-destroyed] { filter: grayscale(1) brightness(.45); }
.tile[data-hit] { animation: tile-hit .42s var(--k-ease); }
@keyframes tile-hit {
  0% { transform: none; }
  22% { transform: translateY(3px) scale(.96); box-shadow: 0 0 22px rgba(214, 69, 65, .8) inset; }
  100% { transform: none; }
}

/* Katapult slot: the drop target */
.slot {
  position: relative;
  min-width: 0;
  min-height: 54px;
  aspect-ratio: 1 / 1.35;   /* closer to a card, so it reads less squat */
  overflow: hidden;
  border: 1px dashed var(--k-line, #3a3458);
  display: grid;
  place-items: center;
  color: var(--k-line, #3a3458);
  font-size: 9px;
  letter-spacing: .12em;
  text-transform: uppercase;
  transition: border-color .16s var(--k-ease), box-shadow .16s var(--k-ease), transform .16s var(--k-ease);
}
/* magnetic snap target while a card is picked up */
.slot[data-droppable] {
  border-color: var(--k-gold-bright, #e8c477);
  color: var(--k-gold-bright, #e8c477);
  box-shadow: 0 0 14px rgba(232, 196, 119, .28) inset;
}
.slot[data-magnet] { transform: scale(1.06); box-shadow: 0 0 20px rgba(232, 196, 119, .5) inset; }
.slot[data-filled] { border-style: solid; border-color: var(--k-gold-dim, #8a713f); }
/* a rival lane whose contents the rules keep secret — not a face-down card */
.slot[data-veiled] {
  border-style: solid;
  border-color: rgba(58, 52, 88, .9);
  background: repeating-linear-gradient(135deg, #1a1730 0 6px, #15121f 6px 12px);
  color: rgba(201, 164, 92, .45);
  font-size: 8px;
}
/* A card keeps its 1:1.946 canon proportion, always. Sizing by height lets the
   aspect-ratio decide the width; the old width:100% + max-height:100% fought
   the ratio and squashed the art. A lane is wider than a card is — the card
   sits centred in it rather than being stretched to fill it. */
.slot > .k-hexcard, .slot > .k-cardback {
  height: 100%;
  width: auto;
  max-width: 100%;
  align-self: center;
  justify-self: center;
}

/* Pick a lane card up to read it: it grows from its own centre, rises above
   its neighbours, and the lane stops clipping for as long as it is lifted.
   Pointer devices only — touch already has long-press inspect. */
@media (hover: hover) and (pointer: fine) {
  .slot:has(> .k-hexcard:hover),
  .slot:has(> .k-cardback:hover) {
    overflow: visible;
    z-index: 40;
  }
  .slot > .k-hexcard, .slot > .k-cardback {
    transition: transform .18s var(--k-ease, cubic-bezier(.25,.46,.45,.94)), filter .18s var(--k-ease, cubic-bezier(.25,.46,.45,.94));
    /* your lane grows UP into the empty battle space, so a lifted card never
       hides the Realm tile it is aimed at */
    transform-origin: center bottom;
  }
  /* the rival row hangs from the top, so its cards grow downward instead */
  #oppSlots .slot > .k-hexcard, #oppSlots .slot > .k-cardback { transform-origin: center top; }
  .slot > .k-hexcard:hover, .slot > .k-cardback:hover {
    transform: scale(1.85);
    filter: drop-shadow(0 14px 26px rgba(0, 0, 0, .6)) drop-shadow(0 0 14px rgba(232, 196, 119, .3));
    z-index: 41;
    cursor: zoom-in;
  }
}
.slot .fused {
  position: absolute;
  top: 2px;
  right: 2px;
  font-size: 8px;
  padding: 1px 4px;
  border-radius: 2px;
  background: var(--k-element, #d9a933);
  color: #241f38;
  font-weight: 700;
}

/* Battle space: the reveal beat owns this area and nothing may cover it */
.battle {
  position: relative;
  display: grid;
  place-items: center;
  min-height: 0;
  overflow: hidden;
}
.battle .banner {
  font-family: var(--k-font-brand, Cinzel, serif);
  font-size: clamp(.8rem, 3.6vw, 1.2rem);
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--k-gold-bright, #e8c477);
  text-align: center;
  padding: 0 14px;
  text-shadow: 0 2px 12px rgba(0, 0, 0, .8);
}
.battle .banner[data-flash] { animation: flash .5s var(--k-ease); }
@keyframes flash {
  0% { transform: scale(.9); opacity: 0; }
  40% { transform: scale(1.06); opacity: 1; }
  100% { transform: none; opacity: 1; }
}
/* a projectile is a Timeline event made visible; it never decides anything */
.shot {
  position: absolute;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: radial-gradient(circle, #fff, var(--k-gold-bright, #e8c477) 45%, transparent 70%);
  filter: drop-shadow(0 0 8px rgba(232, 196, 119, .9));
  pointer-events: none;
}
.spark {
  position: absolute;
  width: 46px;
  height: 46px;
  border-radius: 50%;
  background: radial-gradient(circle, #fff 0%, var(--k-fire, #e8722c) 35%, transparent 70%);
  pointer-events: none;
  animation: spark .42s ease-out forwards;
}
@keyframes spark {
  from { transform: scale(.2); opacity: 1; }
  to { transform: scale(1.6); opacity: 0; }
}

/* Hand: thumb zone, fanned, tap or drag */
.hand {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 0;
  min-width: 0;
  min-height: 0;
  max-height: 26dvh;
  padding: 2px 8px 0;
}
/* THE HAND IS A FAN, AND IT IS TWO LAYERS.
   The holder owns every transform that belongs to the LAYOUT — the fan angle,
   the arc, the lift when a card is picked up, the deal-in. The card inside owns
   the one that belongs to the OBJECT: k-card-tilt rewrites its transform each
   frame while a pointer is on it, and anything else competing for that property
   would simply be overwritten. Keeping them apart is what lets a card tilt in a
   hand that is also fanned. */
.hand-slot {
  width: clamp(62px, 23vw, 104px);
  max-height: 26dvh;
  margin: 0 -6px;
  /* a crowded hand overlaps harder instead of overflowing */
  flex: 0 1 auto;
  min-width: 34px;
  transform-origin: 50% 150%;
  /* --fan is the distance from the middle of the hand, in cards. Angle turns it
     into a spread; the arc lifts the middle so the fan curves instead of
     shearing. Both are damped by the count, so seven cards do not become a
     windmill. */
  --fan-angle: 3.6deg;
  --fan-arc: 1.6px;
  transform:
    rotate(calc(var(--fan, 0) * var(--fan-angle)))
    translateY(calc(var(--fan, 0) * var(--fan, 0) * var(--fan-arc)));
  transition: transform .22s cubic-bezier(.34, 1.56, .64, 1), filter .2s var(--k-ease);
  cursor: grab;
}
.hand-slot > .k-hexcard { display: block; width: 100%; }

.hand-slot[data-selected] {
  transform:
    rotate(calc(var(--fan, 0) * var(--fan-angle) * .25))
    translateY(-16px)
    scale(1.14);
  filter: drop-shadow(0 10px 18px rgba(0, 0, 0, .6)) drop-shadow(0 0 10px rgba(232, 196, 119, .5));
  z-index: 3;
}
/* the raised card in a fan must sit above the ones it overlaps on BOTH sides */
.hand-slot:hover { z-index: 2; }
.hand-slot[data-dragging] { opacity: .35; }
.hand-slot[data-element][data-selected] { filter: drop-shadow(0 0 12px rgba(217, 169, 51, .8)); }

/* DEALT, not appeared. A card arrives from below the rail with a little
   overshoot, staggered by --deal-order so a fresh hand lands as a deal rather
   than as five things becoming visible at once. Only cards the player has not
   seen carry data-deal (see board.mjs), so a view update never re-deals a hand
   somebody is in the middle of reading. */
@keyframes hand-deal {
  from {
    opacity: 0;
    transform:
      rotate(calc(var(--fan, 0) * var(--fan-angle) * 2.4))
      translateY(46px) scale(.82);
  }
  60% { opacity: 1; }
  to {
    opacity: 1;
    transform:
      rotate(calc(var(--fan, 0) * var(--fan-angle)))
      translateY(calc(var(--fan, 0) * var(--fan, 0) * var(--fan-arc)));
  }
}
.hand-slot[data-deal] {
  animation: hand-deal .42s cubic-bezier(.22, 1.3, .5, 1) both;
  animation-delay: calc(var(--deal-order, 0) * 70ms);
}
@media (prefers-reduced-motion: reduce) {
  .hand-slot[data-deal] { animation: none; }
  .hand-slot { transition: none; }
}

.actions {
  display: flex;
  gap: 8px;
  padding: 6px 12px calc(8px + var(--k-safe-bottom));
}
.actions .cta { flex: 1; padding: 14px 12px; font-size: .92rem; }

/* the card that follows the finger while dragging */
#ghost {
  position: fixed;
  z-index: 60;
  width: 84px;
  pointer-events: none;
  transform: translate(-50%, -50%) scale(1.1);
  filter: drop-shadow(0 12px 20px rgba(0, 0, 0, .7));
}

/* ── OVERLAYS ─────────────────────────────────────────────────────── */
.sheet {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: none;
  align-items: center;
  justify-content: center;
  padding: 20px;
  background: rgba(8, 6, 16, .82);
  backdrop-filter: blur(3px);
}
.sheet[data-open] { display: flex; }
.sheet-inner {
  width: min(460px, 100%);
  max-height: 88dvh;
  overflow: auto;
  padding: 22px 20px calc(22px + var(--k-safe-bottom));
  border: 1px solid var(--k-gold-dim, #8a713f);
  background: linear-gradient(180deg, #1c1930, #12101f);
  text-align: center;
}
.sheet h2 {
  font-family: var(--k-font-brand, Cinzel, serif);
  letter-spacing: .12em;
  text-transform: uppercase;
  margin: 0 0 8px;
  color: var(--k-gold-bright, #e8c477);
}
.sheet p { margin: 0 0 14px; font-size: .9rem; color: var(--k-parchment-dim, #d8cba9); }
.sheet .cta { margin-top: 8px; }
.sheet .grab {
  width: 42px;
  height: 4px;
  border-radius: 2px;
  background: var(--k-line, #3a3458);
  margin: -8px auto 14px;
}

.toast {
  position: fixed;
  left: 50%;
  bottom: calc(96px + var(--k-safe-bottom));
  transform: translateX(-50%);
  z-index: 50;
  padding: 10px 16px;
  font-size: .82rem;
  border: 1px solid var(--k-gold-dim, #8a713f);
  background: rgba(18, 16, 31, .96);
  color: var(--k-parchment, #ece2c6);
  opacity: 0;
  transition: opacity .2s var(--k-ease);
  pointer-events: none;
  max-width: 90vw;
  text-align: center;
}
.toast[data-show] { opacity: 1; }

/* first-match coaching: at most three contextual hints, never a tutorial wall */
.hint {
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  padding: 7px 13px;
  font-size: .76rem;
  border: 1px solid var(--k-gold-dim, #8a713f);
  background: rgba(18, 16, 31, .95);
  color: var(--k-gold-bright, #e8c477);
  z-index: 20;
  pointer-events: none;
  white-space: nowrap;
}

/* ── ADS ──────────────────────────────────────────────────────────────
   Side rails on desktop, exactly ONE banner under the board on phones.
   The rules of this surface, in order of authority:

   1. An ad never overlaps the board. Rails and banner are flow siblings of
      the stage — never positioned over it, never given a positive z-index.
   2. An ad never appears during the reveal/clash beat. `body[data-reveal]`
      (set by timeline.mjs) blanks the whole surface with `visibility`, which
      costs no reflow and keeps the box reserved.
   3. An ad never shifts the layout. Every slot's box is reserved here at
      first paint AND is already full — the house ad ships in the markup, so
      there is no empty hole to fill later and nothing moves when (if) a real
      AdSense unit arrives in its place. CLS contribution: zero, by shape.
   4. An ad never costs a frame. `contain` walls each slot off from the
      board's layout, paint and style, so no third-party DOM inside it can
      reflow or repaint the game. ────────────────────────────────────── */

:root {
  /* the reserved boxes. Changing these is the only way to resize the surface. */
  --k-ad-banner-h: 56px;   /* 50px creative + 3px breathing room, top and bottom */
  --k-ad-rail-w: 176px;    /* 160px creative + 8px gutters */
}

.ad {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  /* the wall: nothing inside a slot can reflow, repaint or restyle the board */
  contain: layout paint style;
  isolation: isolate;
  z-index: 0;
  color: var(--k-parchment-dim, #d8cba9);
  background: linear-gradient(180deg, rgba(201, 164, 92, .05), transparent);
}

.ad--banner {
  flex: 0 0 var(--k-ad-banner-h);
  height: var(--k-ad-banner-h);
  padding: 3px 8px;
  border-top: 1px solid rgba(58, 52, 88, .5);
}
/* rails are desktop-only; on a phone they do not exist at all */
.ad--rail { display: none; }

/* The real AdSense unit. It is ALWAYS laid out, never `display:none`: an
   empty <ins> paints nothing, but a display:none container has no width for
   Google to measure, so it would never fill in the first place. It is stacked
   underneath the house creative, in the same reserved box — which is also why
   swapping one for the other cannot shift anything. Fixed pixel sizes on
   purpose: a "responsive" unit measures and resizes itself after load, and
   that is a layout shift. */
.ad-slot {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  z-index: 0;
}
.ad[data-mode="adsense"] .house { display: none; }

/* House ad — the default, and the fallback whenever AdSense is absent,
   unapproved or blocked. It must read as part of the frame: quiet, gold on
   navy, never louder than the board. */
.house {
  position: relative;
  z-index: 1;   /* on top of the empty ad slot until a real ad reports filled */
  display: flex;
  text-decoration: none;
  color: inherit;
  opacity: .72;
  transition: opacity .18s var(--k-ease);
}
.house:hover, .house:focus-visible { opacity: 1; }
.house:active { opacity: 1; transform: translateY(1px); }
.house-eyebrow {
  font-size: 8.5px;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--k-gold, #c9a45c);
}
.house-line {
  font-size: .72rem;
  line-height: 1.35;
  color: var(--k-parchment-dim, #d8cba9);
}
.house-cta {
  font-size: 8.5px;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--k-gold-bright, #e8c477);
  border-bottom: 1px solid var(--k-gold-dim, #8a713f);
  align-self: center;
  padding-bottom: 2px;
}

.ad--banner .house {
  width: 100%;
  height: 100%;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 0 6px;
}
/* one line, always: the banner truncates its middle rather than growing a
   second row, because a second row would resize a box the board depends on */
.ad--banner .house-eyebrow,
.ad--banner .house-cta { flex: 0 0 auto; white-space: nowrap; }
.ad--banner .house-line { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* On the narrowest phones the pitch survives and the CTA goes: the whole
   banner is the link, so the CTA was the redundant half. */
@media (max-width: 430px) {
  .ad--banner .house-cta { display: none; }
}

.ad--rail .house {
  flex-direction: column;
  align-items: center;
  gap: 12px;
  padding: 22px 14px;
  text-align: center;
}

/* the reveal beat blanks the whole surface: nothing competes with the hero
   moment, and no stray tap can land on an ad while the clash plays */
body[data-reveal] .ad,
body[data-ads-paused] .ad {
  visibility: hidden;
  pointer-events: none;
}

/* ── CONSENT ──────────────────────────────────────────────────────────
   One line, dismissible, remembered. Opened by ads.mjs only when real
   third-party ads are on, and only while the home screen is up — so it is
   physically impossible for it to sit over a match. Fixed, therefore out of
   flow, therefore it cannot shift anything either. */
.consent {
  display: none;
  position: fixed;
  left: 50%;
  bottom: calc(var(--k-ad-banner-h) + 10px + var(--k-safe-bottom));
  transform: translateX(-50%);
  z-index: 30;
  align-items: center;
  gap: 12px;
  width: min(460px, calc(100vw - 24px));
  padding: 8px 10px 8px 14px;
  border: 1px solid var(--k-gold-dim, #8a713f);
  background: rgba(18, 16, 31, .97);
}
.consent[data-open] { display: flex; }
body[data-reveal] .consent { display: none; }
.consent p {
  margin: 0;
  flex: 1;
  font-size: .7rem;
  line-height: 1.4;
  color: var(--k-parchment-dim, #d8cba9);
}
.consent-link {
  flex: 0 0 auto;
  font-size: .66rem;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--k-gold, #c9a45c);
}
.consent-ok {
  flex: 0 0 auto;
  min-width: 56px;
  min-height: 36px;
  padding: 6px 14px;
  font-family: var(--k-font-brand, Cinzel, serif);
  font-size: .74rem;
  letter-spacing: .14em;
  border: 1px solid var(--k-gold-dim, #8a713f);
  color: var(--k-gold-bright, #e8c477);
}

/* A short viewport is a landscape phone: the board needs every pixel, so the
   banner is removed outright rather than squeezed. Static, so still no CLS. */
@media (max-height: 620px) and (max-width: 1099px) {
  :root { --k-ad-banner-h: 0px; }
  .ad--banner { display: none; }
}

@media (min-width: 900px) {
  .board { max-width: 720px; margin: 0 auto; width: 100%; }
}

/* Rails only once the desktop rails are genuinely empty space: 1100px leaves
   the 720px board untouched after two 176px rails. Below that, the phone
   banner keeps the surface (never both, never neither). */
@media (min-width: 1100px) {
  :root { --k-ad-banner-h: 0px; }
  #app { flex-direction: row; }
  .ad--rail {
    display: flex;
    flex: 0 0 var(--k-ad-rail-w);
    width: var(--k-ad-rail-w);
    border-left: 1px solid rgba(58, 52, 88, .5);
  }
  .ad--rail:first-child { border-left: 0; border-right: 1px solid rgba(58, 52, 88, .5); }
  .ad--banner { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}

/* ── ACCOUNT ROW ───────────────────────────────────────────────────────────
   Present on the home screen from the first paint. It states who you are and
   offers the one action that changes it. Quiet by default: identity should
   never shout louder than QUICK MATCH. */
.account {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
  flex-wrap: wrap;
  margin-top: 14px;
  font-size: .7rem;
  letter-spacing: .06em;
}
.account-who { color: var(--k-parchment-dim, #9c93b8); }
.account-who[data-linked] { color: var(--k-gold-bright, #e8c477); }
.account-act {
  background: transparent;
  border: 1px solid var(--k-gold-dim, #8a713f);
  color: var(--k-gold, #c9a45c);
  font-family: inherit;
  font-size: .66rem;
  letter-spacing: .1em;
  padding: 7px 14px;
  min-height: 34px;
  cursor: pointer;
  border-radius: 2px;
}
.account-act:hover, .account-act:focus-visible {
  color: var(--k-gold-bright, #e8c477);
  border-color: var(--k-gold-bright, #e8c477);
}
.account-act[hidden] { display: none; }

/* ── THE FULL READING ──────────────────────────────────────────────────────
   One tap on any card opens this. The card itself is the subject — same
   renderer, same art, `full` LOD — and its text sits beside it on a wide
   screen, under it on a phone. The card FLIPs in from wherever it was tapped
   (see js/reading.mjs), which is the whole point: a player must never lose
   track of WHICH card grew.

   Two modes:
     modal  a scrim takes the next tap. For a card already committed to a lane,
            the rival's revealed card, or a Realm tile — nothing follows.
     loose  no scrim and pointer-events: none on everything but the card. For a
            card just picked up in hand: the lanes stay live underneath, so the
            lane is chosen while still looking at the card. */
.reading {
  position: fixed;
  inset: 0;
  z-index: 45;
  display: grid;
  place-items: center;
  padding: 16px;
  padding-bottom: calc(16px + var(--k-safe-bottom));
  pointer-events: none;
}
.reading[hidden] { display: none; }

.reading-scrim {
  position: absolute;
  inset: 0;
  background: rgba(8, 6, 16, .84);
  backdrop-filter: blur(4px);
  opacity: 0;
  transition: opacity .28s var(--k-ease, ease);
  pointer-events: none;
}
.reading[data-mode="modal"] .reading-scrim { pointer-events: auto; }
.reading[data-mode="modal"][data-open] .reading-scrim { opacity: 1; }

.reading-stage {
  position: relative;
  display: grid;
  gap: 14px;
  justify-items: center;
  align-content: center;
  max-width: min(880px, 100%);
  max-height: 100%;
}

/* The card is sized by HEIGHT so its 1:1.946 canon aspect decides the width —
   the same rule the lanes follow. Sizing by width would squash it again. */
.reading-card {
  height: min(52dvh, 420px);
  display: grid;
  place-items: center;
  pointer-events: auto;
  cursor: zoom-out;
  filter: drop-shadow(0 26px 46px rgba(0, 0, 0, .7)) drop-shadow(0 0 22px rgba(232, 196, 119, .22));
}
.reading-card > * { height: 100%; width: auto; max-width: 100%; }

.reading-text {
  pointer-events: auto;
  width: min(420px, 100%);
  max-height: 34dvh;
  overflow: auto;
  padding: 14px 16px calc(14px + var(--k-safe-bottom));
  border: 1px solid var(--k-gold-dim, #8a713f);
  background: linear-gradient(180deg, rgba(28, 25, 48, .96), rgba(18, 16, 31, .96));
  text-align: left;
}
/* LOOSE MODE lives in the top half and nowhere else.
   During planning the rival's rows and the battle stage are dead space, while
   the bottom half — your lanes, your Realm, your hand, LOCK & LAUNCH — is the
   half you are about to touch. A centred reading covered three of the five
   lanes it claimed to leave live, which made the promise a lie. So loose mode
   is pinned to the top, sized to stop short of your own row, and laid out
   side-by-side: the art is already familiar from the hand, the TEXT is what
   was unreadable. Modal mode, with nothing to choose next, stays big. */
.reading[data-mode="loose"] { align-items: start; padding-top: 6px; }
.reading[data-mode="loose"] .reading-text { pointer-events: none; }
.reading[data-mode="loose"] .reading-stage {
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: 12px;
  width: 100%;
  max-height: 34dvh;
}
.reading[data-mode="loose"] .reading-card {
  height: min(30dvh, 280px);
  filter: drop-shadow(0 14px 26px rgba(0, 0, 0, .72));
}
.reading[data-mode="loose"] .reading-text {
  width: 100%;
  max-height: 34dvh;
  padding: 10px 12px;
}
.reading[data-mode="loose"] .reading-name { font-size: .85rem; margin-bottom: 7px; }
.reading[data-mode="loose"] .reading-row { margin-bottom: 6px; }
.reading[data-mode="loose"] .reading-row p { font-size: .74rem; line-height: 1.4; }

.reading-name {
  font-family: var(--k-font-brand, Cinzel, serif);
  font-size: 1.05rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--k-gold-bright, #e8c477);
  margin: 0 0 10px;
}
.reading-row { margin: 0 0 9px; }
.reading-row:last-child { margin-bottom: 0; }
.reading-label {
  display: block;
  font-size: .58rem;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--k-gold, #c9a45c);
  opacity: .8;
  margin-bottom: 2px;
}
.reading-row p {
  margin: 0;
  font-size: .82rem;
  line-height: 1.5;
  color: var(--k-parchment, #e6dcc0);
}

/* a Realm tile has no card renderer, so it gets a plain plate at the same size */
.reading-tile {
  display: grid;
  place-items: center;
  gap: 8px;
  width: min(220px, 60vw);
  aspect-ratio: 1 / 1;
  padding: 16px;
  border: 1px solid var(--k-gold-dim, #8a713f);
  background: linear-gradient(180deg, #241f3d, #14121f);
}
.reading-tile img { width: 56%; height: auto; }
.reading-tile-name {
  font-family: var(--k-font-brand, Cinzel, serif);
  font-size: .82rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--k-gold-bright, #e8c477);
  text-align: center;
}
.reading-tile-hp { color: #d9556a; letter-spacing: .12em; }
.reading-tile-hp .lost { opacity: .22; }

/* Landscape and desktop have the width to put the text beside the card, which
   keeps both at a comfortable size instead of squeezing the card to fit text
   underneath it. */
@media (min-width: 720px) and (min-height: 420px) {
  .reading-stage {
    grid-template-columns: auto minmax(240px, 380px);
    align-items: center;
    gap: 26px;
  }
  .reading-card { height: min(66dvh, 540px); }
  .reading-text { max-height: 66dvh; }
  /* a wide screen has room above the lanes, but not unlimited room — and a
     text column stretched to 1100px is a wall, not a card reading */
  .reading[data-mode="loose"] .reading-stage {
    grid-template-columns: auto minmax(240px, 420px);
    justify-content: center;
    width: auto;
    max-height: 40dvh;
  }
  .reading[data-mode="loose"] .reading-card { height: min(36dvh, 340px); }
  .reading[data-mode="loose"] .reading-text { max-height: 40dvh; width: 100%; }
  .reading[data-mode="loose"] .reading-name { font-size: 1rem; }
  .reading[data-mode="loose"] .reading-row p { font-size: .8rem; }
}
@media (orientation: landscape) and (max-height: 520px) {
  .reading-stage { grid-template-columns: auto minmax(200px, 340px); gap: 16px; }
  .reading-card { height: min(78dvh, 320px); }
  .reading-text { max-height: 78dvh; }
}

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

/* ── SOUND SWITCHES ───────────────────────────────────────────────────────
   Every sound is synthesized at runtime (js/audio.mjs) — the project ships no
   audio files because it holds no licence for any. Sound defaults ON, the
   ambience bed OFF: the bed is the more intrusive of the two. */
.audio-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 16px;
  margin-top: 12px;
}
.hud-mute {
  background: transparent;
  border: 0;
  padding: 4px 8px;
  min-width: 34px;
  min-height: 34px;
  font-size: 1rem;
  line-height: 1;
  color: var(--k-gold, #c9a45c);
  cursor: pointer;
  opacity: .75;
  transition: opacity .16s var(--k-ease, ease), color .16s var(--k-ease, ease);
}
.hud-mute:hover, .hud-mute:focus-visible { opacity: 1; color: var(--k-gold-bright, #e8c477); }
.hud-mute[data-off="1"] { opacity: .4; color: var(--k-parchment-dim, #9c93b8); }

/* ── THE TUNING DESK ───────────────────────────────────────────────────────
   Owner-only, and it looks like a tool rather than a feature: dense rows, real
   numbers, the shipped default printed next to every dial that has moved. A
   playtest is read at arm's length between games, so nothing here is decorative
   and the changed rows are the ones that stand out. */
.admin-inner { width: min(560px, 100%); text-align: left; }
.admin-inner h2 { text-align: center; }
.admin-lead {
  margin: 0 0 14px;
  font-size: .72rem;
  line-height: 1.5;
  color: var(--k-parchment-dim, #d8cba9);
  opacity: .75;
  text-align: center;
}
.admin-dials {
  display: flex;
  flex-direction: column;
  gap: 2px;
  /* the dials scroll, the heading and the three buttons do not: an APPLY you
     have to scroll to find is how a tuning pass gets abandoned half-applied.
     Capped so the whole desk clears 88dvh with the buttons inside it. */
  max-height: min(42dvh, 360px);
  overflow-y: auto;
  overscroll-behavior: contain;
  margin-bottom: 12px;
  border: 1px solid var(--k-line, #3a3458);
  background: rgba(8, 6, 16, .45);
}
.dial {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 10px 12px;
  padding: 9px 12px;
  border-bottom: 1px solid rgba(58, 52, 88, .5);
}
.dial:last-child { border-bottom: 0; }
/* a dial that no longer matches the shipped profile says so, in the margin */
.dial[data-changed] { background: rgba(201, 164, 92, .07); box-shadow: inset 3px 0 0 var(--k-gold, #c9a45c); }
.dial-name { font-size: .8rem; color: var(--k-parchment, #ece2c6); }
.dial-hint {
  grid-column: 1 / -1;
  margin: -4px 0 0;
  font-size: .66rem;
  line-height: 1.4;
  color: var(--k-parchment-dim, #d8cba9);
  opacity: .55;
}
.dial-was {
  display: none;
  font-size: .62rem;
  letter-spacing: .06em;
  color: var(--k-gold, #c9a45c);
  opacity: .85;
}
.dial[data-changed] .dial-was { display: inline; }
.dial input[type="number"], .dial select {
  width: 92px;
  min-height: 40px;
  padding: 8px 10px;
  font-family: inherit;
  font-size: 16px;          /* 16px exactly: iOS zooms the viewport below it */
  color: var(--k-parchment, #ece2c6);
  background: rgba(8, 6, 16, .7);
  border: 1px solid var(--k-line, #3a3458);
  border-radius: 2px;
}
.dial select { width: 118px; }
.dial input:focus-visible, .dial select:focus-visible {
  outline: none;
  border-color: var(--k-gold, #c9a45c);
  box-shadow: 0 0 0 1px rgba(201, 164, 92, .35);
}
/* the switch: a real checkbox, sized for a thumb */
.dial input[type="checkbox"] {
  appearance: none;
  width: 44px;
  height: 26px;
  border-radius: 13px;
  border: 1px solid var(--k-line, #3a3458);
  background: rgba(8, 6, 16, .7);
  position: relative;
  cursor: pointer;
  transition: background .16s var(--k-ease, ease), border-color .16s var(--k-ease, ease);
}
.dial input[type="checkbox"]::after {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 18px;
  height: 18px;
  border-radius: 50%;
  background: var(--k-parchment-dim, #9c93b8);
  transition: transform .16s var(--k-ease, ease), background .16s var(--k-ease, ease);
}
.dial input[type="checkbox"]:checked { background: rgba(201, 164, 92, .3); border-color: var(--k-gold, #c9a45c); }
.dial input[type="checkbox"]:checked::after { transform: translateX(18px); background: var(--k-gold-bright, #e8c477); }
.dial input[type="checkbox"]:focus-visible { outline: 2px solid var(--k-gold-bright, #e8c477); outline-offset: 2px; }

.admin-open { margin-top: 4px; opacity: .7; }
.admin-open[hidden] { display: none; }

/* ── ROOM CODE ─────────────────────────────────────────────────────────────
   Ten letters someone is going to read out loud, so they are set the way a
   number you dictate is set: big, spaced, monospaced so every glyph occupies
   the same width and the eye can keep its place halfway through. */
.code {
  margin: 6px 0 2px;
  padding: 14px 10px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size: clamp(1.5rem, 8vw, 2.1rem);
  font-weight: 600;
  letter-spacing: .12em;
  text-align: center;
  color: var(--k-gold-bright, #e8c477);
  background: rgba(8, 6, 16, .6);
  border: 1px solid var(--k-gold-dim, #8a713f);
  user-select: all;               /* one tap selects the whole thing */
  -webkit-user-select: all;
}
.code-note {
  margin: 0 0 4px;
  font-size: .66rem;
  letter-spacing: .06em;
  text-align: center;
  color: var(--k-parchment-dim, #d8cba9);
  opacity: .6;
}
.code-input {
  width: 100%;
  min-height: 54px;
  padding: 12px;
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  /* 16px floor: iOS zooms the viewport on focus for anything smaller */
  font-size: max(16px, 1.3rem);
  letter-spacing: .1em;
  text-align: center;
  text-transform: lowercase;
  color: var(--k-gold-bright, #e8c477);
  background: rgba(8, 6, 16, .6);
  border: 1px solid var(--k-line, #3a3458);
  border-radius: 2px;
}
.code-input::placeholder { color: rgba(216, 203, 169, .28); }
.code-input:focus-visible, .code-input:focus {
  outline: none;
  border-color: var(--k-gold, #c9a45c);
  box-shadow: 0 0 0 1px rgba(201, 164, 92, .35);
}
