/* Dicebox — line-work dice on a near-white field.
   The palette is almost entirely value; the one hue is reserved for the total. */

:root {
  --paper:   #FCFCFA;
  --face:    #FFFFFF;
  --line:    #1A1A18;
  --muted:   #9A968C;
  --hair:    #E4E2DC;
  --accent:  #3D5A45;
  --danger:  #8C3A2E;

  --tap: 44px;
  --gut: clamp(16px, 5vw, 28px);
}

:root[data-theme="dark"] {
  --paper:  #141413;
  --face:   #1E1E1C;
  --line:   #F2F0EA;
  --muted:  #6E6A62;
  --hair:   #2A2A27;
  --accent: #8FB79A;
  --danger: #D98878;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --paper:  #141413;
    --face:   #1E1E1C;
    --line:   #F2F0EA;
    --muted:  #6E6A62;
    --hair:   #2A2A27;
    --accent: #8FB79A;
    --danger: #D98878;
  }
}

* { box-sizing: border-box; }

/* The `hidden` attribute is only `display: none` in the UA stylesheet, so any
   class that sets display beats it. Without this, .sheet (display:flex) and
   .help render on load despite being marked hidden — the sheet covered the whole
   page in an unclickable blur. */
[hidden] { display: none !important; }

html, body {
  margin: 0;
  height: 100%;
  overscroll-behavior: none;
}

body {
  background: var(--paper);
  color: var(--line);
  font-family: "Inter Tight", system-ui, -apple-system, sans-serif;
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
  transition: background 240ms ease, color 240ms ease;
}

.app {
  display: flex;
  flex-direction: column;
  height: 100%;
  max-width: 620px;
  margin: 0 auto;
  padding: env(safe-area-inset-top) var(--gut) calc(env(safe-area-inset-bottom) + 12px);
  gap: 10px;
}

/* ---- header ---- */

.bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-top: 14px;
  flex: 0 0 auto;
}

/* Present on arrival, then out of the way. Once you have rolled, the name has
   done its job and the tray should be the only thing asking for attention. */
.wordmark {
  margin: 0;
  font-size: 15px;
  font-weight: 500;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--muted);
  transition: opacity 1200ms ease;
}
.wordmark[data-faded="1"] { opacity: 0.28; }

/* The active system, beside the name. It sits in the accent because the accent
   is the thing that changed, so the badge names the colour the app is wearing.
   `margin-right: auto` keeps it next to the wordmark instead of being pushed to
   the middle by the bar's space-between. */
.system-badge {
  appearance: none;
  margin: 0 0 0 10px;
  margin-right: auto;
  border: 1px solid var(--accent);
  background: none;
  color: var(--accent);
  font: inherit;
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  padding: 3px 9px;
  border-radius: 999px;
  cursor: pointer;
}

@media (prefers-reduced-motion: reduce) {
  .wordmark { transition-duration: 1ms; }
}

.bar-actions {
  display: flex;
  align-items: center;
  gap: 6px;
}

.ghost {
  appearance: none;
  border: 1px solid var(--hair);
  background: none;
  color: var(--muted);
  font: inherit;
  font-size: 12px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  min-height: 32px;
  padding: 0 12px;
  border-radius: 999px;
  cursor: pointer;
}

.ghost.icon {
  width: 32px;
  padding: 0;
  font-size: 14px;
  letter-spacing: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

/* One icon at a time: the sun in light mode, the moon in dark. */
#themeToggle [data-icon] { display: none; }
#themeToggle[data-mode="light"] [data-icon="sun"] { display: block; }
#themeToggle[data-mode="dark"] [data-icon="moon"] { display: block; }

.ghost[aria-expanded="true"] {
  color: var(--accent);
  border-color: var(--accent);
}

/* ---- help ---- */

/* Fills the tray rather than floating over the page: the tray is already a
   bounded rectangle, so nothing below it shifts when the panel opens. */
/* The syntax reference is a corner popover under its ? button, the same shape
   as the mode picker — not a layer over the tray. Tall content scrolls inside. */
.help {
  position: fixed;
  z-index: 40;
  width: min(400px, calc(100vw - 20px));
  max-height: min(74vh, 600px);
  overflow-y: auto;
  scrollbar-width: thin;
  padding: 12px 16px;
  background: var(--face);
  border: 1px solid var(--hair);
  border-radius: 14px;
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.28);
  animation: help-in 180ms ease both;
}
.help::-webkit-scrollbar { width: 4px; }

/* The room panel: the same corner popover, under the room button. */
.room-pop {
  position: fixed;
  z-index: 40;
  width: min(340px, calc(100vw - 20px));
  max-height: min(78vh, 620px);
  overflow-y: auto;
  scrollbar-width: thin;
  padding: 12px 16px 14px;
  background: var(--face);
  border: 1px solid var(--hair);
  border-radius: 14px;
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.28);
  animation: help-in 180ms ease both;
}
.room-pop::-webkit-scrollbar { width: 4px; }

/* A popover's title line — the sheet-title look without its close button. */
.pop-title {
  margin: 0 0 8px;
  font-family: "Iosevka Etoile", ui-monospace, monospace;
  font-size: 12px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
}

@keyframes help-in {
  from { opacity: 0; transform: translateY(-3px); }
  to   { opacity: 1; transform: none; }
}

.syntax {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 1px 14px;
  margin: 0;
  align-items: baseline;
}

/* Each example is tappable, so the reference doubles as a set of presets. */
.syntax dt {
  font-family: "Iosevka Etoile", ui-monospace, "SF Mono", monospace;
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  color: var(--line);
  text-align: right;
  cursor: pointer;
  border-radius: 4px;
  padding: 1px 3px;
  margin: -1px -3px;
  transition: color 140ms ease;
}
.syntax dt:hover,
.syntax dt:focus-visible { color: var(--accent); }

.syntax dd {
  margin: 0;
  font-size: 12.5px;
  color: var(--muted);
}

.help .note {
  margin: 12px 0 0;
  font-size: 12px;
  line-height: 1.5;
  color: var(--muted);
  max-width: 44ch;
}

.help .install {
  margin-top: 10px;
}

.install-button {
  appearance: none;
  border: 1px solid var(--accent);
  border-radius: 8px;
  background: none;
  color: var(--accent);
  font: inherit;
  font-size: 12.5px;
  min-height: 30px;
  padding: 0 12px;
  margin-right: 8px;
  cursor: pointer;
}
.install-button:disabled { opacity: 0.5; }

.help .colophon {
  margin: 10px 0 0;
  font-size: 12px;
  /* A wrapping row rather than flowing text: a link that no longer fits moves
     to the next line whole instead of breaking mid-phrase against the edge. */
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: 6px;
  row-gap: 4px;
}

/* Which build is loaded. Quiet and monospaced — it is a serial number, not
   information anyone reads on purpose, and it only earns its place on the days
   when a cached copy is lying about what it is. */
.build {
  display: block;
  flex-basis: 100%;
  margin-top: 6px;
  font-family: "Iosevka Etoile", ui-monospace, monospace;
  font-size: 10.5px;
  letter-spacing: 0.04em;
  color: var(--muted);
  opacity: 0.65;
}

.colophon-sep {
  color: var(--muted);
}

/* The fine print: trademark/affiliation notice and the credits the publishers'
   policies ask for. Quiet, small, and last. */
.disclaimer {
  margin: 12px 0 0;
  font-size: 10.5px;
  line-height: 1.5;
  color: var(--muted);
  opacity: 0.85;
}

.help .colophon a {
  color: var(--accent);
  text-decoration: none;
  border-bottom: 1px solid currentColor;
  padding-bottom: 1px;
  white-space: nowrap;
}

/* ---- tray ---- */

.tray {
  position: relative;
  flex: 1 1 auto;
  min-height: 180px;
  touch-action: none;
}

.tray canvas {
  display: block;
  width: 100%;
  height: 100%;
}

/* The one-time intro. Lines fade in one after another, then the whole thing
   leaves — it should read like a title card, not a tooltip. */
.intro {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 7px;
  pointer-events: none;
  text-align: center;
  transition: opacity 700ms ease;
}
.intro[data-gone="1"] { opacity: 0; }

.intro p {
  margin: 0;
  font-size: 13px;
  letter-spacing: 0.13em;
  text-transform: uppercase;
  color: var(--muted);
  opacity: 0;
  animation: intro-line 620ms ease forwards;
}
.intro p:nth-child(1) { animation-delay: 260ms; }
.intro p:nth-child(2) { animation-delay: 660ms; }
.intro p:nth-child(3) { animation-delay: 1060ms; }

@keyframes intro-line {
  from { opacity: 0; transform: translateY(4px); }
  to   { opacity: 0.8; transform: none; }
}

.intro-last { color: var(--accent) !important; }

@media (prefers-reduced-motion: reduce) {
  .intro p { animation-duration: 1ms; animation-delay: 0ms; opacity: 0.8; }
}

/* ---- readout ---- */

.readout {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  flex: 0 0 auto;
  /* So a headline made of words has an edge to wrap against rather than the
     width of its own longest line. */
  width: 100%;
  min-width: 0;
}

.total {
  font-family: "Iosevka Etoile", ui-monospace, "SF Mono", monospace;
  font-size: clamp(56px, 19vw, 104px);
  font-weight: 600;
  line-height: 0.92;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
  transition: opacity 160ms ease;
}

/* Some systems answer with a phrase rather than a number, and a phrase at the
   numeral's size runs past both edges of a phone and then wraps — onto a second
   line set at 0.92 line-height, which is what put "Bestial Failure" on top of
   itself. Words get their own size and their own leading. */
.total[data-kind="text"] {
  font-family: inherit;
  font-size: clamp(28px, 9vw, 46px);
  font-weight: 600;
  line-height: 1.08;
  letter-spacing: -0.01em;
  text-align: center;
  max-width: 100%;
  text-wrap: balance;
}
.total[data-idle="1"] { color: var(--muted); }
.total[data-rolling="1"] { opacity: 0.35; }

/* Results in the torchlight. While the Shadowdark torch burns (body carries
   data-torchlit from syncTorch), the readout warms: the headline leans
   toward the flame's gold and the breakdown lifts out of the cold muted
   grey; torch dies, words go cold. Token-anchored mixes so both schemes
   follow their own palette. Contrast holds AA: the headline is large text
   (56px+, 3:1 bar) and the warmed breakdown stays >=4.5:1 on both schemes'
   paper and face. Outcome-variant headlines (a nat-20's gold) keep their
   own colour — the variant is the louder fact. */
