/* tokens.css — the shared semantic layer (parity Tier 3.1, docs/parity-pass.md).

   One design language for every surface: the desktop window, both harness
   pages, mobile/replay.html AND the member workspace load THIS file before
   their own stylesheet. It owns the SEMANTICS — results, rarity, status,
   type, space, radius — and derives every tint from its base color, so a
   surface or theme that swaps a base can never leave a stale tint behind
   (the rgba() copies this file killed had already drifted three ways).

   Identity stays ABOVE this layer, one block per surface:
   - desktop: theme.css :root (bnet default) + its [data-theme] swaps
   - web:     style.css :root (the warmmono skin — palette E, 2026-08-08)
   Each identity block defines --accent (and its surface palette); this
   file never does. Theme swaps may override any semantic token below —
   they load after this file, so the cascade lets them.

   Loading rule (checked by scripts/check_parity.py): any page that loads
   theme.css / style.css / replay-theme.css loads tokens.css FIRST.
   build_web_replay.py copies this file to mobile/ byte-for-byte. */

/* Inter Tight, self-hosted, in TWO slices with unicode-range — the way Google
   Fonts serves it. The browser fetches only the slice a page's characters
   need, so an English page costs the 45 KB latin file and nothing else.
   Measured 2026-08-20 and fixed 2026-08-22: the ONE file that stood here
   before was the CYRILLIC slice (224 glyphs, one Latin letter), so neither
   surface had ever rendered in this face — every visitor fell through to
   Segoe while the stylesheet named Inter Tight. assets/fonts/README.md holds
   the provenance and the cmap counts that prove these are the Latin ones. */