body[data-torchlit] .total:not([data-variant]) {
  color: color-mix(in srgb, var(--accent) 82%, #F2C14E 18%);
}
body[data-torchlit] .breakdown {
  color: color-mix(in srgb, var(--muted) 78%, var(--accent) 22%);
}

.breakdown {
  font-size: 13px;
  color: var(--muted);
  text-align: center;
  min-height: 1.3em;
  font-variant-numeric: tabular-nums;
  word-break: break-word;
}

/* ---- dice picker ---- */

.picker {
  display: flex;
  align-items: center;
  gap: 8px;
  flex: 0 0 auto;
}

/* The mode button shows only the active system's mark. */
.mode-ico { display: none; }
#modeToggle[data-system="numeric"] .mode-ico[data-for="numeric"] { display: block; }
#modeToggle[data-system="v5"] .mode-ico[data-for="v5"] { display: block; }
#modeToggle[data-system="fate"] .mode-ico[data-for="fate"] { display: block; }
#modeToggle[data-system="tarot"] .mode-ico[data-for="tarot"] { display: block; }
#modeToggle[data-system="napoletane"] .mode-ico[data-for="napoletane"] { display: block; }
#modeToggle[data-system="hanafuda"] .mode-ico[data-for="hanafuda"] { display: block; }
#modeToggle[data-system="utagaruta"] .mode-ico[data-for="utagaruta"] { display: block; }
#modeToggle[data-system="genesys"] .mode-ico[data-for="genesys"] { display: block; }
#modeToggle[data-system="daggerheart"] .mode-ico[data-for="daggerheart"] { display: block; }
#modeToggle[data-system="cthulhutech"] .mode-ico[data-for="cthulhutech"] { display: block; }
#modeToggle[data-system="yearzero"] .mode-ico[data-for="yearzero"] { display: block; }
#modeToggle[data-system="alien"] .mode-ico[data-for="alien"] { display: block; }
#modeToggle[data-system="bladerunner"] .mode-ico[data-for="bladerunner"] { display: block; }
#modeToggle[data-system="twilight"] .mode-ico[data-for="twilight"] { display: block; }
#modeToggle[data-system="callofcthulhu"] .mode-ico[data-for="callofcthulhu"] { display: block; }
#modeToggle[data-system="deltagreen"] .mode-ico[data-for="deltagreen"] { display: block; }
#modeToggle[data-system="ironsworn"] .mode-ico[data-for="ironsworn"] { display: block; }
#modeToggle[data-system="starforged"] .mode-ico[data-for="starforged"] { display: block; }
#modeToggle[data-system="dcc"] .mode-ico[data-for="dcc"] { display: block; }
#modeToggle[data-system="starwars"] .mode-ico[data-for="starwars"] { display: block; }
#modeToggle[data-system="onering"] .mode-ico[data-for="onering"] { display: block; }
#modeToggle[data-system="pbta"] .mode-ico[data-for="pbta"] { display: block; }
#modeToggle[data-system="mist"] .mode-ico[data-for="mist"] { display: block; }
#modeToggle[data-system="mothership"] .mode-ico[data-for="mothership"] { display: block; }
#modeToggle[data-system="crows"] .mode-ico[data-for="crows"] { display: block; }
#modeToggle[data-system="drawsteel"] .mode-ico[data-for="drawsteel"] { display: block; }
#modeToggle[data-system="stonetop"] .mode-ico[data-for="stonetop"] { display: block; }
#modeToggle[data-system="fitd"] .mode-ico[data-for="fitd"] { display: block; }
#modeToggle[data-system="shadowdark"] .mode-ico[data-for="shadowdark"] { display: block; }
#modeToggle[data-system="dccarl"] .mode-ico[data-for="dccarl"] { display: block; }
#modeToggle[data-system="cards"] .mode-ico[data-for="cards"] { display: block; }
/* A named system's mark carries its own accent so the header reads as that mode
   at a glance; the numeric default stays with the quiet chrome colour. */
#modeToggle[data-system="v5"],
#modeToggle[data-system="fate"],
#modeToggle[data-system="genesys"],
#modeToggle[data-system="daggerheart"],
#modeToggle[data-system="cthulhutech"],
#modeToggle[data-system="starwars"],
#modeToggle[data-system="onering"],
#modeToggle[data-system="pbta"],
#modeToggle[data-system="mist"],
#modeToggle[data-system="mothership"],
#modeToggle[data-system="cards"] { color: var(--accent); }

/* ---- Vampire dice tray ---- */

/* The dice to tap, then a Difficulty chip beneath. */
/* Vampire: a row of die-shaped adders + the difficulty chip, with the Willpower
   reroll popping above. Pool carries the ink, Hunger and Rouse the blood accent
   the V5 palette already sets. Reuses the shared .die-btn, so it reads like
   every other system's picker. */
.v5-picker { flex-direction: column; align-items: center; gap: 10px; }
.v5-row { display: flex; gap: 12px; justify-content: center; align-items: center; flex-wrap: wrap; }
/* Scoped to the picker so these beat .die-btn's own --dc default, which sits
   later in the sheet — Pool in ink, Hunger and Rouse in the blood accent. */
.v5-picker .v5-pool { --dc: var(--line); }
.v5-picker .v5-hunger, .v5-picker .v5-rouse { --dc: var(--accent); }
/* Line-art die marks — the ankh and the fangs — in the button's own colour, so
   they carry no fill the way the shared .die-ico silhouettes do. */
.v5-die-ico { width: 30px; height: 30px; display: block; }
.v5-die-ico path, .v5-die-ico circle { fill: none; stroke: currentColor; }
/* Rouse staged: the die is in hand waiting to be thrown, so the button reads as
   loaded even though it carries no count. */
.v5-picker .v5-rouse.is-staged { border-color: var(--dc); background: color-mix(in srgb, var(--dc) 12%, transparent); }
.v5-picker .v5-rouse.is-staged .die-btn-label { color: var(--line); }

/* ---- Fate dice ---- */

/* Fate is fixed at 4dF, so the picker is just the modifier chip, centred. */
.fate-picker { justify-content: center; }

/* ---- Genesys dice ---- */

.genesys-picker { padding: 0; }
/* One row of buttons shaped like the real dice — tap to add, hold to remove. */
.gen-chips {
  display: flex;
  gap: 6px;
  width: 100%;
}
.gen-chip {
  --dc: var(--muted);
  flex: 1 1 0;
  min-width: 0;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 9px 2px 6px;
  border: 1px solid var(--hair);
  border-radius: 12px;
  background: none;
  color: var(--dc);
  font: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transition: border-color 140ms ease, background 140ms ease, transform 120ms ease;
}
/* The die silhouette: a faint fill of its colour under a solid outline. */
.die-ico { width: 27px; height: 27px; display: block; }
.die-ico path, .die-ico rect {
  fill: currentColor;
  fill-opacity: 0.16;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linejoin: round;
}
.gen-chip-label {
  white-space: nowrap;
  font-size: 10.5px;
  color: var(--muted);
  letter-spacing: 0.01em;
}
/* A loaded chip lights up in its die's colour, its silhouette fills, and a count
   badge rides the corner. */
.gen-chip[data-count] { border-color: var(--dc); background: color-mix(in srgb, var(--dc) 12%, transparent); }
.gen-chip[data-count] .gen-chip-label { color: var(--line); }
.gen-chip[data-count] .die-ico path, .gen-chip[data-count] .die-ico rect { fill-opacity: 0.34; }
.gen-chip[data-count]::after {
  content: attr(data-count);
  position: absolute;
  top: -6px;
  right: -4px;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  border-radius: 8px;
  background: var(--dc);
  color: var(--paper);
  font: 700 10px system-ui;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  display: flex;
  align-items: center;
  justify-content: center;
}
.gen-chip:active { transform: scale(0.94); }

/* The die-type hues, constant across light and dark (matched to the on-tray
   dice in app.js GEN_COLORS). */
.gen-chip[data-type="ability"]     { --dc: #4E9E60; }
.gen-chip[data-type="proficiency"] { --dc: #C39A2E; }
.gen-chip[data-type="boost"]       { --dc: #4C86C6; }
.gen-chip[data-type="difficulty"]  { --dc: #8A5CC0; }
.gen-chip[data-type="challenge"]   { --dc: #C24046; }
.gen-chip[data-type="setback"]     { --dc: #7C828A; }
/* The staged Force die's neutral metal grey — FORCE_STAGE_COLOR in app.js;
   the chip and the die it stages must be the same grey. */
.gen-chip[data-type="force"]       { --dc: #9AA0A6; }

/* The Force chip belongs to Star Wars only — hidden until that mode adds it. */
.genesys-picker:not(.with-force) .gen-chip-force { display: none; }

/* Year Zero — the Genesys chip rack, coloured per type (the dice all share a
   d6 shape, so colour is what tells Base from Stress). Matched to YZ_COLORS. */
.yz-picker { padding: 0; display: flex; flex-direction: column; align-items: stretch; gap: 8px; }
.yz-chip[data-type="base"]   { --dc: #D9A441; }
.yz-chip[data-type="skill"]  { --dc: #5BA860; }
.yz-chip[data-type="gear"]   { --dc: #8C919A; }
.yz-chip[data-type="stress"] { --dc: #D86A3A; }
/* Year Zero rolls Base/Skill/Gear; Alien swaps Gear out for the Stress die. */
.yz-picker:not(.alien) .yz-chip[data-type="stress"] { display: none; }
.yz-picker.alien .yz-chip[data-type="gear"] { display: none; }
/* A resting chip carries a faint wash of its die colour so the four read apart. */
.yz-chip .die-ico path { fill-opacity: 0.24; }
/* Push appears on the result and rerolls everything that is not a 6 or a 1. */
.yz-push {
  appearance: none; align-self: center; cursor: pointer;
  padding: 6px 22px; border: 1px solid var(--accent); border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 14%, transparent); color: var(--line);
  font: 600 13px/1 inherit; letter-spacing: 0.02em;
  transition: background 140ms ease, transform 120ms ease;
}
.yz-push:hover { background: color-mix(in srgb, var(--accent) 22%, transparent); }
.yz-push:active { transform: scale(0.96); }

/* The V5 post-roll actions share one row and each wears its mark — a hidden
   pair collapses to nothing, so the row never reserves space. */
.v5-actions { display: flex; gap: 8px; justify-content: center; }
.v5-actions .yz-push { display: inline-flex; align-items: center; gap: 7px; }
/* The display override above would beat the UA's [hidden] rule — restate it,
   or the pair would show before any roll. */
.v5-actions .yz-push[hidden] { display: none; }
.v5-act-ico { width: 17px; height: 17px; display: block; flex: none; }
/* The remembered Surge size rides the button, the way every persistent stat
   shows itself. */
.v5-actions .yz-push { position: relative; }
.v5-actions .yz-push[data-count]::after {
  content: attr(data-count);
  position: absolute; top: -6px; right: -4px;
  min-width: 16px; height: 16px; padding: 0 4px; border-radius: 8px;
  background: var(--accent); color: var(--paper);
  font: 700 10px system-ui; line-height: 1; font-variant-numeric: tabular-nums;
  display: flex; align-items: center; justify-content: center;
}

/* While a Willpower reroll is arming, the button is the way out: read it as a
   cancel, filled and quiet rather than inviting. */
.v5-willpower.is-arming {
  background: color-mix(in srgb, var(--line) 14%, transparent);
  border-color: color-mix(in srgb, var(--line) 34%, transparent);
  color: var(--muted);
}

/* Blade Runner: two step-die selectors (Attribute teal, Skill amber) and an odds
   stepper that greens for advantage, reddens for disadvantage. */
.br-picker { display: flex; flex-direction: column; align-items: stretch; gap: 8px; padding: 0; }
.br-dice { display: flex; gap: 8px; }
.br-die {
  flex: 1 1 0; min-width: 0; position: relative;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px;
  padding: 9px 6px; border: 1px solid var(--hair); border-radius: 12px;
  background: none; color: var(--line); font: inherit; cursor: pointer;
  transition: border-color 140ms ease, background 140ms ease, transform 120ms ease;
}
.br-die:active { transform: scale(0.95); }
.br-die .die-ico { width: 24px; height: 24px; }
.br-die-label { font-size: 9.5px; color: var(--muted); letter-spacing: 0.06em; text-transform: uppercase; }
.br-die-val { font-size: 16px; font-weight: 700; font-variant-numeric: tabular-nums; }
#br-attr { border-color: color-mix(in srgb, #38C6C6 45%, var(--hair)); }
#br-attr .br-die-val, #br-attr .die-ico { color: #38C6C6; }
#br-skill { border-color: color-mix(in srgb, #D9A441 45%, var(--hair)); }
#br-skill .br-die-val, #br-skill .die-ico { color: #D9A441; }
.br-mod .br-die-val { font-size: 12.5px; }
.br-mod[data-state="adv"] { border-color: #5BA860; }
.br-mod[data-state="adv"] .br-die-val { color: #5BA860; }
.br-mod[data-state="dis"] { border-color: var(--danger); }
.br-mod[data-state="dis"] .br-die-val { color: var(--danger); }
/* Twilight: 2000 reuses the selectors — olive Attribute, amber Skill, tan Ammo. */
#t2k-attr { border-color: color-mix(in srgb, #9AA36A 45%, var(--hair)); }
#t2k-attr .br-die-val, #t2k-attr .die-ico { color: #9AA36A; }
#t2k-skill { border-color: color-mix(in srgb, #D9A441 45%, var(--hair)); }
#t2k-skill .br-die-val, #t2k-skill .die-ico { color: #D9A441; }
#t2k-ammo { border-color: color-mix(in srgb, #C8B48A 45%, var(--hair)); }
#t2k-ammo .br-die-val, #t2k-ammo .die-ico { color: #C8B48A; }

/* A standalone die-add button — the Genesys rack's look, sized to sit inline
   rather than fill a row. Used where a pool is one die type (CthulhuTech d10,
   Fate dF, the Vampire pair, a One Ring Success die). */
.die-btn {
  --dc: var(--muted);
  position: relative;
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 10px 18px 7px;
  border: 1px solid var(--hair);
  border-radius: 12px;
  background: none;
  color: var(--dc);
  font: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transition: border-color 140ms ease, background 140ms ease, transform 120ms ease;
}
.die-btn .die-ico { width: 30px; height: 30px; }
.die-btn-label { font-size: 11px; color: var(--muted); white-space: nowrap; }
.die-btn[data-count] { border-color: var(--dc); background: color-mix(in srgb, var(--dc) 12%, transparent); }
.die-btn[data-count] .die-btn-label { color: var(--line); }
.die-btn[data-count] .die-ico path, .die-btn[data-count] .die-ico rect { fill-opacity: 0.34; }
.die-btn[data-count]::after {
  content: attr(data-count);
  position: absolute;
  top: -6px;
  right: -4px;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  border-radius: 8px;
  background: var(--dc);
  color: var(--paper);
  font: 700 10px system-ui;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  display: flex;
  align-items: center;
  justify-content: center;
}
.die-btn:active { transform: scale(0.94); }

/* Tap-to-cycle chip for a single number (Difficulty, Power, Target): tap to
   raise, hold to lower — the die buttons' gesture, for the non-dice controls. */
.spin-chip {
  display: inline-flex;
  align-items: baseline;
  gap: 8px;
  padding: 10px 16px;
  border: 1px solid var(--hair);
  border-radius: 999px;
  background: none;
  color: var(--muted);
  font: inherit;
  font-size: 12.5px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transition: border-color 140ms ease, transform 120ms ease;
}
.spin-chip-label { color: var(--muted); letter-spacing: 0.01em; }
/* One line-height for both states: the unset dash is smaller type, and without
   the shared line box the chip changed height when a value landed — which
   shifted the whole picker under a scrolled thumb. */
.spin-chip-val { color: var(--line); font-size: 16px; line-height: 20px; font-variant-numeric: tabular-nums; }
.spin-chip-val[data-unset="1"] { color: var(--muted); font-size: 12.5px; line-height: 20px; }
.spin-chip.is-set { border-color: color-mix(in srgb, var(--accent) 55%, var(--hair)); }
.spin-chip:active { transform: scale(0.96); }
/* Text chips wear a small mark in the family stroke — the V5 brain/drop
   precedent. The ± ligature marks the signed number you bring to the roll
   (DS Characteristic, the Crows Modifier): neither rolls a die of its own,
   so an abstract mark tells the truth where a die silhouette would lie,
   and a plus-over-minus still reads at 15px. Baseline-aligned chips need
   the mark pulled to centre or it hangs. */
.spin-mark { width: 13px; height: 13px; flex: none; align-self: center; color: var(--muted); }

/* CthulhuTech: one d10 to build the pool, Difficulty as a subordinate chip. */
.ct-picker { display: flex; gap: 14px; justify-content: center; align-items: center; }
.ct-picker .die-btn { --dc: #57B591; }

/* Forged in the Dark: one control, the d6 pool button. It wears the ghost-fire
   teal, and at a pool of zero — the desperate roll — it goes to the danger
   tone with a tinted well and its "Desperate" label, so the grim state reads
   before you throw. */
.fitd-picker { display: flex; gap: 14px; justify-content: center; align-items: center; }
.fitd-picker .die-btn { --dc: #3FC7BD; }
.fitd-pool[data-desperate="1"] {
  --dc: var(--danger);
  border-color: var(--danger);
  background: color-mix(in srgb, var(--danger) 12%, transparent);
}
.fitd-pool[data-desperate="1"] .die-btn-label { color: var(--danger); letter-spacing: 0.02em; }

/* PbtA / Mist: nothing to build (the 2d6 sit in the tray), so just the one chip. */
.twod6-picker { display: flex; gap: 14px; justify-content: center; align-items: center; }

/* Draw Steel: the 2d10 stage themselves, so one row — the characteristic
   spinner flanked by the Edge and Bane counters. The counters are conditions
   on the roll, not dice, so they stay in the spin-chip family and badge their
   count the way a loaded die button does. */
/* Edge/Bane wear a chevron that doubles when the mechanic changes kind — one
   chevron while the effect is ±2, both when a double shifts the tier — and an
   effect readout (+2 / Tier ↑) instead of a bare count. */
.ds-count-ico { width: 15px; height: 15px; flex: none; }
.ds-count .ds-chev-one { opacity: 1; }
.ds-ico-down { transform: none; }   /* the bane's chevrons point down in their own paths */
.ds-count .ds-chev-two { opacity: 0.18; }
.ds-count[data-count="2"] .ds-chev-two { opacity: 1; }
/* Centred under the chevrons; the column tile sizes itself to its widest
   line, so no width floor is needed any more. */
.ds-effect { text-align: center; }
.ds-picker { display: flex; gap: 10px; align-items: center; }
/* Slightly tighter than the stock spin-chip: the ± mark joined
   Characteristic, and the flank budget that centres the Power Roll pays
   for it here rather than by clipping the row's left edge. */
.ds-picker .spin-chip { padding: 10px 12px; gap: 6px; }
/* Edge and Bane in the Daggerheart adv/dis language (the owner's call):
   a column tile — generous stacked chevrons as the mark, one short line
   of text under them — instead of the crowded inline chip. The line is
   self-labelling like the torch button: the word at rest, the EFFECT
   (+2 / Tier ↑) once a count is set, when the tint and corner badge have
   taken over saying which chip this is. Chevrons wear their side's
   colour full-time, exactly as the DH buttons do. */
.ds-count {
  position: relative;
  flex-direction: column;
  align-items: center;
  gap: 3px;
  padding: 7px 13px 5px;
  border-radius: 12px;
}
.ds-count .ds-count-ico { width: 24px; height: 24px; color: var(--dc); }
.ds-count .spin-chip-label,
.ds-count .spin-chip-val { font-size: 10.5px; line-height: 1.2; }
.ds-count[data-count] .spin-chip-label { display: none; }
.ds-count:not([data-count]) .ds-effect { display: none; }
.ds-count[data-count] .ds-effect { color: var(--line); }

/* Row-overflow guard for the one-row system pickers — the Mothership rail
   precedent (DESIGN.md: wide content scrolls in its own container, never
   clips): no wrap, touch scroll, scrollbar hidden, and content-edge padding
   so an end control never sits flush on the screen edge. `safe center` is
   the load-bearing word: plain `center` is what CLIPPED the Draw Steel row —
   centred overflow hangs past the LEFT edge, where no scroll can reach it —
   while `safe` falls back to flex-start the moment the row outgrows the
   viewport. */
.ds-picker, .crows-picker, .sd-picker, .dccarl-picker {
  justify-content: safe center;
  flex-wrap: nowrap;
  max-width: 100%;
  min-width: 0;
  overflow-x: auto;
  padding-inline: 10px;
  scrollbar-width: none;
  -webkit-overflow-scrolling: touch;
}
.ds-picker::-webkit-scrollbar, .crows-picker::-webkit-scrollbar, .sd-picker::-webkit-scrollbar, .dccarl-picker::-webkit-scrollbar { display: none; }
/* A scrollport clips at its padding box, and the corner count badges hang
   6px above their buttons — inside a scrolling row they were getting their
   tops cut. Block padding buys the overhang room INSIDE the scrollport,
   and the matching negative margin hands the same room straight back to
   the layout, so the rows sit exactly where they did. Applied to every
   row that scrolls, Mothership's original and the deck row included. */
.ds-picker, .crows-picker, .sd-picker, .dccarl-picker, .picker.mothership-rail, .cards-picker {
  padding-block: 8px;
  margin-block: -8px;
}

/* Flanks around the Power Roll tile — the Duality precedent: the primary
   action in the middle, its modifiers either side. At width the flanks go
   equal-flex (min-width: 0 zeroes their hypothetical size, which is what
   makes the shares equal), so the tile sits dead centre; the breakpoints
   below are each picker's point where an equal share still covers the
   wider flank's content — under them the flanks fall back to content size
   and the whole cluster centres as a group, because an equal-flex flank
   narrower than its content doesn't scroll, it CLIPS. The lead side hugs
   the centre from the left, the trail side from the right. */
.pick-side { display: flex; align-items: center; gap: 10px; flex: 0 0 auto; }
.pick-side-lead { justify-content: flex-end; }
.pick-side-trail { justify-content: flex-start; }
@media (min-width: 431px) {
  .crows-picker .pick-side { flex: 1 1 0; min-width: 0; }
}
@media (min-width: 610px) {
  .ds-picker .pick-side { flex: 1 1 0; min-width: 0; }
}

/* Honest abbreviation under pressure: the widest labels shorten at phone
   widths; every abbreviated button's aria-label keeps the full word. */
.lbl-narrow { display: none; }
@media (max-width: 430px) {
  .lbl-wide { display: none; }
  .lbl-narrow { display: inline; }
  /* Cheap compressions before the scroll has to earn its keep: tighter
     gaps and chip padding. The Edge/Bane tiles keep their full anatomy at
     every width — chevrons, one text line, badge — because the column
     layout already made them narrow. */
  .ds-picker { gap: 6px; padding-inline: 6px; }
  .pick-side { gap: 6px; }
  .ds-picker .spin-chip { padding: 8px 8px; gap: 5px; }
  .ds-picker .ds-count { padding: 6px 9px 4px; gap: 3px; }
  .ds-picker .power-roll { padding: 8px 10px 6px; }
  .crows-picker { gap: 8px; }
  /* The ± mark widened the Modifier chip; the padding pays it back. */
  .crows-picker .spin-chip { padding: 8px 8px; gap: 5px; }
  .sd-picker, .dccarl-picker { gap: 6px; }
  .sd-picker .spin-chip, .dccarl-picker .spin-chip { padding: 5px 8px 4px; }
  .sd-adv, .dccarl-adv, .twod6-adv { padding: 5px 4px 4px; }
  .sd-torch { padding: 5px 8px 4px; }
}

/* The Power Roll tile, shared by Draw Steel and Crows: buttons that add dice
   are shaped like the dice, and this one selects a PAIR, so it wears both
   d10 kites — the Mothership percentile tile's two-dice grammar in the
   wireframe family's stroke. The rear die sits back a step in opacity, the
   way the far die of a thrown pair reads. In Crows it carries aria-pressed:
   lit while the power roll holds the tray, dark while a usage pool does —
   which of the two rolls Roll will throw was a real point of confusion. */
.die-btn .die-ico.power-ico { width: 40px; height: 30px; }
.power-die-rear { opacity: 0.55; }
/* The Power Roll is the primary action, so it wears the accent at rest —
   the Duality precedent, exactly. Draw Steel's tile (no aria-pressed) also
   carries the tinted well full-time; Crows' keeps the well for its pressed
   state, so the accent border says "this is THE action" while the fill
   still says which of the two rolls holds the tray. */
.power-roll { border-color: var(--accent); }
.power-roll .die-btn-label { color: var(--accent); }
.power-roll:not([aria-pressed]),
.power-roll[aria-pressed="true"] {
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.power-roll:not([aria-pressed]) .die-btn-label,
.power-roll[aria-pressed="true"] .die-btn-label { color: var(--line); }
.ds-edge { --dc: #57B591; }
.ds-bane { --dc: #C0453F; }
.ds-count[data-count] { border-color: var(--dc); background: color-mix(in srgb, var(--dc) 12%, transparent); }
.ds-count[data-count] .spin-chip-label { color: var(--line); }
.ds-count[data-count]::after {
  content: attr(data-count);
  position: absolute;
  top: -6px;
  right: -4px;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  border-radius: 8px;
  background: var(--dc);
  color: var(--paper);
  font: 700 10px system-ui;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  display: flex;
  align-items: center;
  justify-content: center;
}

/* Crows: the 2d10 stage themselves, so one row — the usage d6 (which builds an
   item's pool instead) and the single resolved-modifier chip. The pips on the
   usage die are stroke dots, one faded: a die already burning down. */
.crows-picker { display: flex; gap: 14px; align-items: center; }
.crows-picker .die-btn { --dc: #98A08A; }
.crows-pip { fill: none; stroke-width: 3; stroke-linecap: round; }
.crows-pip-fading { opacity: 0.35; }

/* ---- the scrub dial (prototype: Crows Modifier, Shadowdark Modifier/DC) ----

   A spin-chip that scrubs: wheel or press-drag moves the value, and during
   either gesture a ghost rail of neighbouring values materialises around the
   pill, fading with distance. touch-action: none is what lets the drag own
   the gesture — without it the browser scrolls the page out from under a
   scrub. */
/* Doubled on purpose — this is load-bearing, not a typo. Several host
   buttons declare touch-action: manipulation, and some of those rules sit
   LATER in this sheet: at equal single-class specificity the later rule
   wins, which is how the custom die (.dbtn.custom.scrub-dial) computed
   `manipulation`, let the browser claim every vertical drag for the dice
   row's horizontal scroll (pointercancel, scrub dead) on real phones —
   while synthetic pointer events, which never consult gesture
   arbitration, passed every test. Two classes beat one wherever the rule
   sits, so no future button styling can silently take the gesture back;
   the markup suite guards the doubling and rejects rival two-class
   touch-action rules. */
.scrub-dial.scrub-dial { touch-action: none; }

/* The huge-range chips' typing door: tapping the value floats a numeric
   field exactly over the chip (never inside it — an input inside a button
   is a focus fight). Mono and tabular because a number being typed is dice
   data; accent border because an open editor is a loaded state. */
.scrub-entry {
  position: fixed;
  z-index: 41;
  box-sizing: border-box;
  padding: 0 6px;
  border: 1px solid var(--accent);
  border-radius: 8px;
  background: var(--face);
  color: var(--line);
  font-family: "Iosevka Etoile", ui-monospace, "SF Mono", monospace;
  font-size: 15px;
  font-variant-numeric: tabular-nums;
  text-align: center;
  outline: none;
}

/* The rail is a fixed-position overlay anchored to the chip from JS — never
   in the picker's layout, so nothing shifts when it appears. The shell owns
   only anchoring and the fade; inside it the rows hang on a wireframe DRUM:
   JS poses each one from the cylinder math in scrub-math.js (translateY on
   R·sin θ, scaleY on cos θ), so the gesture visibly turns a rotary drum and
   the rows curve over its tangents as they enter and leave. The values are
   dice data, so they speak mono; each row wears a faint paper pill so the
   numbers stay legible over whatever the tray has behind them. */
.scrub-rail {
  position: fixed;
  z-index: 40;
  transform: translate(-50%, -50%);
  pointer-events: none;
  opacity: 0;
  transition: opacity 150ms ease;
}
.scrub-rail[data-show="1"] { opacity: 1; }
/* The drum's viewport: sized to the cylinder (2 × DRUM_R plus a row's
   height of breathing room) so the fixed shell centres it on the chip.
   Rows position absolutely inside. No tangent rules — the owner cut the
   hairline edges; the curving rows carry the cylinder alone. */
.scrub-rail-col {
  position: relative;
  width: 150px;
  height: 212px;
}
.scrub-rail-val {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translate(-50%, -50%);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-width: 44px;
  height: 24px;
  padding: 0 10px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--paper) 84%, transparent);
  font-family: "Iosevka Etoile", ui-monospace, "SF Mono", monospace;
  font-size: 15px;
  font-variant-numeric: tabular-nums;
  color: var(--line);
}
/* The book's rung names ride beside their numbers in the interface voice —
   a label, not dice data. */
.scrub-rail-tag {
  font-family: "Inter Tight", system-ui, -apple-system, sans-serif;
  font-size: 10px;
  letter-spacing: 0.01em;
  color: var(--muted);
}
/* Fading with distance: the drum's far rows dim as they curve away. */
.scrub-d1 { opacity: 0.6; }
.scrub-d2 { opacity: 0.35; }
.scrub-d3 { opacity: 0.15; }
@media (prefers-reduced-motion: reduce) {
  /* The drum still appears — its geometry is information: the
     foreshortened rows say what the gesture does — but nothing fades
     or turns: JS pins the rotation at zero, so steps snap in place. */
  .scrub-rail { transition: none; }
}

/* ---- Shadowdark ---- */

/* One row, five controls, so everything goes compact and columnar: the two
   keep-pair flags (Mothership's icon language on d20 hexagons — chevrons
   point where the kept NUMBER goes), the torch, and the Modifier/DC chips
   restacked vertically to match. */
.sd-picker { display: flex; gap: 8px; align-items: stretch; }
.sd-picker .spin-chip {
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0;
  padding: 5px 12px 4px;
  border-radius: 12px;
}
/* Dungeon Crawler Carl: the same two-chip check as Shadowdark, without the
   advantage flanks or the torch — Modifier and DC, centred. */
.dccarl-picker { display: flex; gap: 10px; align-items: stretch; justify-content: center; }
.dccarl-picker .spin-chip {
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0;
  padding: 5px 12px 4px;
  border-radius: 12px;
}
.sd-adv, .dccarl-adv, .twod6-adv {
  /* Flexible like Mothership's tiles, so the controls always share the row
     width instead of overflowing a narrow phone. Dungeon Crawler Carl's and
     the 2d6 engine's advantage pairs are the same control as Shadowdark's. */
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  padding: 5px 8px 4px;
  border: 1px solid var(--hair);
  border-radius: 12px;
  background: none;
  color: var(--muted);
  font: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transition: border-color 140ms ease, color 140ms ease, background 140ms ease, transform 120ms ease;
}
.sd-keep-dice { width: 34px; height: 34px; display: block; }
.sd-keep-dice .drop { fill: none; stroke: var(--muted); stroke-width: 1.8; stroke-linejoin: round; }
.sd-keep-dice .keep { fill: color-mix(in srgb, var(--accent) 18%, var(--paper)); stroke: var(--accent); stroke-width: 2.1; stroke-linejoin: round; }
.sd-keep-dice .chev { fill: none; stroke: var(--accent); stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.sd-adv-label { font-size: 10px; letter-spacing: 0.01em; white-space: nowrap; }
.sd-adv[aria-pressed="true"], .dccarl-adv[aria-pressed="true"], .twod6-adv[aria-pressed="true"] { color: var(--accent); border-color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, transparent); }
.sd-adv[aria-pressed="true"] .sd-adv-label, .dccarl-adv[aria-pressed="true"] .sd-adv-label, .twod6-adv[aria-pressed="true"] .sd-adv-label { color: var(--line); }
.sd-adv:active, .dccarl-adv:active, .twod6-adv:active { transform: scale(0.96); }
/* Mist has no advantage, so its skin hides the pair. */
.twod6-picker.hide-adv .twod6-adv { display: none; }

/* The torch. Unlit it sits quiet in outline; lit it burns — accent border,
   a warm tinted well, the flame filled — and its label is the time it has
   left, in the tabular figures all dice data wear. Under ten minutes it
   gutters: dimmer, and flickering by a slow irregular pulse. At zero the
   flame is out and the label says what the crawl now knows. */
.sd-torch { padding: 5px 12px 4px; }
.sd-torch .die-ico path { fill: none; }
.sd-torch .die-btn-label { font-variant-numeric: tabular-nums; }
.sd-torch[data-torch="lit"], .sd-torch[data-torch="guttering"] {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  color: var(--accent);
}
.sd-torch[data-torch="lit"] .sd-flame, .sd-torch[data-torch="guttering"] .sd-flame {
  fill: color-mix(in srgb, var(--accent) 30%, transparent);
}
.sd-torch[data-torch="lit"] .die-btn-label, .sd-torch[data-torch="guttering"] .die-btn-label { color: var(--line); }
.sd-torch[data-torch="guttering"] { animation: sd-gutter 2.8s ease-in-out infinite; }
@keyframes sd-gutter {
  0%, 100% { opacity: 1; }
  34% { opacity: 0.55; }
  52% { opacity: 0.85; }
  71% { opacity: 0.5; }
  84% { opacity: 0.75; }
}
/* The flicker is testimony (a dying light), but a vestibular-safe machine
   gets the same fact as a steady dimming. */
@media (prefers-reduced-motion: reduce) {
  .sd-torch[data-torch="guttering"] { animation: none; opacity: 0.7; }
}
.sd-torch[data-torch="dead"] { color: var(--muted); }
.sd-torch[data-torch="dead"] .die-btn-label { color: var(--danger); }

/* ---- Daggerheart ---- */

.dh-picker {
  flex-direction: column;
  align-items: stretch;
  gap: 8px;
}
/* Three column buttons: the Advantage and Disadvantage d6 adders flank the
   Duality roll — a split gold/violet d12, accent-filled so it reads as the
   action rather than another adder. */
.dh-adv-row { display: flex; align-items: stretch; gap: 8px; }
.dh-adv-btn, .dh-roll {
  flex: 1 1 0;
  min-width: 0;
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 9px 4px 6px;
  border: 1px solid var(--hair);
  border-radius: 12px;
  background: none;
  font: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transition: border-color 140ms ease, background 140ms ease, transform 120ms ease;
}
.dh-adv-btn { --dc: var(--muted); color: var(--dc); }
.dh-adv-btn[data-adv="adv"] { --dc: #4E9E60; }
.dh-adv-btn[data-adv="dis"] { --dc: #C24046; }
.dh-adv-label, .dh-roll-label { font-size: 10.5px; color: var(--muted); white-space: nowrap; }
/* The adder die with its direction chevrons — up adds a d6, down subtracts. */
.dh-adv-ico { width: 30px; height: 30px; display: block; }
.dh-adv-ico .die { fill: currentColor; fill-opacity: 0.16; stroke: currentColor; stroke-width: 2; stroke-linejoin: round; }
.dh-adv-ico .chev { fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; }
.dh-adv-btn[data-count] { border-color: var(--dc); background: color-mix(in srgb, var(--dc) 12%, transparent); }
.dh-adv-btn[data-count] .dh-adv-label { color: var(--line); }
.dh-adv-btn[data-count] .dh-adv-ico .die { fill-opacity: 0.34; }
.dh-adv-btn[data-count]::after {
  content: attr(data-count);
  position: absolute;
  top: -6px;
  right: -4px;
  min-width: 16px;
  height: 16px;
  padding: 0 4px;
  border-radius: 8px;
  background: var(--dc);
  color: var(--paper);
  font: 700 10px system-ui;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  display: flex;
  align-items: center;
  justify-content: center;
}
/* The Duality roll is the primary action, so it wears the accent. */
.dh-roll { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, transparent); }
.dh-roll .dh-roll-label { color: var(--accent); }
.dh-adv-btn:active, .dh-roll:active { transform: scale(0.95); }

/* The modifier and difficulty chips, centred beneath the dice. */
.dh-fields-row { display: flex; justify-content: center; gap: 12px; }

/* Daggerheart's strip carries only the standard dice — hide the DCC oddities,
   keep the custom-die picker and any dice the player made themselves. */
.dice-buttons.standard-only .dbtn:not([data-std]):not(.custom):not([data-custom]) { display: none; }
/* The owned PSG/WOM use only d5, d10, d20 and d100. Keep d? and a custom die
   created through it, but hide Dicebox's broader DCC/novelty catalog here. */
.dice-buttons.mothership-only .dbtn:not(.custom):not([data-sides="5"]):not([data-sides="10"]):not([data-sides="20"]):not([data-sides="100"]):not([data-custom]) { display: none; }
/* Shadowdark's damage runs on the classic polyhedrals, d4 through d12, with
   the d20 for checks; no d100 in play, so the strip trims to the game's own
   dice. d? and player-made dice stay for table-specific rolls. */
.dice-buttons.shadowdark-only .dbtn:not(.custom):not([data-sides="4"]):not([data-sides="6"]):not([data-sides="8"]):not([data-sides="10"]):not([data-sides="12"]):not([data-sides="20"]):not([data-custom]) { display: none; }
/* DCC: the dice chain as a horizontal scroll-snap selector. Each die is its own
   2D shape in its own colour; the token centred in the row is the action die —
   enlarged and full colour — and what Roll throws. The side padding lets the
   first and last die reach the centre. */
.dcc-picker { display: block; padding: 0; }
.dcc-chain {
  display: flex; align-items: flex-start; gap: 5px;
  /* Extra bottom room so the enlarged action chip (which grows downward) stays
     inside the box — no vertical scrollbar, and clear of the notation field. */
  overflow-x: auto; scrollbar-width: none; padding: 6px 8px 18px;
  -webkit-overflow-scrolling: touch;
}
.dcc-chain::-webkit-scrollbar { display: none; }
.dcc-token {
  flex: 0 0 auto; position: relative;
  display: flex; flex-direction: column; align-items: center;
}
/* A crown above each die — dim and hollow until you crown that die; then it is
   solid gold and the die below it glows as the action die (the king). The space
   above every die is a tap target that crowns it, but only the crowned die shows
   a crown — no ghost outlines cluttering the rest. Desktop gets a faint hover
   hint so the affordance is discoverable. On the crowned die the crown is flanked
   by ‹ › arrows that step it one die along the chain. */
.dcc-crown-row { display: flex; align-items: center; justify-content: center; height: 16px; }
.dcc-crown { appearance: none; border: 0; background: transparent; cursor: pointer; width: 30px; height: 16px; padding: 0; display: grid; place-items: center; }
.dcc-crown svg { width: 20px; height: 13px; fill: none; stroke: var(--muted); stroke-width: 1.5; stroke-linejoin: round; opacity: 0; transition: opacity .15s; }
.dcc-crown:hover svg { opacity: 0.35; }
.dcc-token.is-action .dcc-crown svg { fill: #E6C63C; stroke: #E6C63C; opacity: 1; }
/* The step-arrows collapse to nothing on every die but the crowned one, so the
   rest of the chain keeps its tight spacing. */
.dcc-arrow {
  appearance: none; border: 0; background: transparent; cursor: pointer;
  width: 0; height: 16px; padding: 0; overflow: hidden; display: grid; place-items: center;
  opacity: 0; pointer-events: none; transition: opacity .15s;
}
.dcc-arrow svg { width: 11px; height: 14px; fill: none; stroke: #E6C63C; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.dcc-arrow-l { padding-right: 1px; }
.dcc-arrow-r { padding-left: 1px; }
.dcc-token.is-action .dcc-arrow { width: 16px; opacity: 0.9; pointer-events: auto; }
.dcc-token.is-action .dcc-arrow:hover { opacity: 1; }
.dcc-token.is-action .dcc-arrow[disabled] { opacity: 0.2; cursor: default; }

/* Each die is a Genesys-style chip: a filled silhouette under a solid outline,
   in a bordered button. --dc is the chip's active colour — neutral by default,
   and the chain hue only once the die is crowned. */
.dcc-chip {
  --dc: var(--muted);
  position: relative;
  display: flex; flex-direction: column; align-items: center; gap: 3px;
  width: 48px; padding: 7px 4px 5px;
  border: 1px solid var(--hair); border-radius: 12px;
  background: none; color: var(--dc); cursor: pointer;
  transition: border-color 140ms ease, background 140ms ease, transform 120ms ease;
}
.dcc-chip .die-ico { width: 30px; height: 30px; }
.dcc-chip-label { font-size: 10.5px; font-weight: 700; color: var(--muted); font-variant-numeric: tabular-nums; }
.dcc-chip:active { transform: scale(0.94); }
/* A die that is neither crowned nor pooled recedes, so the action die reads as
   the brightest thing on the chain even when its own hue is a pale one. */
.dcc-token:not(.is-action):not(.in-pool) .dcc-chip { opacity: 0.6; }
/* Loaded into the pool: the chip lights up in its (neutral) colour, its
   silhouette fills, and a count badge rides the corner. */
.dcc-token.in-pool .dcc-chip { border-color: var(--dc); background: color-mix(in srgb, var(--dc) 12%, transparent); }
.dcc-token.in-pool .die-ico path { fill-opacity: 0.34; }
.dcc-token.in-pool .dcc-chip-label { color: var(--line); }
.dcc-chip[data-count]::after {
  content: "×" attr(data-count);
  position: absolute; top: -6px; right: -5px;
  min-width: 16px; height: 16px; padding: 0 4px; border-radius: 8px;
  background: var(--dc); color: var(--paper);
  font: 800 10px system-ui; line-height: 16px; text-align: center;
  font-variant-numeric: tabular-nums;
}
/* The action die is the only coloured chip — its chain hue, grown ~30% and
   lifted above its neighbours. Colour alone says "this is your action die". */
.dcc-token.is-action { z-index: 2; }
.dcc-token.is-action .dcc-chip {
  --dc: var(--chain);
  border: 1.5px solid var(--dc);
  background: color-mix(in srgb, var(--dc) 20%, transparent);
  transform: scale(1.3); transform-origin: center top;
}
.dcc-token.is-action .die-ico path { fill-opacity: 0.52; stroke-width: 2.4; }
.dcc-token.is-action .dcc-chip-label { color: var(--dc); }
/* The neutral-coloured dice — d4, d20, d100 — have no hue to spend, so when
   crowned they take the theme ink instead: brightest thing on the chain in dark
   mode, boldest in light, always clear of the muted greys around them. */
.dcc-token.is-action[data-sides="4"] .dcc-chip,
.dcc-token.is-action[data-sides="20"] .dcc-chip,
.dcc-token.is-action[data-sides="100"] .dcc-chip { --dc: var(--line); }
@media (prefers-reduced-motion: reduce) {
  .dcc-chip { transition: none; }
}

/* The total tinted by the duality tone. */
.total[data-variant="hope"] { color: #C9A227; }
.total[data-variant="fear"] { color: #8577C0; }
.total[data-variant="critical"] { color: #E7C24C; }

/* CthulhuTech: the hit count glows green when it meets the Difficulty, greys
   out when it falls short. */
/* Roll-under systems (Call of Cthulhu, Delta Green): a crit reads gold, a
   success green, a plain failure muted, a fumble red. */
.total[data-variant="roll-crit-success"] { color: #E7C24C; }
.total[data-variant="roll-success"] { color: #57B591; }
.total[data-variant="roll-failure"] { color: var(--muted); }
.total[data-variant="roll-crit-failure"] { color: #C0453F; }
.total[data-variant="ct-success"] { color: #57B591; }
.total[data-variant="ct-failure"] { color: var(--muted); }
.total[data-variant="yz-success"] { color: #5BA860; }
.total[data-variant="yz-fail"] { color: var(--muted); }
.total[data-variant="yz-panic"] { color: var(--accent); }
.total[data-variant="br-crit"] { color: #D9A441; }
.total[data-variant="br-success"] { color: #5BA860; }
.total[data-variant="br-fail"] { color: var(--muted); }
.total[data-variant="t2k-success"] { color: #9AA36A; }
.total[data-variant="t2k-fail"] { color: var(--muted); }
.total[data-variant="v5-hunger"] { color: var(--accent); }

/* CthulhuTech's difficulty can be unset — just count the hits. */
.ct-picker .count[data-unset="1"] { color: var(--muted); }

/* The One Ring: gold for a Gandalf auto-success, green for a success, muted for
   a failure. */
.total[data-variant="tor-gandalf"] { color: #E8C24E; }
.total[data-variant="tor-success"] { color: #8FB77A; }
.total[data-variant="tor-failure"] { color: var(--muted); }

/* PbtA / Mist Engine share the Daggerheart picker chrome; their lone modifier
   stepper just sits centred. */
.twod6-picker { justify-content: center; }

/* PbtA / Mist Engine 2d6 outcome: a hit reads green, a partial amber, a miss
   muted — the same bands the dice take. */
.total[data-variant="band-hit"] { color: #57B591; }
.total[data-variant="band-partial"] { color: #C99A3C; }
.total[data-variant="band-miss"] { color: var(--muted); }

/* Draw Steel tiers: the weakest result in a muted ember-red, the middle in the
   cautionary amber the 2d6 bands use, tier 3 in their green, and the natural
   19-20 in the critical gold — one vocabulary of outcome colour across modes. */
.total[data-variant="ds-tier1"] { color: #C4705C; }
.total[data-variant="ds-tier2"] { color: #C99A3C; }
.total[data-variant="ds-tier3"] { color: #57B591; }
.total[data-variant="ds-crit"] { color: #E7C24C; }

/* Crows reuses the shared outcome vocabulary — failure ember, partial amber,
   success green, Critical the crit gold — and gives Doom the danger red it is.
   A usage roll's survivor count reads in the plain accent (bone/moonlight);
   only the emptied pool goes danger red. */
.total[data-variant="crows-tier1"] { color: #C4705C; }
.total[data-variant="crows-tier2"] { color: #C99A3C; }
.total[data-variant="crows-tier3"] { color: #57B591; }
.total[data-variant="crows-crit"] { color: #E7C24C; }
.total[data-variant="crows-doom"] { color: var(--danger); }
.total[data-variant="crows-out"] { color: var(--danger); }

/* Shadowdark reads in the shared outcome vocabulary — success green, failure
   ember — with the natural 20 in crit gold and the natural 1 in danger red,
   the two results the whole table turns for. */
.total[data-variant="sd-success"] { color: #57B591; }
.total[data-variant="sd-failure"] { color: #C4705C; }
.total[data-variant="sd-crit"] { color: #E7C24C; }
.total[data-variant="sd-fumble"] { color: var(--danger); }
.total[data-variant="dccarl-success"] { color: #57B591; }
.total[data-variant="dccarl-failure"] { color: #C4705C; }
.total[data-variant="dccarl-crit"] { color: #E7C24C; }
.total[data-variant="dccarl-fumble"] { color: var(--danger); }
.total[data-variant="dccarl-near"] { color: #C99A3C; }
.total[data-variant="dccarl-major"] { color: var(--danger); }
/* Forged in the Dark: the ghost-fire ladder — a critical in bright ghostlight
   cyan, a full success in the electric Duskwall teal, a partial in the shared
   cautionary amber, a failure in the muted tone. One outcome vocabulary. */
.total[data-variant="fitd-crit"] { color: #6FE0D5; }
.total[data-variant="fitd-success"] { color: #3FB8AE; }
.total[data-variant="fitd-partial"] { color: #C99A3C; }
.total[data-variant="fitd-failure"] { color: var(--muted); }
/* Oracle readings are prose, not a headline word — smaller, serif, wrapping. */
.total[data-variant="oracle"] {
  color: var(--line);
  font-family: Georgia, "Iowan Old Style", serif;
  font-size: clamp(18px, 5vw, 26px);
  font-weight: 600;
  line-height: 1.2;
  text-wrap: balance;
}

/* ---- The One Ring picker ---- */
/* One Ring: a Success-d6 button + Target chip, then the condition toggles. */
.tor-picker { flex-direction: column; align-items: center; gap: 10px; }
.tor-controls-row { display: flex; gap: 12px; align-items: center; }
/* TOR_COLORS.success — the button adds Success dice, so it wears their
   parchment, not the Feat die's bronze. */
.tor-success-btn { --dc: #CBBF9F; }
.tor-flags-row { display: flex; gap: 8px; width: 100%; }
/* Each condition is an icon toggle: favoured/ill show the two Feat dice with the
   kept one on top (high for favoured, low for ill); Weary a struck Success die. */
.tor-flag {
  flex: 1 1 0;
  min-width: 0;
  appearance: none;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  padding: 8px 4px 7px;
  border: 1px solid var(--hair);
  border-radius: 12px;
  background: none;
  color: var(--muted);
  font: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transition: border-color 140ms ease, color 140ms ease, background 140ms ease, transform 120ms ease;
}
.tor-flag-ico { width: 30px; height: 30px; display: block; }
.tor-flag-label { font-size: 10.5px; letter-spacing: 0.01em; white-space: nowrap; }
/* Two Feat dice: the dropped one outlined grey, the kept one bronze and opaque so
   it sits clearly on top; chevrons point to which was kept. */
.tor-flag-ico .drop { fill: none; stroke: #6f6857; stroke-width: 2; stroke-linejoin: round; }
.tor-flag-ico .keep { fill: color-mix(in srgb, #B5893C 18%, var(--paper)); stroke: #B5893C; stroke-width: 2.3; stroke-linejoin: round; }
.tor-flag-ico .chev { fill: none; stroke: #B5893C; stroke-width: 2.1; stroke-linecap: round; stroke-linejoin: round; }
/* Weary: a Success die with its low faces struck through. */
.tor-flag-ico .weary-die { fill: none; stroke: var(--muted); stroke-width: 2.2; }
.tor-flag-ico .weary-pip { fill: var(--muted); }
.tor-flag-ico .weary-strike { fill: none; stroke: var(--danger); stroke-width: 2.4; stroke-linecap: round; }
.tor-flag[aria-pressed="true"] {
  color: var(--accent);
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.tor-flag[aria-pressed="true"] .tor-flag-label { color: var(--line); }
.tor-flag:active { transform: scale(0.96); }

/* ---- Mothership ---- */
/* Signature dice are direct roll actions; modifiers flank them in one tactile row. */
.ms-picker { flex-direction: column; align-items: stretch; gap: 6px; }
.ms-fields-row, .ms-action-row { display: flex; align-items: stretch; gap: 6px; }
/* The second and final visual row is a single horizontal rail: character setup,
   dice-per-tap, then the five rules-grounded dice. It scrolls instead of wrapping. */
.picker.mothership-rail {
  width: 100%;
  max-width: 100%;
  min-width: 0;
  overflow-x: auto;
  flex-wrap: nowrap;
  scrollbar-width: thin;
  scrollbar-color: color-mix(in srgb, var(--accent) 65%, transparent) transparent;
  -webkit-overflow-scrolling: touch;
}
.picker.mothership-rail::-webkit-scrollbar { height: 4px; }
.picker.mothership-rail::-webkit-scrollbar-thumb { background: color-mix(in srgb, var(--accent) 65%, transparent); border-radius: 999px; }
.mothership-rail .ms-fields-row, .mothership-rail .count-chip, .mothership-rail .dice-scroll { flex: 0 0 auto; }
.mothership-rail .dice-scroll { min-width: max-content; }
/* No flex `order` here any more — Emory's row ruling: the strip reads
   count chip, then d?, then the dice, in EVERY mode, and that is exactly
   the markup's source order. The old rules pushed the count chip past the
   dice on this one rail, which is where the inconsistency was spotted. */
.mothership-rail .dice-buttons { overflow: visible; }
.mothership-rail .nudge { display: none; }
.mothership-rail .count-chip, .mothership-rail .dbtn { min-height: 44px; }
.mothership-rail .ms-direct-field {
  width: 72px;
  box-sizing: border-box;
  flex-direction: column;
  justify-content: center;
  gap: 1px;
  padding: 0 4px;
}
.mothership-rail .ms-dial-field { position: relative; padding: 0; overflow: hidden; }
.mothership-rail .ms-dial-field > span {
  position: absolute;
  z-index: 1;
  top: 5px;
  left: 0;
  width: 100%;
  text-align: center;
  pointer-events: none;
}
.mothership-rail .ms-direct-field span { font-size: 9px; line-height: 1; }
.mothership-rail .ms-skill-field { width: 82px; position: relative; padding: 0; overflow: hidden; }
.mothership-rail .ms-skill-field > span {
  position: absolute;
  z-index: 1;
  top: 5px;
  left: 0;
  width: 100%;
  text-align: center;
  pointer-events: none;
}
.mothership-rail .count-chip { padding: 0 8px; }
.mothership-rail .dice-buttons { gap: 2px; }
.mothership-rail .dbtn { min-width: 38px; padding-inline: 3px; }
/* Square SVG viewports prevent the percentile pair and d20 from inheriting the
   old wide-card distortion that made one look thin and the other look fat. */
.ms-role-roll {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  padding: 4px;
  border: 1px solid var(--hair);
  border-radius: 12px;
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  color: var(--accent);
  font: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transition: border-color 140ms ease, color 140ms ease, background 140ms ease, transform 120ms ease;
}
.ms-role-ico { width: 34px; height: 34px; flex: 0 0 auto; display: block; }
.ms-percentile-ico { width: 42px; }
.ms-panic-ico { overflow: visible; }
.ms-role-ico polygon { fill: color-mix(in srgb, currentColor 10%, transparent); stroke: currentColor; stroke-width: 1.3; stroke-linejoin: round; vector-effect: non-scaling-stroke; }
.ms-percentile-ico .rear { opacity: 0.58; }
.ms-role-label { color: var(--line); font-size: 10.5px; font-weight: 650; white-space: nowrap; }
.ms-role-die { color: var(--muted); font-size: 9px; letter-spacing: 0.08em; text-transform: uppercase; }
.ms-role-roll:active { transform: scale(0.97); }
/* Target and Stress launch the tactile number wheel; Skill remains a direct
   select. The small ↕ says “flickable dial”, not one-point spinner arrows. */
.ms-fields-row { justify-content: center; align-items: center; }
.ms-direct-field {
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: 0 10px;
  border: 1px solid var(--hair);
  border-radius: 999px;
  color: var(--muted);
  font-size: 11px;
  white-space: nowrap;
}
.ms-direct-field input, .ms-direct-field select {
  min-width: 0;
  border: 0;
  outline: 0;
  background: none;
  color: var(--line);
  font: 600 15px/1 system-ui, sans-serif;
  font-variant-numeric: tabular-nums;
}
.ms-direct-field input { width: 42px; text-align: center; }
.ms-direct-field select { width: 94px; cursor: pointer; }
.ms-value-dial {
  width: 100%;
  min-width: 42px;
  min-height: 44px;
  padding: 10px 4px 0;
  border: 0;
  border-radius: 999px;
  background: none;
  color: var(--line);
  font: 600 15px/1 system-ui, sans-serif;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
}
.ms-value-dial::after { content: "↕"; margin-left: 4px; color: var(--accent); font-size: 10px; }
.ms-value-dial:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
/* Skill is a four-tier cycle (None → Trained → Expert → Master), not a number
   wheel, so it is a plain tap-to-raise / hold-to-lower button with no ↕ marker. */
.ms-skill-dial {
  width: 100%;
  min-height: 44px;
  padding: 12px 4px 0;
  border: 0;
  border-radius: 999px;
  background: none;
  color: var(--line);
  font: 600 12px/1 system-ui, sans-serif;
  cursor: pointer;
}
.ms-skill-dial:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.ms-direct-field:focus-within { border-color: var(--accent); }
/* Advantage and Disadvantage borrow One Ring's visual explanation: the outlined
   die is discarded and the accent die is kept. This remains accurate because
   Mothership compares resolved outcomes, not merely high or low numbers. */
.ms-adv, .ms-role-roll {
  flex: 1 1 0;
  min-width: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  padding: 5px 4px 4px;
  border: 1px solid var(--hair);
  border-radius: 12px;
  background: none;
  color: var(--muted);
  font: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transition: border-color 140ms ease, color 140ms ease, background 140ms ease, transform 120ms ease;
}
/* Only the active roll type carries the accent, so one tile reads as primary at a
   time — matching the staged dice in the tray. The inactive tile stays neutral
   like the Advantage/Disadvantage buttons beside it. Cascade order is deliberate:
   this sits after the shared modifier/action geometry above. */
.ms-role-roll[aria-pressed="true"] {
  border-color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  color: var(--accent);
}
.ms-keep-dice { width: 34px; height: 34px; display: block; }
.ms-keep-dice .drop { fill: none; stroke: var(--muted); stroke-width: 1.8; stroke-linejoin: round; }
.ms-keep-dice .keep { fill: color-mix(in srgb, var(--accent) 18%, var(--paper)); stroke: var(--accent); stroke-width: 2.1; stroke-linejoin: round; }
.ms-keep-dice .chev { fill: none; stroke: var(--accent); stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.ms-adv-label { font-size: 10px; letter-spacing: 0.01em; white-space: nowrap; }
.ms-adv[aria-pressed="true"] { color: var(--accent); border-color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, transparent); }
.ms-adv[aria-pressed="true"] .ms-adv-label { color: var(--line); }
.ms-adv:active, .ms-role-roll:active { transform: scale(0.96); }

/* ---- mode sheet ---- */

/* The mode sheet is a touch narrower than the modifier sheet: two cards want a
   compact shelf, not the full width. */
/* ---- Cards: the deck rail ---- */
/* Compact: count chip, Shuffle, the two icon toggles, and the live count. The
   draw itself is the entry button (relabelled Draw) and the deck on the tray. */
.cards-picker { display: flex; gap: 8px; justify-content: center; align-items: stretch; flex-wrap: nowrap; overflow-x: auto; scrollbar-width: thin; }
.deck-shuffle {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-width: 64px;
  padding: 5px 10px 4px;
  border: 1px solid var(--hair);
  border-radius: 12px;
  background: none;
  color: var(--muted);
  font: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transition: border-color 140ms ease, background 140ms ease, transform 120ms ease;
}
.deck-shuffle svg { width: 26px; height: 26px; }
.deck-shuffle svg path { fill: none; stroke: currentColor; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
.deck-shuffle-label { font-size: 10px; color: var(--muted); white-space: nowrap; }
.deck-shuffle:active { transform: scale(0.94); }
/* The toggles wear their meaning: a card with a joker star, a card with a
   return arrow — pressed takes the accent, like every flag in the app. */
.deck-flag {
  display: inline-flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  min-width: 58px;
  padding: 4px 8px 4px;
  border: 1px solid var(--hair);
  border-radius: 12px;
  background: none;
  color: var(--muted);
  font: inherit;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
  touch-action: manipulation;
  transition: border-color 140ms ease, color 140ms ease, background 140ms ease, transform 120ms ease;
}
.deck-flag-ico { width: 26px; height: 26px; display: block; }
.deck-flag-ico .crd { fill: none; stroke: currentColor; stroke-width: 1.7; }
.deck-flag-ico .mark { fill: currentColor; }
.deck-flag-ico .mark2 { fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
.deck-flag-name { font-size: 10px; letter-spacing: 0.01em; white-space: nowrap; }
.deck-flag[aria-pressed="true"] { color: var(--accent); border-color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, transparent); }
.deck-flag[aria-pressed="true"] .deck-flag-name { color: var(--line); }
.deck-flag:active { transform: scale(0.94); }
.deck-remain {
  align-self: center;
  font: 600 12px ui-monospace, monospace;
  color: var(--muted);
  padding: 0 4px;
  white-space: nowrap;
  font-variant-numeric: tabular-nums;
}
/* A single red card's label in the big readout keeps its suit's colour. */
#total[data-variant="card-red"] { color: #C74B3F; }

/* ---- discard browser ---- */
/* The pile fanned open: same frost as the close-up, one layer beneath it, so
   a card tapped here rises above the spread it came from. */
#discardPanel {
  position: fixed;
  z-index: 29;
  left: 0; right: 0; bottom: 0;
  /* top is set from the real bar height when it opens */
  overflow-y: auto;
  background: color-mix(in srgb, var(--paper) 82%, transparent);
  backdrop-filter: blur(7px);
  -webkit-backdrop-filter: blur(7px);
  cursor: zoom-out;
  padding: 10px 14px 20px;
}
.discard-head {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 4px 4px 10px;
}
.discard-title { font: 650 14px/1.2 inherit; letter-spacing: 0.02em; color: var(--line); }
.discard-hint { font-size: 11px; color: var(--muted); }
.discard-grid {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  justify-content: center;
}
.discard-card {
  appearance: none;
  border: 0;
  padding: 0;
  background: none;
  width: clamp(84px, 17vw, 132px);
  cursor: zoom-in;
  -webkit-tap-highlight-color: transparent;
  transition: transform 120ms ease;
}
.discard-card:active { transform: scale(0.96); }
.discard-card svg,
.discard-card img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 8px;
  box-shadow: 0 6px 18px rgba(0, 0, 0, 0.3);
}
/* Explicit origin: SVG elements can default transform-origin to their top-left
   corner, which spins the card clean out of its cell and leaves it blank. */
.discard-card.rev svg { transform: rotate(180deg); transform-origin: 50% 50%; transform-box: fill-box; }

/* ---- card close-up ---- */
/* Everything below the top bar frosts over and the held card rises into it.
   The bar stays clear (and above, z-wise), so the corner menus keep working
   while a card is up. */
#cardFocus {
  position: fixed;
  z-index: 30;
  left: 0; right: 0; bottom: 0;
  /* top is set from the real bar height when it opens */
  display: flex;
  align-items: center;
  justify-content: center;
  background: color-mix(in srgb, var(--paper) 82%, transparent);
  backdrop-filter: blur(7px);
  -webkit-backdrop-filter: blur(7px);
  cursor: zoom-out;
}
.card-focus-art {
  transition: transform 280ms cubic-bezier(0.2, 0.8, 0.25, 1), opacity 220ms ease;
  /* No will-change: pinning this as a permanent GPU layer makes the browser
     rasterise the vector card once at a low resolution and scale the texture,
     which fills a high-DPI screen with a blurry blow-up. The transform
     transition still composites on its own for the 280ms it runs; letting the
     layer retire afterwards re-rasterises the SVG crisp at device resolution. */
}
.card-focus-art svg,
.card-focus-art img {
  display: block;
  width: 100%;
  height: 100%;
  border-radius: 12px;
  box-shadow: 0 20px 52px rgba(0, 0, 0, 0.45);
}
/* The two-faced close-up: decks with a reading side (uta-garuta) flip over.
   The front is the traced art, the back a typeset reading of the poem. */
/* Perspective belongs ONLY on the flipping element, never the base close-up.
   On the base class it wrapped every card — cards, tarot, napoletane — in a 3D
   context, and Firefox rasterises a 3D-context layer at a size cap, so a card
   filling the screen came out a blurry blow-up. Applying perspective() inside
   the flip's own transform keeps the uta-garuta turn in 3D while leaving every
   other close-up a plain, crisp vector. */
.cf-flip {
  position: relative;
  width: 100%;
  height: 100%;
  transform-style: preserve-3d;
  transition: transform 460ms cubic-bezier(0.3, 0.7, 0.3, 1);
  transform: perspective(1400px) rotateY(0deg);
}
.cf-flip.flipped { transform: perspective(1400px) rotateY(180deg); }
.cf-face {
  position: absolute;
  inset: 0;
  backface-visibility: hidden;
  -webkit-backface-visibility: hidden;
}
.cf-back { transform: rotateY(180deg); }
.uta-read {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  background: var(--face);
  border: 1.5px solid var(--hair);
  border-radius: 12px;
  box-shadow: 0 20px 52px rgba(0, 0, 0, 0.45);
  color: var(--line);
  padding: 0.9em 0.9em 0.8em;
  overflow: hidden;
}
.uta-read::before {
  content: '';
  position: absolute;
  inset: 0.55em;
  border: 1px solid var(--accent);
  opacity: 0.5;
  border-radius: 8px;
  pointer-events: none;
}
.uta-read-head { text-align: center; line-height: 1.35; margin-top: 0.35em; }
.uta-read-n {
  font-family: Iosevka Etoile, Georgia, serif;
  color: var(--accent);
  font-size: 0.72em;
  letter-spacing: 0.08em;
  display: block;
}
.uta-read-ja { font-size: 1.05em; letter-spacing: 0.12em; display: block; }
.uta-read-cat { font-size: 0.62em; color: var(--muted); letter-spacing: 0.2em; display: block; }
.uta-read-en {
  font-family: Iosevka Etoile, Georgia, serif;
  font-size: 0.62em;
  color: var(--muted);
  letter-spacing: 0.04em;
  display: block;
}
/* The poem proper: five ku, five vertical columns, right to left. Each ku is
   its own column (row-reverse puts the first on the right), so a column can
   never break in the middle of a verse. */
.uta-read-poem {
  flex: 1;
  display: flex;
  flex-direction: row-reverse;
  align-items: center;
  justify-content: center;
  gap: 0.55em;
  margin: 0.4em 0;
  font-family: "Hiragino Mincho ProN", "Yu Mincho", "Noto Serif JP", "Noto Serif CJK JP", serif;
  font-size: 0.92em;
  line-height: 1.7;
  letter-spacing: 0.1em;
}
.uta-read-poem > div { writing-mode: vertical-rl; }
.uta-read-solo {
  writing-mode: horizontal-tb;
  font-family: Iosevka Etoile, Georgia, serif;
  font-style: italic;
  font-size: 0.8em;
  line-height: 1.6;
  text-align: center;
  letter-spacing: 0.01em;
  padding: 0 0.6em;
}
.uta-read-kana {
  text-align: center;
  color: var(--muted);
  font-size: 0.56em;
  line-height: 1.5;
  margin-bottom: 0.5em;
}
.uta-read-trans {
  text-align: center;
  font-family: Iosevka Etoile, Georgia, serif;
  font-style: italic;
  font-size: 0.66em;
  line-height: 1.5;
  opacity: 0.92;
  margin-bottom: 0.6em;
}
.card-focus-hint {
  position: absolute;
  left: 0;
  right: 0;
  bottom: max(14px, env(safe-area-inset-bottom));
  margin: 0;
  text-align: center;
  color: var(--muted);
  font-size: 12.5px;
  letter-spacing: 0.06em;
  pointer-events: none;
}
.uta-lang { min-width: 64px; }
@media (prefers-reduced-motion: reduce) {
  .cf-flip { transition: none; }
}

/* A reversed tarot card reads in the mode's violet rather than an alarm red —
   reversal is a meaning, not a failure. */
#total[data-variant="tarot-rev"] { color: var(--accent); }

/* ---- system picker popover ---- */
/* A corner menu anchored under the header's mode button (openMode positions it
   from the button's rect). One-line rows: mark, shorthand, dice. */
.mode-pop {
  position: fixed;
  z-index: 40;
  width: 252px;
  /* Tall enough for all thirteen rows on a phone; small screens still scroll. */
  max-height: min(84vh, 640px);
  overflow-y: auto;
  background: var(--face);
  border: 1px solid var(--hair);
  border-radius: 14px;
  box-shadow: 0 12px 32px rgba(0, 0, 0, 0.28);
  padding: 6px;
  display: flex;
  flex-direction: column;
  gap: 1px;
}
.mode-row {
  appearance: none;
  display: flex;
  align-items: center;
  gap: 9px;
  width: 100%;
  padding: 7px 9px;
  border: 0;
  border-radius: 9px;
  background: none;
  color: var(--line);
  font: inherit;
  text-align: left;
  cursor: pointer;
  transition: background 120ms ease;
}
.mode-row:hover { background: color-mix(in srgb, var(--line) 7%, transparent); }
.mode-row:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
/* The active system holds its own colour, matching the tray it recolours. */
.mode-row[aria-pressed="true"] {
  background: color-mix(in srgb, var(--sys, var(--accent)) 13%, transparent);
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--sys, var(--accent)) 50%, transparent);
}
/* Card decks sit below the dice systems, past their own rule. */
.mode-row-break {
  border-top: 1px solid var(--hair);
  border-radius: 0 0 9px 9px;
  margin-top: 3px;
  padding-top: 9px;
}
/* Dicebox itself sits pinned above the alphabetical systems. */
.mode-row-pinned {
  border-bottom: 1px solid var(--hair);
  border-radius: 9px 9px 0 0;
  margin-bottom: 3px;
  padding-bottom: 9px;
}
.mode-row-ico { width: 20px; height: 20px; flex: 0 0 auto; color: var(--sys, var(--muted)); }
.mode-row-name { font-size: 13px; font-weight: 650; letter-spacing: 0.01em; white-space: nowrap; }
.mode-row-sub {
  margin-left: auto;
  font-size: 10.5px;
  color: var(--muted);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* Each system's row wears the colour its mode paints the app with. */
.mode-cards { --sys: #2F8B57; }
.mode-tarot { --sys: #8B76D6; }
.mode-napoletane { --sys: #12A0D7; }
.mode-hanafuda { --sys: #E8483B; }
.mode-utagaruta { --sys: #C9A227; }
.mode-v5 { --sys: #C3212E; }
.mode-fate { --sys: #6E96BE; }
.mode-genesys { --sys: #BFA766; }
.mode-daggerheart { --sys: #C9A227; }
.mode-cthulhutech { --sys: #57B591; }
.mode-yearzero { --sys: #8FA05C; }
.mode-alien { --sys: #D9922E; }
.mode-bladerunner { --sys: #38C6C6; }
.mode-twilight { --sys: #B08A3E; }
/* ---- Call of Cthulhu picker: skill dial and the two d10-shaped modifier keys
   all on one row — the keys are compact, so nothing needs its own line. */
.coc-picker { flex-direction: row; align-items: center; justify-content: center; gap: 12px; flex-wrap: wrap; }
.coc-picker .gen-chips { width: auto; gap: 8px; }
.coc-picker .gen-chip { flex: 0 0 auto; min-width: 58px; }
.iron-picker { flex-direction: column; align-items: stretch; gap: 8px; }

/* One shelf holds every roll: Action, Progress, each pinned oracle, and the
   +Oracles tile that opens the browser. Tap a tile to load it on the tray and
   roll it; the active tile is ringed in the mode accent, and a tap on the tray
   re-rolls it. No labels — the tiles speak for themselves. */
.iron-shelf { display: flex; gap: 8px; overflow-x: auto; scrollbar-width: none; padding-bottom: 2px; }
.iron-shelf::-webkit-scrollbar { display: none; }
.iron-tile {
  flex: 0 0 auto; display: flex; align-items: stretch;
  border: 1px solid var(--hair); border-radius: 12px; background: var(--face); overflow: hidden;
}
.iron-tile.is-active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.iron-tile-main {
  appearance: none; border: 0; background: transparent; cursor: pointer; text-align: left;
  display: flex; flex-direction: column; gap: 1px; padding: 8px 12px; color: var(--line); font: inherit; min-width: 0;
}
.iron-tile-name { font-size: 13.5px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.iron-tile-sub { font-size: 10px; letter-spacing: 0.04em; color: var(--muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* A pinned oracle shows its collection path as the sub; cap the width so a deep
   path (Planet › Inhabited World) truncates rather than stretching the shelf. */
.iron-tile-oracle .iron-tile-main { max-width: 190px; }
.iron-tile.is-active .iron-tile-name { color: var(--accent); }
.iron-tile-main:hover { background: color-mix(in srgb, var(--accent) 8%, transparent); }
/* The Action tile's +N chip opens the modifier dial without leaving the tile. */
.iron-tile-mod {
  appearance: none; border: 0; border-left: 1px solid var(--hair); background: transparent;
  color: var(--line); font: inherit; font-weight: 700; font-size: 15px; padding: 0 12px; cursor: pointer;
  font-variant-numeric: tabular-nums;
}
.iron-tile-mod:hover { background: color-mix(in srgb, var(--accent) 10%, transparent); }
.iron-tile-close {
  appearance: none; border: 0; border-left: 1px solid var(--hair); background: transparent;
  color: var(--muted); font-size: 11px; line-height: 1; padding: 0 9px; cursor: pointer;
}
.iron-tile-close:hover { color: var(--danger); }
/* The three kinds of tile read apart by their sub-label tint: the two dice rolls
   stay muted; a pinned oracle picks up the steel oracle accent. */
.iron-tile-action .iron-tile-sub,
.iron-tile-progress .iron-tile-sub { color: var(--muted); }
.iron-tile-oracle .iron-tile-sub { color: color-mix(in srgb, var(--accent) 70%, var(--muted)); }
/* The +Oracles tile opens the browser to pin more — dashed and accent so it
   reads as "add", distinct from a roll. */
.iron-tile-add {
  appearance: none; flex: 0 0 auto; display: inline-flex; flex-direction: column; align-items: center; justify-content: center; gap: 1px;
  border: 1px dashed var(--accent); border-radius: 12px; background: transparent; color: var(--accent);
  padding: 8px 14px; font: inherit; cursor: pointer;
}
.iron-tile-add-plus { font-size: 16px; line-height: 1; }
.iron-tile-add-label { font-size: 11px; font-weight: 700; }
.iron-tile-add:hover { background: color-mix(in srgb, var(--accent) 10%, transparent); }

/* Ask the Oracle — the five likelihoods on one scrolling line with a small inline
   label, the way the die strip scrolls. */
.iron-ask-odds { display: flex; align-items: center; gap: 6px; overflow-x: auto; scrollbar-width: none; padding-bottom: 2px; }
.iron-ask-odds::-webkit-scrollbar { display: none; }
.iron-ask-inline {
  flex: 0 0 auto; font-family: "Iosevka Etoile", ui-monospace, monospace;
  font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); padding-right: 2px;
}
.iron-odd {
  appearance: none; flex: 0 0 auto; border: 1px solid var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, transparent); color: var(--accent);
  border-radius: 999px; padding: 6px 12px; font: inherit; font-size: 12px; font-weight: 600; cursor: pointer; white-space: nowrap;
}
.iron-odd:hover { background: color-mix(in srgb, var(--accent) 20%, transparent); }
.iron-odd:active { transform: scale(0.96); }

/* Oracle browser — a full sheet: sticky search over a collapsible table tree. */
.oracle-sheet .sheet-panel { max-width: 560px; margin-left: auto; margin-right: auto; }

.oracle-search {
  position: sticky; top: 0; z-index: 1;
  display: flex; align-items: center; gap: 8px; margin: 0 8px 8px; padding: 9px 12px;
  background: var(--face); border: 1px solid var(--hair); border-radius: 10px;
}
.oracle-search svg { width: 15px; height: 15px; color: var(--muted); flex: none; }
.oracle-search input { border: 0; background: transparent; color: var(--line); font: inherit; font-size: 14px; flex: 1; outline: none; }
.oracle-tree { padding: 0 8px 28px; }
.oracle-coll-head {
  appearance: none; width: 100%; text-align: left; display: flex; align-items: center; gap: 8px;
  border: 0; background: transparent; color: var(--line); font: inherit; font-size: 14px; font-weight: 600;
  padding: 10px 8px; cursor: pointer; border-radius: 8px;
}
.oracle-coll-head:hover { background: var(--face); }
.oracle-caret { width: 14px; height: 14px; color: var(--muted); flex: none; transition: transform .15s; }
.oracle-coll.open > .oracle-coll-head .oracle-caret { transform: rotate(90deg); }
.oracle-coll-name { flex: 1; }
.oracle-coll-count { color: var(--muted); font-size: 12px; font-weight: 500; font-variant-numeric: tabular-nums; }
.oracle-kids { display: none; padding-left: 14px; }
.oracle-coll.open > .oracle-kids { display: block; }
.oracle-table {
  appearance: none; width: 100%; text-align: left; display: flex; align-items: center; gap: 8px;
  border: 0; background: transparent; color: var(--line); font: inherit; font-size: 13.5px;
  padding: 9px 8px 9px 22px; cursor: pointer; border-radius: 8px;
}
.oracle-table:hover { background: var(--face); }
/* The keyboard-highlighted result (↑/↓ in the search box; Enter rolls it). */
.oracle-table.is-highlighted { background: color-mix(in srgb, var(--accent) 16%, transparent); box-shadow: inset 2px 0 0 var(--accent); }
.oracle-table-name { flex: 1; }
.oracle-table-path { display: none; color: var(--muted); font-size: 11px; }
.oracle-dtag { color: var(--muted); font-size: 10px; border: 1px solid var(--hair); border-radius: 5px; padding: 1px 5px; font-variant-numeric: tabular-nums; }
.oracle-tree.searching .oracle-coll-head,
.oracle-tree.searching .oracle-coll-count { display: none; }
.oracle-tree.searching .oracle-kids { display: block !important; padding-left: 0; }
.oracle-tree.searching .oracle-table-path { display: inline; margin-left: 6px; }
.oracle-empty { color: var(--muted); font-size: 13px; text-align: center; padding: 24px; }
@media (prefers-reduced-motion: reduce) {
  .oracle-caret { transition: none; }
}
.coc-bonus { --dc: #5DAE6A; }
.coc-penalty { --dc: #C0453F; }
.mode-callofcthulhu { --sys: #7FA05A; }
.mode-deltagreen { --sys: #4E9068; }
.mode-ironsworn { --sys: #6E8B9A; }
.mode-starforged { --sys: #8E86C9; }
.mode-dcc { --sys: #C36A3C; }
.mode-starwars { --sys: #5B9BD5; }
.mode-onering { --sys: #B5893C; }
.mode-pbta { --sys: #D97A3C; }
.mode-mist { --sys: #3F9FA0; }
.mode-drawsteel { --sys: #E27B3F; }
.mode-fitd { --sys: #3FC7BD; }
.mode-crows { --sys: #98A08A; }
.mode-stonetop { --sys: #8FB265; }
.mode-shadowdark { --sys: #E39A2E; }
.mode-dccarl { --sys: #F576AD; }
.mode-mothership { --sys: #AEC93C; }

/* How many dice each tap adds. Typing a number here and tapping d6 is how you
   get 100d6 without tapping a hundred times. */
/* The classic −/n/+ trio survives in ONE place: the modifier sheet's
   exact-count row — a typed count beside per-die options, inside the one
   menu the picker grammar allows. The strip's dice-per-tap control is the
   .count-chip barrel below. */
.counter {
  display: flex;
  align-items: center;
  flex: 0 0 auto;
  border: 1px solid var(--hair);
  border-radius: 8px;
  overflow: hidden;
}

.counter .step {
  border: 0;
  border-radius: 0;
  width: 30px;
  height: 34px;
}

.count {
  width: 34px;
  height: 34px;
  border: 0;
  border-left: 1px solid var(--hair);
  border-right: 1px solid var(--hair);
  background: none;
  color: var(--line);
  font: inherit;
  font-size: 14px;
  font-variant-numeric: tabular-nums;
  text-align: center;
  padding: 0;
  -moz-appearance: textfield;
}
.count::-webkit-outer-spin-button,
.count::-webkit-inner-spin-button { appearance: none; margin: 0; }

.step {
  appearance: none;
  width: 34px;
  height: 34px;
  flex: 0 0 auto;
  border: 1px solid var(--hair);
  border-radius: 8px;
  background: none;
  color: var(--line);
  font: inherit;
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
}

/* Dice per tap: one barrel chip where the strip's −/count/+ row stood.
   The × is the multiplication it performs on every die tap, quiet. */
.count-chip {
  appearance: none;
  display: flex;
  align-items: center;
  gap: 2px;
  flex: 0 0 auto;
  border: 1px solid var(--hair);
  border-radius: 8px;
  background: none;
  color: var(--line);
  font: inherit;
  height: 34px;
  padding: 0 10px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}
.count-chip-val {
  font-size: 14px;
  font-variant-numeric: tabular-nums;
}
.count-x {
  font-size: 11px;
  color: var(--muted);
}

/* The row holds more dice than fit, so it scrolls. Arrows appear at whichever
   end has more to show — a scrollable strip with no visible edge reads as the
   whole set, and everything past the eighth die goes unnoticed. */
.dice-scroll {
  position: relative;
  display: flex;
  align-items: center;
  gap: 2px;
  flex: 1 1 auto;
  min-width: 0;
}

.dice-buttons {
  display: flex;
  gap: 4px;
  overflow-x: auto;
  scrollbar-width: none;
  flex: 1 1 auto;
  scroll-behavior: smooth;
  /* Momentum scrolling on iOS. */
  -webkit-overflow-scrolling: touch;
}
.dice-buttons::-webkit-scrollbar { display: none; }

.nudge {
  appearance: none;
  flex: 0 0 auto;
  width: 22px;
  min-height: 34px;
  border: 0;
  background: none;
  color: var(--muted);
  font-size: 19px;
  line-height: 1;
  padding: 0;
  cursor: pointer;
}
.nudge:hover { color: var(--accent); }

.dbtn {
  appearance: none;
  flex: 0 0 auto;
  min-width: 46px;
  min-height: 34px;
  border: 1px solid var(--hair);
  border-radius: 8px;
  background: none;
  color: var(--line);
  font: inherit;
  font-size: 13px;
  font-variant-numeric: tabular-nums;
  cursor: pointer;
  transition: border-color 160ms ease, color 160ms ease;
}
.dbtn:active { border-color: var(--line); }

/* A die in the tray reads as selected, with its count on the button — the pool
   is visible in the row you are already looking at, not only in the field. */
.dbtn[aria-pressed="true"] {
  color: var(--accent);
  border-color: var(--accent);
}

/* Kept inside the button's padding: the hold-fill needs overflow:hidden, so
   anything poking past the border would be clipped. */
.dbtn[data-count]::after {
  content: attr(data-count) "\00d7";
  position: absolute;
  top: 2px;
  right: 4px;
  font-size: 9.5px;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0;
  color: var(--accent);
  pointer-events: none;
}

/* Make room for the badge so it never sits on the label. */
.dbtn[data-count] {
  padding-right: 14px;
}

/* The modifier's own glyph, bottom-left so it never collides with the count
   badge top-right. A shared underline said "modified" but not which. */
.dbtn[data-mod]::before {
  content: attr(data-mod);
  position: absolute;
  bottom: 1px;
  left: 4px;
  z-index: 1;
  font-size: 9px;
  line-height: 1;
  letter-spacing: 0;
  color: var(--accent);
  pointer-events: none;
}

.dbtn[data-mod] {
  padding-left: 13px;
}

.sheet-option[aria-pressed="true"] .sheet-option-name {
  color: var(--accent);
}

/* The custom-die button reads as an action rather than a die. Its label
   span is what the scrub previews — d? at rest, dN while a pending size
   waits for the minting tap. */
.dbtn.custom {
  color: var(--muted);
  font-family: "Iosevka Etoile", ui-monospace, monospace;
}
.custom-label { position: relative; font-variant-numeric: tabular-nums; }

/* A die added through the picker. Dashed to show it is not part of the standard
   set, but otherwise a full die: tappable, countable, holdable for modifiers. */
.dbtn[data-custom] {
  border-style: dashed;
}


/* Holding a die fills it, which both signals that a long press does something
   and shows how much longer to hold. The fill is its own element rather than a
   pseudo-element: ::before carries the modifier glyph and ::after the count, and
   a third use would collide with one of them. */
.dbtn {
  position: relative;
  overflow: hidden;
  isolation: isolate;
  /* Left to the browser, so a sideways drag scrolls the row. Claiming the
     gesture for scrubbing instead made the row unscrollable on a phone, which
     put every die past the eighth out of reach — a far worse trade than losing
     drag-to-scrub. Scrubbing now lives behind the long press, where it does not
     compete with panning. */
  touch-action: manipulation;
}

.dbtn-fill {
  position: absolute;
  inset: 0;
  z-index: -1;
  background: color-mix(in srgb, var(--accent) 18%, transparent);
  transform: scaleX(0);
  transform-origin: left;
  pointer-events: none;
}
.dbtn[data-holding="1"] .dbtn-fill {
  transform: scaleX(1);
  transition: transform 450ms linear;
}

@media (prefers-reduced-motion: reduce) {
  .dbtn[data-holding="1"] .dbtn-fill { transition-duration: 1ms; }
}

/* ---- modifier sheet ---- */

/* Fills the tray, like the help panel. A floating card over a dimmed page is a
   different design language than the rest of this app — the tray is the surface
   everything else happens on, so options belong there too. */
.sheet {
  position: absolute;
  inset: 0;
  z-index: 3;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  overflow-y: auto;
  scrollbar-width: none;
  background: var(--paper);
  animation: help-in 160ms ease both;
}
.sheet::-webkit-scrollbar { display: none; }

.sheet-panel {
  width: 100%;
  padding: 2px;
  /* Sits toward the top of a tall tray rather than floating in the middle of a
     large empty field, which reads as stranded on a desktop window. */
  margin-top: clamp(0px, 4vh, 28px);
  margin-bottom: auto;
}

.sheet-title {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin: 0 0 8px;
  padding: 0 10px;
  font-family: "Iosevka Etoile", ui-monospace, monospace;
  font-size: 13px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--muted);
}

.sheet-close {
  appearance: none;
  border: 0;
  background: none;
  color: var(--muted);
  font: inherit;
  font-size: 13px;
  line-height: 1;
  padding: 6px;
  margin: -6px;
  cursor: pointer;
}

.sheet-options {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.sheet-option {
  appearance: none;
  display: grid;
  grid-template-columns: 1fr auto;
  gap: 0 12px;
  width: 100%;
  text-align: left;
  border: 0;
  border-radius: 8px;
  background: none;
  padding: 9px 10px;
  font: inherit;
  cursor: pointer;
  transition: background 140ms ease;
}
.sheet-option:hover,
.sheet-option:focus-visible {
  background: color-mix(in srgb, var(--accent) 8%, transparent);
}

/* How many of this die are staged. Dragging the button sideways is quicker, but
   only a typed number lands on an exact count. */
.sheet-count {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 4px 10px 10px;
  margin-bottom: 4px;
  border-bottom: 1px solid var(--hair);
}

.sheet-count-label {
  font-size: 14px;
  color: var(--line);
}

.sheet-count-field {
  width: 46px;
}

/* Dragging a die button to dial its count. */
.dbtn[data-scrubbing] {
  border-color: var(--accent);
  color: var(--accent);
}

/* Another modifier already answers this question, so it cannot also apply. */
.sheet-option[data-blocked] {
  opacity: 0.34;
  cursor: default;
}
.sheet-option[data-blocked]:hover { background: none; }

.sheet-option-name {
  font-size: 14px;
  color: var(--line);
}

/* The same mark the die button shows, so the row's glyphs teach themselves. */
.sheet-option-mark {
  display: inline-block;
  width: 1.3em;
  color: var(--accent);
  font-size: 12px;
}

.sheet-option-notation {
  font-family: "Iosevka Etoile", ui-monospace, monospace;
  font-size: 12.5px;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
  text-align: right;
}

.sheet-option-hint {
  grid-column: 1 / -1;
  font-size: 12px;
  color: var(--muted);
  margin-top: 1px;
}

.clear {
  appearance: none;
  flex: 0 0 auto;
  width: var(--tap);
  min-height: var(--tap);
  border: 1px solid var(--hair);
  border-radius: 10px;
  background: none;
  color: var(--muted);
  font: inherit;
  font-size: 13px;
  cursor: pointer;
}

/* ---- notation entry ---- */

.entry {
  display: flex;
  gap: 8px;
  flex: 0 0 auto;
}

.entry input {
  flex: 1 1 auto;
  min-width: 0;
  min-height: var(--tap);
  padding: 0 14px;
  border: 1px solid var(--hair);
  border-radius: 10px;
  background: var(--face);
  color: var(--line);
  font: inherit;
  font-size: 16px; /* 16px keeps iOS from zooming on focus */
}
.entry input::placeholder { color: var(--muted); }
.entry input:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: transparent;
}

.roll {
  appearance: none;
  min-height: var(--tap);
  padding: 0 22px;
  border: 0;
  border-radius: 10px;
  background: var(--line);
  color: var(--paper);
  font: inherit;
  font-size: 15px;
  font-weight: 500;
  letter-spacing: 0.04em;
  cursor: pointer;
}
.roll:active { opacity: 0.8; }

.error {
  margin: 0;
  font-size: 13px;
  color: var(--danger);
  text-align: center;
  flex: 0 0 auto;
}

/* ---- history ---- */

/* The recent strip stays short; the full session lives behind its own panel.
   Only the list scrolls, so the "full history" button is always reachable. */
.log {
  flex: 0 0 auto;
}

.log ul {
  max-height: 92px;
  overflow-y: auto;
  scrollbar-width: none;
}
.log ul::-webkit-scrollbar { display: none; }
.log::-webkit-scrollbar { display: none; }

.log ul {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.log li {
  font-size: 12px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  padding: 3px 0;
  border-top: 1px solid var(--hair);
}

/* Notation on the left, total on the right. */
.log-line {
  display: flex;
  justify-content: space-between;
  gap: 12px;
}
.log li b {
  color: var(--line);
  font-weight: 600;
}

.log-more {
  margin: 4px 0 0;
  text-align: center;
}

.log-more-button {
  appearance: none;
  border: 0;
  background: none;
  color: var(--muted);
  font: inherit;
  font-size: 11px;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 3px 6px;
  cursor: pointer;
}
.log-more-button:hover { color: var(--accent); }

.log-count {
  font-variant-numeric: tabular-nums;
  text-transform: none;
  letter-spacing: 0;
  opacity: 0.8;
}

/* ---- full history ---- */

/* The one sheet that outgrows the tray.
   Every other sheet is a control acting on the dice underneath it, so covering
   only the tray is right for those. This one is a reading view — the question
   it answers is "what did I roll twenty minutes ago" — and confining it to the
   tray meant scrolling a session through a window a third of a phone tall.
   So it covers the app instead, and the list inside it does the scrolling. */
#historyPanel {
  position: fixed;
  inset: 0;
  z-index: 6;
  overflow: hidden;
  padding: calc(env(safe-area-inset-top) + 10px) var(--gut)
           calc(env(safe-area-inset-bottom) + 10px);
}

/* Overrides .sheet-panel's top margin and auto bottom margin, which exist to
   stop a short panel floating in the middle of a tall tray. This one is not
   short — it fills what it is given. */
#historyPanel .sheet-panel {
  display: flex;
  flex-direction: column;
  height: 100%;
  min-height: 0;
  max-width: 620px;
  margin: 0 auto;
}

#historyPanel .sheet-title,
#historyPanel .history-actions { flex: 0 0 auto; }

.history-full {
  list-style: none;
  margin: 0;
  padding: 0 2px;
  overflow-y: auto;
  scrollbar-width: none;
  /* Reading size, adjustable from the panel's own control and remembered
     between sessions. Rows below scale off this, so one property moves the
     whole list. */
  font-size: var(--history-text, 13px);
}
#historyPanel .history-full {
  flex: 1 1 auto;
  min-height: 0;
}
.history-full::-webkit-scrollbar { display: none; }

.history-full li {
  font-size: 1em;
  line-height: 1.4;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  padding: 6px 8px;
  border-bottom: 1px solid var(--hair);
}
.history-full li:last-child { border-bottom: 0; }
.history-full li b { color: var(--line); font-weight: 600; }

.history-time {
  font-family: "Iosevka Etoile", ui-monospace, monospace;
  font-size: 0.9em;
  opacity: 0.75;
  min-width: 7em;
}

/* Scaled with the list rather than pinned at 11px: this is the line that says
   what each die landed on, which is the whole reason to open the panel, and it
   was the least readable thing in it. */
#historyPanel .log-detail {
  font-size: 0.92em;
  opacity: 0.85;
}

.history-size {
  display: flex;
  gap: 2px;
  margin-left: auto;
  margin-right: 6px;
}

.history-size-step {
  appearance: none;
  border: 1px solid var(--hair);
  border-radius: 6px;
  background: none;
  color: var(--muted);
  font: inherit;
  font-size: 11px;
  line-height: 1;
  /* A comfortable tap target without making the title row tall. */
  min-width: 30px;
  padding: 6px 4px;
  cursor: pointer;
}
.history-size-step:hover:not(:disabled) { color: var(--accent); border-color: var(--accent); }
.history-size-step:disabled { opacity: 0.35; cursor: default; }

.history-notation {
  flex: 1 1 auto;
  text-align: left;
  padding-left: 10px;
}

.history-empty {
  text-align: center;
  padding: 18px 0 !important;
  opacity: 0.7;
}

.history-actions {
  display: flex;
  gap: 6px;
  padding: 10px 8px 2px;
  border-top: 1px solid var(--hair);
  margin-top: 6px;
}

.history-actions .ghost {
  flex: 1 1 auto;
  min-height: 34px;
  padding: 0 8px;
}

.history-clear {
  flex: 0 0 auto !important;
  color: var(--danger) !important;
}

/* Table events in the log — the torch lit, snuffed, guttered out. Same row
   shape as a roll, quieter voice: no notation, no arithmetic, just the fact
   at its time, muted so the rolls stay the subject of the record. */
#history li[data-event] .log-event {
  color: var(--muted);
  font-size: 12px;
  letter-spacing: 0.01em;
}
#historyPanel li[data-event] b {
  color: var(--muted);
  font-weight: 500;
}

/* What each die landed on, under its own roll. Quieter than the total, since
   the total is what you look for first and this is what you check afterwards. */
.log-detail {
  display: block;
  font-family: "Iosevka Etoile", ui-monospace, monospace;
  font-size: 11px;
  line-height: 1.35;
  color: var(--muted);
  opacity: 0.75;
  word-break: break-word;
}

/* ---- rooms ---- */

/* The standing answer to "are my rolls leaving this device?". It sits in the
   header rather than inside the panel, because the panel is shut almost all of
   the time and the question is always live. */
.share-flag {
  display: inline-flex;
  align-items: center;
  gap: 5px;
  font-size: 10.5px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent);
  margin-right: 2px;
}

.share-dot {
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--accent);
  animation: share-pulse 2.4s ease-in-out infinite;
}

/* Slow enough to read as "on" rather than as an alert. A steady dot was too
   easy to stop seeing; anything faster looked like something was wrong. */
@keyframes share-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.35; }
}

@media (prefers-reduced-motion: reduce) {
  .share-dot { animation: none; }
}

/* On a narrow phone the word would push the buttons off the row; the dot alone
   still carries it, and the panel spells it out. */
@media (max-width: 380px) {
  .share-flag-text { display: none; }
  .wordmark { font-size: 13px; letter-spacing: 0.15em; }
  .system-badge { margin-left: 6px; padding: 3px 6px; font-size: 9px; letter-spacing: 0.1em; }
}

#roomToggle[data-live] {
  color: var(--accent);
  border-color: var(--accent);
}

/* The before-joining half of the panel. Toggled wholesale against .room-live,
   so it needs a display of its own for [hidden] to have something to beat. */
.room-setup {
  display: block;
}

.room-note,
.room-hint {
  margin: 0;
  font-size: 12px;
  line-height: 1.5;
  color: var(--muted);
  max-width: 46ch;
}

.room-note { padding: 0 10px 12px; }

/* The link sits directly under the note it belongs to, so the note above it
   drops its own bottom padding rather than the pair reading as two blocks. */
.room-note:has(+ .room-note-more) { padding-bottom: 6px; }

.room-block {
  padding: 12px 10px;
  border-top: 1px solid var(--hair);
}

/* The first thing in the panel needs no rule above it — the title is already
   the boundary, and a hairline directly under it reads as a stray line. */
.room-block-lead {
  border-top: 0;
  padding-top: 4px;
}

/* Owlbear mode notice (panel only). Sharing is part of the extension contract,
   so this is intentionally information rather than a preference control. */
.room-obr { border-top: 0; padding-top: 4px; }
.room-obr-note { margin: 0; font-size: 11px; line-height: 1.35; }

/* The privacy note, now at the foot of the panel rather than in front of it.
   Set apart and quieter than the controls: it is there to be found, not to be
   read before you are allowed to roll. */
.room-privacy {
  margin-top: 4px;
  padding-top: 10px;
  border-top: 1px solid var(--hair);
}

.room-create { width: 100%; }

.room-block .room-hint { margin-top: 8px; }

.room-label {
  display: block;
  margin: 0 0 6px;
  font-family: "Iosevka Etoile", ui-monospace, monospace;
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}

.room-made {
  padding: 12px 10px;
  border-top: 1px solid var(--hair);
}

/* Shown plainly and large enough to read across a table, since reading it out
   loud is how it usually travels. Selectable, because copying by hand is the
   fallback when the clipboard API is unavailable. */
.room-phrase {
  margin: 0 0 10px;
  font-family: "Iosevka Etoile", ui-monospace, monospace;
  font-size: 15px;
  line-height: 1.5;
  color: var(--accent);
  word-spacing: 0.3em;
  word-break: break-word;
  cursor: pointer;
  /* `all` rather than `text`: one tap takes the whole phrase, which is what
     anyone highlighting it wants, and a partial passphrase is never useful. */
  user-select: all;
  -webkit-user-select: all;
}
.room-phrase:hover { opacity: 0.8; }

/* The confirmation. A brief lift and a fade rather than a colour change: the
   words are already the accent colour, so there is nothing for a colour change
   to say. */
.room-phrase[data-copied] { animation: phrase-copied 700ms ease; }

@keyframes phrase-copied {
  0%   { opacity: 1;    transform: none; }
  14%  { opacity: 0.35; transform: scale(1.04); }
  46%  { opacity: 1;    transform: scale(1.01); }
  100% { opacity: 1;    transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .room-phrase[data-copied] { animation-duration: 1ms; }
}

.room-actions {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
}

.room-actions .ghost {
  flex: 1 1 auto;
  min-height: 34px;
  padding: 0 8px;
}

.room-leave {
  flex: 0 0 auto !important;
  color: var(--danger) !important;
}

.room-join input,
#roomName {
  width: 100%;
  min-height: var(--tap);
  padding: 0 12px;
  margin-bottom: 8px;
  border: 1px solid var(--hair);
  border-radius: 10px;
  background: var(--face);
  color: var(--line);
  font: inherit;
  font-size: 16px; /* 16px keeps iOS from zooming on focus */
}
.room-join input::placeholder { color: var(--muted); }

.room-join-button { width: 100%; }

.room-live {
  padding: 12px 10px;
  border-top: 1px solid var(--hair);
}

.room-roster-label { margin-top: 14px; }

.room-roster {
  list-style: none;
  margin: 0 0 12px;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
  max-height: 26vh;
  overflow-y: auto;
  scrollbar-width: none;
}
.room-roster::-webkit-scrollbar { display: none; }

.room-roster li {
  font-size: 13.5px;
  color: var(--line);
  padding: 5px 2px;
  border-bottom: 1px solid var(--hair);
}
.room-roster li:last-child { border-bottom: 0; }

.room-roster-empty {
  color: var(--muted) !important;
  font-size: 12.5px !important;
}

.room-state {
  margin: 0;
  padding: 12px 10px 2px;
  border-top: 1px solid var(--hair);
  font-size: 12px;
  line-height: 1.5;
  color: var(--muted);
  min-height: 1.5em;
}

/* Someone else's roll. Marked quietly — a name in the accent colour and a rule
   down the left — because the log is read at a glance and a loud treatment for
   half the entries would make the whole strip harder to scan, not easier. */
.log-who {
  color: var(--accent);
  margin-right: 6px;
}

/* Your own name, once a room makes it worth showing. Deliberately not the
   accent colour: every line in the log carries a name now, so if yours were
   painted like a peer's there would be no difference left for the accent to
   carry and the strip would read as one undifferentiated block. Quiet enough
   to be a label, loud enough to answer "which of these are mine". */
.log-who[data-self] {
  color: var(--muted);
  opacity: 0.75;
}

.log li[data-remote] {
  border-left: 2px solid var(--accent);
  padding-left: 6px;
  margin-left: -8px;
}

/* Same rule in the full history, but paid for out of the existing padding
   rather than a negative margin: these rows are inside a panel with an edge, so
   pulling them left would hang them off it. 6px + a 2px border lands the text
   exactly where an unmarked row's text sits. */
.history-full li[data-remote] {
  border-left: 2px solid var(--accent);
  padding-left: 6px;
}

:where(button, input):focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}

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

.room-owlbear{margin-top:12px;padding-top:12px;border-top:1px solid var(--hair);line-height:1.5}
.room-owlbear code{font-family:ui-monospace,monospace;font-size:0.92em;background:color-mix(in srgb,var(--line) 8%,transparent);padding:1px 5px;border-radius:5px;word-break:break-all}