@font-face {
  font-family: "Inter Tight";
  src: url("assets/fonts/intertight-latin.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  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;
}
@font-face {
  font-family: "Inter Tight";
  src: url("assets/fonts/intertight-latin-ext.woff2") format("woff2");
  font-weight: 100 900;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}

:root {
  /* results — one green, one red, everywhere */
  --win: #2fd6a3;
  --loss: #ff5d6c;
  --unknown: #8b97ad;
  --win-tint: color-mix(in srgb, var(--win) 12%, transparent);
  --loss-tint: color-mix(in srgb, var(--loss) 12%, transparent);

  /* status */
  --warn: #ffb454;
  --err: var(--loss);

  /* card rarity ramp (Hearthstone palette; light themes may swap) */
  --rar-l: #ff8000;
  --rar-e: #a335ee;
  --rar-r: #4a9eff;
  --card-glow: 0 0 8px rgba(231, 237, 247, 0.25);
  --card-outline: #070b12;

  /* type — Inter Tight is self-hosted in assets/fonts/ (OFL, see its
     README): the @font-face rules above resolve relative to THIS stylesheet,
     so the same rules serve client/ui (file://) and mobile/ (the copied
     pair). Nick's pick from the 2026-08-08 typeface lab — a pick made over
     Segoe, since the file then was a Cyrillic slice (fixed 2026-08-22);
     Segoe stays as the fallback. */
  --font: "Inter Tight", "Segoe UI Variable", "Segoe UI", system-ui, sans-serif;
  /* display face (Nick 2026-08-13): Hearthstone's own stack, verbatim from
     Blizzard's official Hearthstone site. Belwe Bold is a COMMERCIAL Monotype face —
     the file is NOT shipped here; until a licensed woff2 lands in
     assets/fonts/ (then add its @font-face above), every visitor falls
     through to Georgia, which is on effectively every machine. A locally
     installed Belwe also matches by name, so Nick can preview by
     installing the font on his own box. */
  --font-display: "Belwe Bold", "Belwe Std Bold", Belwe, Georgia, Times, "Times New Roman", serif;
  --mono: "Cascadia Mono", Consolas, monospace;
  --fs-0: 12px; --fs-1: 13px; --fs-2: 14px; --fs-3: 16px; --fs-4: 20px;

  /* space + radius */
  --sp-1: 4px; --sp-2: 8px; --sp-3: 12px; --sp-4: 16px; --sp-5: 24px;
  --r-1: 6px; --r-2: 10px; --r-3: 14px;
}

/* the one icon system (Tier 3.3): sized by font, colored by context.
   Markup: <svg class="icon"><use href="#i-name"/></svg> — symbols are
   injected into the document by icons.js (file:// blocks external refs). */
.icon {
  width: 1.15em; height: 1.15em;
  display: inline-block; vertical-align: -0.18em;
  pointer-events: none;
}

/* read-spec B2: the player profile document. Same rule as .glance below —
   profile.js is ONE renderer shipped to both surfaces, so its styles live in
   the shared layer rather than being written twice. Do not re-implement in a
   surface sheet; a hand-twinned copy would drift the moment one surface is
   restyled, and the whole point of the copied renderer is that both surfaces
   show the player the same document. */
.pf-doc { max-width: 62ch; }
.pf-section { margin: 0 0 var(--sp-5); }
.pf-section h3 {
  margin: 0 0 var(--sp-2);
  font-size: var(--fs-3);
}
/* The printed evidence standard. Quieter than the heading because it is a
   PROMISE ABOUT the contents, not a heading of its own — and it is checked:
   a section standing on "only what they said outright" may not carry an item
   the coach inferred (profileItemAllowed). */
.pf-standard {
  font-weight: 400;
  font-size: var(--fs-1);
  color: var(--unknown);
}
.pf-list { margin: 0; padding-left: var(--sp-4); }
.pf-item { margin: 0 0 var(--sp-2); line-height: 1.45; }
.pf-lead { margin-right: var(--sp-1); }
/* Dates are the evidence mechanism (§3.3): appended on reinforcement, never
   rewritten, and the COUNT is the strength signal — so they render verbatim
   and terminal, quiet enough not to compete with the statement they date. */
.pf-dates {
  font-size: var(--fs-0);
  color: var(--unknown);
  white-space: nowrap;
}
.pf-generated { font-size: var(--fs-2); }
.pf-empty { color: var(--unknown); line-height: 1.5; max-width: 52ch; }

/* The document is SELECTABLE, and on the desktop that is not automatic: the
   app window turns selection off globally (a chromeless frame should not
   rubber-band its own chrome) and turns it back on element by element. This
   one was missed, which made the whole highlight ladder below unreachable
   there — you cannot correct what you cannot select. */
.pf-doc, .pf-empty { user-select: text; -webkit-user-select: text; }

/* The scroll box the document sits in. Each surface sizes and paints its own
   (a modal card is surface chrome), but the POSITIONING lives here because
   the shared renderer depends on it: the highlight offer below is placed
   inside this box, so this box must be its offsetParent. Without it the bar
   lands relative to whichever ancestor happens to be positioned. */
.pf-scroll { position: relative; }

/* B5's highlight toolbar. Nothing sends from here — it composes — so it reads
   as an offer, not an action. It is placed OVER THE SELECTION by
   mountProfileToolbar (Nick, 2026-08-18), absolutely inside .pf-scroll above.
   NOT `fixed`: a transformed ancestor becomes the containing block for fixed
   descendants, and the web's modal card is transform-centred, so a fixed bar
   landed 300px from the words it was about. Its top and left come from JS,
   so nothing here may set either. */
/* Nick 2026-08-18: "they are a pill but the pill is a button thats on top of
   the modal.. almost even hovering with a shadow."

   IT WAS GENUINELY TRANSPARENT, and not by styling choice: this file is
   BYTE-COPIED to the web, and it named `var(--bg-1)` — a token only the
   desktop's palette defines. On the web that resolved to nothing, so the bar
   painted onto the document and the words underneath read straight through
   the buttons. `--raised` is the bridging token each surface now defines,
   which is the rule this broke: a SHARED stylesheet may only name tokens
   BOTH surfaces define. `check_parity`'s `tokens-shared` pins it. */
.pf-toolbar {
  position: absolute;
  z-index: 30;
  display: flex;
  gap: var(--sp-1);
  align-items: center;
  padding: 5px;
  border: 1px solid var(--line);
  border-radius: 999px;                 /* the pill */
  background: var(--raised);            /* OPAQUE — never see the doc through it */
  box-shadow: 0 12px 28px color-mix(in srgb, black 55%, transparent),
              0 2px 6px color-mix(in srgb, black 35%, transparent);
  font-size: var(--fs-1);
  /* A phone's document is narrower than two verbs on one line, and place()
     can only clamp x to 0 — the second verb then hung off the right edge,
     clipped by the scroller, on the surface where it is the ONLY way to edit
     memory. Wrapping is the honest answer at any width that cannot hold it;
     each verb still refuses to break inside itself. */
  flex-wrap: wrap;
  max-width: 100%;
}
/* each verb is its own pill, filled rather than outlined — an outline over a
   document reads as part of the document */
.pf-verb {
  font: inherit;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 7px 14px;
  border-radius: 999px;
  border: 1px solid transparent;
  background: var(--raised-hi);
  color: var(--raised-ink);
  white-space: nowrap;
}
.pf-verb:hover { border-color: var(--accent); color: var(--accent); }

/* B4's receipt. Quiet but not hidden: it is the structural enforcement of
   user-invoked-only, so it must be legible in the thread on both surfaces
   without reading as a coach turn. Shared for the same reason the document
   above is. */
.msg-receipt {
  font-size: var(--fs-1);
  color: var(--unknown);
  padding: var(--sp-2) 0;
  border-left: 2px solid var(--unknown);
  padding-left: var(--sp-3);
  margin: var(--sp-2) 0;
}

/* glance-spec G4: the live frame. Card image + context. Both rooms load
   this file first; do not re-implement in a surface sheet. */
.glance {
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  min-width: 0;
}
.glance-art {
  width: 92px;
  height: auto;
  flex: 0 0 92px;
  border-radius: var(--r-1);
  display: block;
  /* Nick 2026-08-29: the frame's card is small and the mouse on it did
     nothing — it enlarges under the pointer (a phone has no hover and keeps
     the 92px frame). Scaled, not re-laid: the row's box never moves. */
  position: relative;
  transform-origin: left center;
  transition: transform .12s ease;
  cursor: zoom-in;
}
.glance-art:hover { transform: scale(2.4); z-index: 20; }
.glance-why {
  min-width: 0;
  font-size: var(--fs-2);
  line-height: 1.35;
}
.glance.is-new .glance-art { animation: glance-in .35s ease; }
@keyframes glance-in {
  from { opacity: 0; transform: scale(.96); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .glance.is-new .glance-art { animation: none; }
}

/* tour-spec §4 (R-38): the first-landing tour's layer. tour.js is ONE file
   byte-copied to both surfaces, so its paint lives here too — and reads only
   tokens BOTH surfaces define (check_parity `tokens-shared`): --accent,
   --accent-ink, --raised, --raised-ink, --raised-hi, --line, plus this
   file's own. The veil is the desktop's own overlay ground (theme.css
   #modal-veil, 58% black); tour.js sets the layer's top from the surface's
   inset — 0 on the web, --titlebar-h on the desktop — so the frame stays
   draggable (I-01). The layer itself catches nothing: the veil does, the
   bubble does, and the hole cut in the veil is where the real element is. */
.tour-layer {
  position: fixed; left: 0; right: 0; bottom: 0; top: 0;
  z-index: 80;                 /* under #toast on both surfaces: an error is SEEN */
  pointer-events: none;
  font-family: var(--font);
}
.tour-veil {
  position: absolute; inset: 0;
  pointer-events: auto;
  background: color-mix(in srgb, black 58%, transparent);
}
.tour-ring {
  position: absolute;
  border-radius: var(--r-3);
  pointer-events: none;
  box-shadow: 0 0 0 2px var(--accent),
              0 0 0 6px color-mix(in srgb, var(--accent) 22%, transparent);
  animation: tour-pulse 2.2s ease-in-out infinite;
}
@keyframes tour-pulse {
  0%, 100% { box-shadow: 0 0 0 2px var(--accent), 0 0 0 6px color-mix(in srgb, var(--accent) 22%, transparent); }
  50%      { box-shadow: 0 0 0 2px var(--accent), 0 0 0 10px color-mix(in srgb, var(--accent) 8%, transparent); }
}
.tour-bubble {
  position: absolute;
  box-sizing: border-box;
  pointer-events: auto;
  padding: var(--sp-4) var(--sp-4) var(--sp-3);
  border: 1px solid var(--line);
  border-radius: var(--r-3);
  background: var(--raised);
  color: var(--raised-ink);
  box-shadow: 0 18px 48px color-mix(in srgb, black 55%, transparent),
              0 6px 16px color-mix(in srgb, black 40%, transparent);
  font-size: var(--fs-2);
  line-height: 1.5;
  outline: none;
  /* glides between stops (tour.js keeps the old spot until the new anchor is found) */
  transition: top .36s cubic-bezier(.22, 1, .36, 1), left .36s cubic-bezier(.22, 1, .36, 1);
}
.tour-bubble:focus-visible { border-color: var(--accent); }
/* the tail: a rotated square sharing the card's ground and border, so it
   reads as one surface pointing at the element */
.tour-caret {
  position: absolute; width: 14px; height: 14px;
  transform: rotate(45deg);
  background: var(--raised);
  border: 1px solid var(--line);
}
.tour-caret[data-side="top"]    { top: -8px;    border-right: 0; border-bottom: 0; }
.tour-caret[data-side="bottom"] { bottom: -8px; border-left: 0;  border-top: 0; }
.tour-caret[data-side="left"]   { left: -8px;   border-right: 0; border-top: 0; }
/* the speaker: OP's SETTLED face through md.js's one builder (R-38) */
.tour-head { display: flex; align-items: center; gap: var(--sp-2); margin-bottom: var(--sp-2); font-weight: 700; }
.tour-head .op-face { display: inline-flex; align-items: center; }
.tour-head .op-face-svg { display: block; width: 2.2em; height: 2.2em; }
/* R-50 (Nick 2026-09-01): the face standing AS the name inside a sentence —
   a hint, the consent bar, the finished bar, a tour line, a public tile.
   Glyph-sized and on the baseline; never a clock (faceLatest skips it). */
.op-face[data-inline] { display: inline-flex; align-items: center; vertical-align: -.3em; }
.op-face[data-inline] .op-face-svg { display: block; width: 1.3em; height: 1.3em; }
.tour-title { margin: 0; font-size: var(--fs-3); font-weight: 700; letter-spacing: -.01em; }
.tour-body { margin: var(--sp-1) 0 0; color: color-mix(in srgb, var(--raised-ink) 74%, transparent); }
.tour-foot { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-3); margin-top: var(--sp-4); }
.tour-dots { display: flex; gap: 5px; }
.tour-dots i { display: block; width: 6px; height: 6px; border-radius: 999px; background: var(--line); transition: width .2s, background .2s; }
.tour-dots i.on { width: 16px; background: var(--accent); }
.tour-acts { display: flex; gap: var(--sp-1); }
.tour-back, .tour-next, .tour-skip { font: inherit; cursor: pointer; border: 0; border-radius: var(--r-2); }
.tour-back { padding: 7px 10px; background: none; color: color-mix(in srgb, var(--raised-ink) 70%, transparent); }
.tour-back:hover { background: var(--raised-hi); color: var(--raised-ink); }
.tour-next { padding: 7px 14px; background: var(--accent); color: var(--accent-ink); font-weight: 650; }
.tour-next:hover { filter: brightness(1.08); }
.tour-skip { margin-top: var(--sp-2); padding: 2px 0; background: none; font-size: var(--fs-0); color: color-mix(in srgb, var(--raised-ink) 55%, transparent); }
.tour-skip:hover { color: var(--raised-ink); }
/* the one accessibility law this repo already keeps on two surfaces
   (face-spec F1): under reduced motion the ring holds still and the bubble
   lands instead of gliding */
@media (prefers-reduced-motion: reduce) {
  .tour-ring { animation: none; }
  .tour-bubble { transition: none; }
}

/* skeleton-spec (2026-08-27): the wait state for a region a fetch leaves
   blank. ONE primitive, shared — md.js's skeleton builders are byte-copied
   to both surfaces and their paint lives here, reading only tokens BOTH
   surfaces define (check_parity `tokens-shared`): --raised-ink, plus this
   file's own radius. The ground is the surface's ink at a tenth, so a
   light theme and a dark one each get a grey that belongs to them
   (nextclip's bg-muted-foreground/15, derived rather than copied).

   A skeleton is CHROME (md.js's loading-word law): no word, no number, no
   name — a block the size of the thing that is coming and nothing a
   player could read as a claim. It stands ONLY while a read is in flight
   (I-02: absent is never zero — the surface swaps it for the answer, the
   empty state, or the error the moment one lands), never over content
   already painted (boot is cache-first), and never in a conversation pane.
   Composites carry the surface's own row/tile classes, so this file sizes
   nothing but the line. The look — pulse, tint, radius — is a first cut
   and Nick's to restyle; the laws above are not. */
.sk {
  display: block;
  border-radius: var(--r-1);
  background: color-mix(in srgb, var(--raised-ink) 10%, transparent);
  animation: sk-pulse 1.6s ease-in-out infinite;
}
/* a line of text: sits inline where the words will, one em tall */
.sk-line { display: inline-block; height: 1em; vertical-align: middle; max-width: 100%; }
/* a disc — an avatar */
.sk-disc { border-radius: 50%; }
/* fills the box it sits in — an avatar cell, a board frame */
.sk-fill { width: 100%; height: 100%; }
/* a composite's root: no pointer, no selection; aria-hidden is set by skGhost */
.sk-ghost { pointer-events: none; user-select: none; -webkit-user-select: none; }
@keyframes sk-pulse { 50% { opacity: .45; } }
/* face-spec F1's law, kept here too: under reduced motion the block holds
   still and keeps its ground */
@media (prefers-reduced-motion: reduce) {
  .sk { animation: none; }
}
