/* ─── Platytutor shared components ─────────────────────────────────────────
   Canonical, token-driven UI primitives shared across every app page and
   game. Load order is always: tokens.css -> components.css -> page sheet.
   Page/game-specific rules stay in their own sheets or inline blocks.      */

/* ─── Base: paper body + grain overlay ──────────────────────────────────── */

/* The page's minimum is the small viewport (svh): iOS Safari's toolbar comes
   and goes at the ends of a scroll, and a dvh minimum would grow and shrink
   the page with it, so the end of the page would jump (lib/README.md,
   "Scrolling without shaking"). Sheets and overlays may follow dvh. */
body {
  margin: 0;
  min-height: 100vh;
  min-height: 100svh;
  color: var(--ink);
  font-family: var(--font-body);
  font-weight: 500;
  background-color: var(--paper);
  background-image:
    radial-gradient(var(--ink-wash) 1px, transparent 1px),
    radial-gradient(circle at 12% 8%, var(--sun-glow), transparent 45%),
    radial-gradient(circle at 88% 4%, var(--mint-glow), transparent 42%),
    radial-gradient(circle at 80% 94%, var(--coral-glow), transparent 50%),
    linear-gradient(180deg, var(--paper) 0%, var(--paper-deep) 100%);
  background-size: 22px 22px, auto, auto, auto, auto;
  background-attachment: fixed;
  letter-spacing: 0.005em;
  -webkit-font-smoothing: antialiased;
}

/* The hidden attribute always wins. Components here set their own display
   (.btn-stamp is inline-flex), which would otherwise beat the UA rule. */
[hidden] {
  display: none !important;
}

/* Every raster picture on a compositing layer of its own. The media are
   made large (1200 px pictures, 256 px avatars, badge GIFs) and shown much
   smaller; painted into the page, Chrome shrinks them with a cheap filter,
   and on a 1x screen (WQHD) the ink outlines come out jagged — a 2x screen
   hides it. A layer is shrunk smoothly. Zero specificity: a component may
   still turn it off. An SVG is drawn at its size and needs no layer.
   Only on a 1x screen, then: on a 2x or 3x one (a phone, a Retina laptop)
   a layer for every picture only multiplies the layers (and everything
   drawn over a picture needs one too), which a phone pays for in memory
   and in each frame it composites. A component that needs a layer of its
   own asks for it. */
@media (max-resolution: 1.5dppx) {
  :where(img:not([src*=".svg"])) {
    will-change: transform;
  }
}

body::before {
  content: "";
  position: fixed;
  inset: 0;
  pointer-events: none;
  z-index: 0;
  opacity: 0.34;
  mix-blend-mode: multiply;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0.11  0 0 0 0 0.22  0 0 0 0 0.35  0 0 0 0.08 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
}

/* A phone or a touch screen: the same grain, drawn into the page's own
   background. The layer above is fixed over the whole window and blended
   (multiply), and such a layer costs a phone an offscreen pass over the
   whole screen on every frame it composites (a card turning, a scroll, a
   sheet), its noise drawn again on the CPU whenever it repaints. The tile
   (assets/paper-grain-v1.webp, 180px like the noise) is that noise rendered
   once, its strength and the multiply with the paper baked in, so it lies
   on the paper in plain alpha and looks the same. The page's background
   scrolls with it here, as iOS Safari draws it anyway (it ignores fixed). */
@media (max-width: 767.98px), (pointer: coarse) {
  body {
    background-image:
      url("../assets/paper-grain-v1.webp"),
      radial-gradient(var(--ink-wash) 1px, transparent 1px),
      radial-gradient(circle at 12% 8%, var(--sun-glow), transparent 45%),
      radial-gradient(circle at 88% 4%, var(--mint-glow), transparent 42%),
      radial-gradient(circle at 80% 94%, var(--coral-glow), transparent 50%),
      linear-gradient(180deg, var(--paper) 0%, var(--paper-deep) 100%);
    background-size: 180px 180px, 22px 22px, auto, auto, auto, auto;
    background-attachment: scroll;
  }

  body::before {
    display: none;
  }
}

/* The scripts hide things with the `hidden` attribute, and an author
   display: flex / grid would beat the UA's [hidden] rule — Bootstrap's reboot
   used to cover this on every page. */
[hidden] {
  display: none !important;
}

/* ─── Brand ──────────────────────────────────────────────────────────────
   --brand-mark-size lets a page tune the mark (44px on the home hero,
   40px in the compact game/nav bars) without a separate rule. */

.brand {
  display: inline-flex;
  align-items: center;
  gap: 0.7rem;
  font-family: var(--font-display);
  font-variation-settings: "SOFT" 30, "WONK" 1;
  font-weight: 700;
  font-size: var(--fs-title);
  letter-spacing: -0.01em;
  color: var(--ink);
  text-decoration: none;
}

.brand-mark {
  --brand-mark-size: 40px;
  width: var(--brand-mark-size);
  height: var(--brand-mark-size);
  border-radius: 12px;
  background: var(--ink);
  color: var(--sun);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-display);
  font-weight: 900;
  font-size: var(--fs-body-lg);
  box-shadow: var(--shadow-stamp-sm);
  transform: rotate(-4deg);
}

.brand em,
.brand-name em { font-style: normal; color: var(--coral); 
}

/* ─── Avatar ─────────────────────────────────────────────────────────────
   Single base + size modifier. Top-bar chip uses --sm; user tables use --md. */

.avatar {
  --avatar-size: 32px;
  width: var(--avatar-size);
  height: var(--avatar-size);
  border-radius: 50%;
  object-fit: cover;
  background: var(--paper-deep);
  border: 1.5px solid var(--line-strong);
  flex-shrink: 0;
  display: inline-block;
}

.avatar--sm { --avatar-size: 28px; 
}
.avatar--md { --avatar-size: 32px; 
}
.avatar--lg { --avatar-size: 40px; 
}

.avatar--placeholder {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: var(--fs-caption);
  font-weight: 800;
  color: var(--ink-muted);
  line-height: 1;
}

/* ─── Buttons: stamp CTA ─────────────────────────────────────────────────
   Canonical hover is lift only (no scale). Size/colour variants are modifiers
   below — never restyle .btn-stamp under a page id. */

.btn-stamp {
  --btn-bg: var(--coral);
  --btn-fg: var(--on-color);
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  padding: 0.95rem 1.6rem;
  font-family: var(--font-body);
  font-weight: 800;
  font-size: var(--fs-body-lg);
  letter-spacing: 0.01em;
  color: var(--btn-fg);
  background: var(--btn-bg);
  border: 2px solid var(--ink);
  border-radius: var(--r-full);
  cursor: pointer;
  text-decoration: none;
  box-shadow: var(--shadow-stamp);
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}

/* The lift answers a hovering mouse only: on a touch screen a tap leaves the
   :hover state behind, and the stamp would stay lifted as if still chosen.
   Every lift, turn, scale and hover tooltip in the shared sheets sits behind
   this same query; :active is the press everywhere. */
@media (hover: hover) and (pointer: fine) {
  .btn-stamp:hover { transform: translate(-2px, -3px); box-shadow: var(--shadow-stamp-hover); color: var(--btn-fg);
}
}
.btn-stamp:active { transform: translate(2px, 2px); box-shadow: var(--shadow-stamp-press);
}

/* Disabled: the stamp lies flat — no lift, no shadow, muted ink — so it
   cannot be mistaken for a live button. */
.btn-stamp:disabled,
.btn-stamp[aria-disabled="true"] {
  --btn-bg: var(--paper-deep);
  --btn-fg: var(--ink-muted);
  border-color: var(--ink-muted);
  box-shadow: none;
  cursor: not-allowed;
}
.btn-stamp:disabled:hover, .btn-stamp:disabled:active,
.btn-stamp[aria-disabled="true"]:hover, .btn-stamp[aria-disabled="true"]:active {
  transform: none;
  box-shadow: none;
}

.btn-stamp--ghost { --btn-bg: var(--card); --btn-fg: var(--ink); 
}
.btn-stamp--mint { --btn-bg: var(--mint); --btn-fg: var(--on-bright); 
}
.btn-stamp--sun { --btn-bg: var(--sun); --btn-fg: var(--on-bright); 
}
.btn-stamp--ink { --btn-bg: var(--ink); --btn-fg: var(--paper); 
}
.btn-stamp--danger { --btn-bg: var(--coral); --btn-fg: var(--on-color);
}
/* Audio check's "nyomd meg a zöld gombot" — pre-readers find it by colour.
   --mint is turquoise, so none of the other tints would do. */
.btn-stamp--success { --btn-bg: var(--success); --btn-fg: var(--on-color); 
}

/* Dense toolbar / game-header size (was the Bootstrap .btn override recipe). */
.btn-stamp--sm {
  gap: 0.4rem;
  padding: 0.6rem 1.1rem;
  font-size: var(--fs-body);
  box-shadow: var(--shadow-stamp-sm);
}
@media (hover: hover) and (pointer: fine) {
  .btn-stamp--sm:hover { box-shadow: var(--shadow-stamp-md);
}
}

/* Icon-only stamp — narrower than a labelled sibling. */
.btn-stamp--icon {
  padding: 0.6rem 0.8rem;
  gap: 0;
}

/* Dirty-save pulse (journeys ladder / editor save). */
.btn-stamp--highlight {
  box-shadow: var(--shadow-stamp), 0 0 0 4px var(--sun-ring);
}
.btn-stamp--sm.btn-stamp--highlight {
  box-shadow: var(--shadow-stamp-sm), 0 0 0 4px var(--sun-ring);
}

/* Shared stamp lift/press. Two distances only; the landing hero keeps its own. */
.stamp-interactive,
.chip,
.tab-btn,
.login-close,
.mediagen-close,
.resource-modal-play,
.mediagen-btn,
.game-resource-btn,
.md-statement,
.md-lead-btn,
.md-lead-stuck,
.md-stuck-sweep,
.md-brief-close {
  transition: transform 0.15s ease, box-shadow 0.15s ease;
}

@media (hover: hover) and (pointer: fine) {
  .stamp-interactive:hover,
  .chip:hover,
  .tab-btn:hover,
  .login-close:hover,
  .mediagen-close:hover,
  .resource-modal-play:hover,
  .mediagen-btn:hover,
  .game-resource-btn:hover,
  .md-statement:hover,
  .md-lead-btn:hover,
  .md-lead-stuck:hover,
  .md-stuck-sweep:hover,
  .md-brief-close:hover {
    transform: translate(-2px, -2px);
    box-shadow: var(--shadow-stamp-md);
  }
}

.stamp-interactive:active,
.chip:active,
.tab-btn:active,
.login-close:active,
.mediagen-close:active,
.resource-modal-play:active,
.mediagen-btn:active,
.game-resource-btn:active,
.md-lead-btn:active,
.md-lead-stuck:active,
.md-stuck-sweep:active,
.md-brief-close:active {
  transform: translate(2px, 2px);
  box-shadow: var(--shadow-stamp-press);
}

@media (hover: hover) and (pointer: fine) {
  .stamp-interactive--lift-lg:hover,
  .skill-card:hover,
  .jf-face:hover,
  .jf-icon-btn:hover,
  .playing-card:hover:not(.sort-edit-card):not(.dragging):not(.selected):not(.matched):not(.memory-card-front, .instant-print) {
    transform: translate(-2px, -3px);
    box-shadow: var(--shadow-stamp-hover);
  }
}

.stamp-interactive--lift-lg:active,
.jf-face:active,
.jf-icon-btn:active {
  transform: translate(2px, 2px);
  box-shadow: var(--shadow-stamp-press);
}

/* ─── Touch ───────────────────────────────────────────────────────────────
   A finger gets the whole press, at once: the stamp travels its own offset
   and lies flat on the paper (a mouse keeps the half press above). Not on a
   disabled stamp, which lies flat already. */
@media (pointer: coarse) {
  .btn-stamp:active:not(:disabled, [aria-disabled="true"]),
  .stamp-interactive:active,
  .chip:active,
  .tab-btn:active,
  .login-close:active,
  .resource-modal-play:active,
  .game-resource-btn:active:not(:disabled),
  .stamp-interactive--lift-lg:active,
  .jf-face:active,
  .jf-icon-btn:active {
    transform: translate(var(--stamp-off-sm), var(--stamp-off-sm));
    box-shadow: 0 0 0 var(--ink-shadow);
    transition-duration: 0.06s;
  }

  .btn-stamp:not(.btn-stamp--sm):active:not(:disabled, [aria-disabled="true"]),
  .stamp-interactive--lift-lg:active {
    transform: translate(var(--stamp-off), var(--stamp-off));
  }

  /* Every control is at least --tap square to a finger. The small stamps
     grow to it; the round closes and plays too. Icons stay centred. The
     closes and plays are sized again further down this file and in
     game-chrome.css (32px), so this one is a (0,1,1) rule that a later
     plain class rule does not undo. */
  .btn-stamp--sm {
    min-height: var(--tap);
  }

  .btn-stamp--icon {
    min-width: var(--tap);
    justify-content: center;
  }

  button:is(.login-close, .mediagen-close, .resource-modal-play, .game-resource-btn) {
    width: var(--tap);
    height: var(--tap);
  }

  /* Small inline controls keep their look; a ::before takes the finger. */
  .id-chip-copy,
  .chip-remove,
  .field-help {
    position: relative;
  }

  .id-chip-copy::before,
  .chip-remove::before,
  .field-help::before {
    content: "";
    position: absolute;
    inset: -0.85rem;
  }
}

/* ─── Answer stamps — .stamp-answer ──────────────────────────────────────
   An answer control (a stamp button, a card button) says at the tap whether
   it was right, the way the metric-quest choice does: is-right — green takes
   the whole stamp, with a tick, held until the game moves on; is-wrong — red
   with a cross and a shake, cleared after a beat so the child tries again.
   The glyph is drawn here, so the accessible name stays the label. */

.stamp-answer.is-right,
.stamp-answer.is-wrong {
  color: var(--on-color);
  box-shadow: inset 0 0 0 3px var(--ink), var(--shadow-stamp-sm);
  transform: none;
}

.stamp-answer.is-right {
  background: var(--success);
}

.stamp-answer.is-wrong {
  background: var(--error);
  animation: stamp-answer-shake 0.4s ease;
}

.stamp-answer.is-right::after,
.stamp-answer.is-wrong::after {
  margin-left: 0.45rem;
  font-weight: 900;
}

.stamp-answer.is-right::after { content: "✓"; }
.stamp-answer.is-wrong::after { content: "✗"; }

/* Colour and shake only: the tick and the cross stay off the control. */
.stamp-answer--plain.is-right::after,
.stamp-answer--plain.is-wrong::after {
  content: none;
}

/* ─── Held answers — .answer-held ────────────────────────────────────────
   An answer control waiting out a reading. Flat, muted, not a button, until
   the clip ends. `holdAnswers()` sets `disabled` for the same stretch. */

.answer-held,
.answer-held:disabled {
  opacity: 0.45;
  filter: grayscale(0.55);
  box-shadow: none;
  cursor: not-allowed;
  transform: none;
  pointer-events: none;
}

.answer-held:hover,
.answer-held:disabled:hover,
.answer-held:active,
.answer-held:disabled:active {
  transform: none;
  box-shadow: none;
}

@keyframes stamp-answer-shake {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-6px); }
  75% { transform: translateX(6px); }
}

@media (prefers-reduced-motion: reduce) {
  .stamp-answer.is-wrong { animation: none; }
}

/* ─── Chips ──────────────────────────────────────────────────────────────── */

.chip {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  font-family: var(--font-body);
  font-weight: 700;
  font-size: var(--fs-body);
  padding: 0.55rem 0.95rem;
  border-radius: var(--r-full);
  background: var(--card);
  color: var(--ink);
  border: 1.5px solid var(--line-strong);
  box-shadow: var(--shadow-stamp-sm);
  transition: transform 0.18s ease, box-shadow 0.18s ease;
}

.chip-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--mint-deep); 
}

/* Compact, static chip for dense lists of picked items (no lift on hover). */
.chip--sm {
  gap: 0.35rem;
  padding: 0.15rem 0.2rem 0.15rem 0.5rem;
  font-size: var(--fs-label);
  line-height: 1.2;
  border-color: var(--ink);
  background: var(--paper);
  box-shadow: none;
  transition: none;
}
.chip--sm:hover, .chip--sm:active { transform: none; box-shadow: none; 
}
/* Marks a chip the system added (e.g. picked from a story), not the user. */
.chip--accent { border-color: var(--mint-deep); 
}
/* A target the chip's story is a thin source for (orange) or no source at
   all (coral) — see the KST fit scores on the batch proposal. */
.chip--weak-fit {
  border-color: var(--warning);
  background: var(--warning-mid);
  color: var(--warning-text);
}
.chip--misfit {
  border-color: var(--coral);
  background: var(--error-muted);
  color: var(--coral-deep);
}
.chip code { font-family: inherit; font-weight: 800; 
}
.chip-value { color: var(--mint-ink); font-weight: 800; 
}
/* × inside a chip; the chip's parent handles the click (event delegation). */
.chip-remove {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.15rem;
  height: 1.15rem;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--ink-muted);
  font: inherit;
  font-size: var(--fs-body);
  line-height: 1;
  cursor: pointer;
}
.chip-remove:focus-visible { background: var(--ink); color: var(--paper); outline: none;
}
@media (hover: hover) and (pointer: fine) {
  .chip-remove:hover { background: var(--ink); color: var(--paper); outline: none;
}
}

/* ─── ID chip (UUID + copy) ───────────────────────────────────────────────
   Compact identifier for generation jobs, tasks and journeys. Default
   display is the first 8 characters; the copy button always writes the
   full value from data-id. .id-chip--full shows the whole UUID.         */

.id-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.28rem;
  max-width: 100%;
  padding: 0.1rem 0.16rem 0.1rem 0.5rem;
  border: 1.5px solid var(--line-strong);
  border-radius: var(--r-full);
  background: var(--paper-deep);
  color: var(--ink);
  vertical-align: middle;
  line-height: 1;
}

.id-chip-value {
  margin: 0;
  padding: 0.12rem 0;
  font-family: var(--font-mono);
  font-size: var(--fs-label);
  font-weight: 700;
  letter-spacing: -0.02em;
  line-height: 1.3;
  color: var(--ink);
  background: none;
  white-space: nowrap;
  /* Meta lines (.note-item-meta) uppercase their content; a UUID must not be. */
  text-transform: none;
}

.id-chip--full .id-chip-value {
  white-space: normal;
  word-break: break-all;
}

.id-chip-copy {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex-shrink: 0;
  width: 1.55rem;
  height: 1.55rem;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: transparent;
  color: var(--ink-soft);
  cursor: pointer;
}

.id-chip-copy:focus-visible {
  background: var(--card);
  color: var(--ink);
  outline: none;
}

@media (hover: hover) and (pointer: fine) {
  .id-chip-copy:hover {
    background: var(--card);
    color: var(--ink);
    outline: none;
  }
}

.id-chip-copy:focus-visible {
  box-shadow: 0 0 0 3px var(--ink-focus);
}

.id-chip-copy svg {
  width: 13px;
  height: 13px;
}

.id-chip.is-copied {
  border-color: var(--mint-deep);
  background: var(--mint-muted);
}

.id-chip.is-copied .id-chip-copy {
  color: var(--mint-ink);
}

/* Chip on its own line below a title, in a table cell or a meta block. */
.id-chip-row {
  display: flex;
  min-width: 0;
  margin-top: 0.25rem;
}

/* ─── Tab bar ────────────────────────────────────────────────────────────── */

.tab-bar {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  margin-bottom: 1.5rem;
}

.tab-btn {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.55rem 1rem;
  border: 2px solid var(--ink);
  border-radius: var(--r-full);
  background: var(--card);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: var(--fs-label);
  font-weight: 800;
  cursor: pointer;
  box-shadow: var(--shadow-stamp-sm);
  transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.15s ease;
  text-decoration: none;
}

.tab-btn.is-active {
  background: var(--ink);
  color: var(--paper);
}

.tab-panel { display: none; 
}
.tab-panel.is-active { display: block; 
}

/* ─── Field help — .field-help ────────────────────────────────────────────
   Small "?" beside a field label. The explanation lives in data-help and
   pops up on hover/focus (CSS only, keyboard reachable). Stamp language:
   ink border + offset shadow, like .chip / .btn-stamp. */
.field-help {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.1rem;
  height: 1.1rem;
  margin-left: 0.35rem;
  padding: 0;
  border: 1.5px solid var(--ink-muted);
  border-radius: 50%;
  background: transparent;
  color: var(--ink-muted);
  font: inherit;
  font-size: var(--fs-caption);
  font-weight: 800;
  line-height: 1;
  letter-spacing: 0;
  text-transform: none;
  vertical-align: middle;
  cursor: help;
  position: relative;
}
.field-help:focus { color: var(--ink); border-color: var(--ink); outline: none;
}
@media (hover: hover) and (pointer: fine) {
  .field-help:hover { color: var(--ink); border-color: var(--ink); outline: none;
}
}
.field-help::after {
  content: attr(data-help);
  position: absolute;
  left: 50%;
  bottom: calc(100% + 0.5rem);
  transform: translateX(-50%);
  width: max-content;
  max-width: 18rem;
  padding: 0.5rem 0.65rem;
  z-index: 20;
  background: var(--card);
  color: var(--ink);
  border: 1.5px solid var(--ink);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-stamp-sm);
  font-size: var(--fs-label);
  font-weight: 600;
  line-height: 1.4;
  text-align: left;
  text-transform: none;
  letter-spacing: 0;
  white-space: normal;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.12s ease;
}
.field-help:focus::after { opacity: 1;
}
@media (hover: hover) and (pointer: fine) {
  .field-help:hover::after { opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) { .field-help::after { transition: none; 
} 
}

/* ─── Form fields ────────────────────────────────────────────────────────
   Base light field used by admin/parent/image-debug. The --on-dark variant
   covers the dark-section inputs on the landing page (the partner form). */

.field-label,
.eyebrow,
.mediagen-eyebrow {
  display: block;
  font-size: var(--fs-label);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--ink-muted);
  margin-bottom: 0.35rem;
}

.eyebrow--warning { color: var(--warning-text); }
.eyebrow--accent,
.mediagen-eyebrow { color: var(--coral); letter-spacing: 0.12em; }

.field-input,
.field-select,
.field-textarea {
  width: 100%;
  padding: 0.65rem 0.85rem;
  border: 1.5px solid var(--line-strong);
  border-radius: var(--r-md);
  background: var(--paper);
  font-family: var(--font-body);
  font-size: var(--fs-body);
  font-weight: 600;
  color: var(--ink);
  margin-bottom: 1rem;
}

.field-input:focus,
.field-select:focus,
.field-textarea:focus {
  outline: none;
  border-color: var(--ink);
  box-shadow: 0 0 0 3px var(--ink-focus);
}

/* Searchable dropdown. Same face as .field-select; the panel holds a search
   box and the options. Used by the generator's story picker and the task
   catalogue's journey filter. */
.story-combo {
  position: relative;
  margin-bottom: 1rem;
}

.story-combo-trigger {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.65rem 0.85rem;
  border: 1.5px solid var(--line-strong);
  border-radius: var(--r-md);
  background: var(--paper);
  font-family: var(--font-body);
  font-size: var(--fs-body);
  font-weight: 600;
  color: var(--ink);
  text-align: left;
  cursor: pointer;
}

.story-combo-trigger:focus {
  outline: none;
  border-color: var(--ink);
  box-shadow: 0 0 0 3px var(--ink-focus);
}

.story-combo.is-open .story-combo-trigger {
  border-bottom-left-radius: 0;
  border-bottom-right-radius: 0;
  box-shadow: none;
}

.story-combo-value {
  flex: 1;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.story-combo-caret {
  flex-shrink: 0;
  display: flex;
  opacity: 0.7;
  transition: transform 0.12s ease;
}

.story-combo.is-open .story-combo-caret { transform: rotate(180deg); }

.story-combo-panel {
  display: none;
  position: absolute;
  z-index: 30;
  left: 0;
  right: 0;
  top: 100%;
  margin-top: -1.5px;
  border: 1.5px solid var(--line-strong);
  border-top: 1px solid var(--line);
  border-radius: 0 0 var(--r-md) var(--r-md);
  background: var(--paper);
  box-shadow: var(--shadow-stamp-sm);
  padding: 0.4rem;
}

.story-combo.is-open .story-combo-panel { display: block; }

.story-combo-search {
  width: 100%;
  box-sizing: border-box;
  padding: 0.55rem 0.75rem;
  border: 0;
  border-bottom: 1px solid var(--line);
  font: inherit;
  font-size: var(--fs-body);
  background: var(--card);
  margin-bottom: 0.35rem !important;
}

.story-combo-search:focus { outline: none; }

.story-combo-list {
  list-style: none;
  margin: 0;
  padding: 0;
  max-height: 12rem;
  overflow: auto;
}

.story-combo-option {
  display: block;
  width: 100%;
  padding: 0.4rem 0.5rem;
  border: 0;
  border-radius: var(--r-xs);
  background: transparent;
  font: inherit;
  font-size: var(--fs-label);
  font-weight: 600;
  text-align: left;
  cursor: pointer;
  color: var(--ink);
}

.story-combo-option:focus-visible,
.story-combo-option.is-active {
  background: var(--paper-deep);
  outline: none;
}

@media (hover: hover) and (pointer: fine) {
  .story-combo-option:hover {
    background: var(--paper-deep);
    outline: none;
  }
}

.story-combo-option.is-selected {
  color: var(--ink);
  box-shadow: inset 2px 0 0 var(--ink);
}

.story-combo-empty {
  padding: 0.4rem 0.5rem;
  font-size: var(--fs-label);
  font-weight: 600;
  color: var(--ink-muted);
}

.field-textarea {
  min-height: 5rem;
  resize: vertical;
}

.field-hint {
  color: var(--ink-muted);
  font-size: var(--fs-label);
  margin: -0.5rem 0 1rem;
}

.field-on-dark .field-label {
  color: var(--paper-on-dark);
}

.field-input--on-dark {
  border-color: var(--paper);
  background: var(--paper-on-dark-soft);
  color: var(--on-color);
}

.field-input--sm,
.role-select,
.kst-textarea,
.sort-edit-input.field-input,
.sort-edit-input.field-select,
.sort-edit-textarea.field-textarea {
  padding: 0.35rem 0.55rem;
  font-size: var(--fs-label);
}

.field-input--on-dark::placeholder { color: var(--paper-on-dark-muted); 
}
.field-input--on-dark:focus { border-color: var(--sun); background: var(--paper-on-dark-mid); box-shadow: none; 
}

/* ─── Page hero (title + lead on admin/tool pages) ─────────────────────── */

.page-hero h1 {
  font-family: var(--font-display);
  font-size: clamp(1.6rem, 3.5vw, 2.2rem);
  margin: 0 0 0.25rem;
  color: var(--ink);
}

.page-hero p {
  margin: 0 0 1.25rem;
  color: var(--ink-muted);
}

/* Shared panel chrome. Width, layout and heading styles stay on each class. */
.panel,
.page-card,
.invite-card,
.pre-register-card,
.chart-card,
.gen-panel,
.debug-panel,
.stat-badge {
  background: var(--card);
  border: 1.5px solid var(--line-strong);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-stamp-sm);
  padding: 1.25rem 1.5rem;
}

/* Shared pill. Colour, border and case stay on the individual classes.
   .status-badge[data-status] is the stamped game-header variant and keeps
   its own padding. .tab-btn-badge is a count bubble and is not included. */
.badge,
.status-badge,
.event-badge,
.status-pill,
.tag-chip,
.step-chip,
.review-badge,
.default-badge,
.cache-chip,
.run-input-chip,
.sort-edit-hint-chip,
.topic-chip {
  display: inline-block;
  padding: 0.15rem 0.55rem;
  border-radius: var(--r-full);
  font-size: var(--fs-label);
  font-weight: 800;
  white-space: nowrap;
  line-height: 1.3;
}

.notice--danger,
.users-error {
  background: var(--error-muted);
  color: var(--coral-deep);
  border: 1.5px dashed var(--coral);
  border-radius: var(--r-md);
  padding: 0.75rem 1rem;
  font-size: var(--fs-body);
  font-weight: 700;
  margin-bottom: 1rem;
}

.notice--warning,
.precondition.is-warn,
.progress-header.is-failed {
  background: var(--warning-soft);
  border-color: var(--warning);
  color: var(--warning-text);
}

/* Task workflow badges (game header) — stamped variant keyed by data-status */
.status-badge[data-status] {
  display: inline-flex;
  align-items: center;
  padding: 0.32rem 0.75rem;
  border: 2px solid var(--ink);
  box-shadow: var(--shadow-stamp-sm);
  font-weight: 800;
  font-size: var(--fs-caption);
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--ink);
}

.status-badge[data-status="draft"] { background: var(--sun); color: var(--on-bright); 
}
.status-badge[data-status="reviewed"] { background: var(--mint); color: var(--on-bright); 
}
.status-badge[data-status="published"] { background: var(--success); color: var(--on-color); 
}
.status-badge[data-status="needs_correction"] { background: var(--coral); color: var(--on-color); 
}
.status-badge[data-status="review_rejected"] { background: var(--berry-bright); color: var(--on-color); 
}

.status-badge--success { background: var(--badge-success-bg); color: var(--badge-success-fg); 
}
.status-badge--warning { background: var(--badge-warning-bg); color: var(--badge-warning-fg); 
}
.status-badge--danger { background: var(--badge-danger-bg); color: var(--badge-danger-fg); 
}
.status-badge--neutral { background: var(--paper-deep); color: var(--ink-muted); 
}

/* Small inline status pills used after a name (admin/parent). */
.badge-prereg,
.badge-active {
  display: inline-block;
  padding: 0.12rem 0.5rem;
  border-radius: var(--r-full);
  font-size: var(--fs-caption);
  font-weight: 800;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  vertical-align: middle;
}

.badge-prereg { background: var(--badge-warning-bg); color: var(--badge-warning-fg);
}
.badge-active { background: var(--mint-active); color: var(--ink);
}

/* Staleness hint next to an error report's task title: the reported version
   is no longer the current one, so "Open task" lands on newer content. */
.error-report-version {
  color: var(--ink-muted);
  font-size: var(--fs-caption);
  white-space: nowrap;
}

/* Small count badge on a tab button (e.g. open error reports). */
.tab-btn-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 1.15rem;
  height: 1.15rem;
  padding: 0 0.35rem;
  border-radius: var(--r-full);
  background: var(--badge-danger-bg);
  color: var(--badge-danger-fg);
  font-size: var(--fs-caption);
  font-weight: 800;
}

/* ─── Feedback: spinner, toast, utilities ────────────────────────────────── */

.spinner {
  display: inline-block;
  width: 28px;
  height: 28px;
  border: 3px solid var(--line-strong);
  border-top-color: var(--coral);
  border-radius: 50%;
  animation: spin 0.7s linear infinite;
  margin-bottom: 0.75rem;
}

@keyframes spin { to { transform: rotate(360deg); 
} 
}

.users-toast {
  position: fixed;
  bottom: 1.5rem;
  right: 1.5rem;
  z-index: var(--z-toast);
  min-width: 220px;
  max-width: 360px;
  padding: 0.85rem 1rem;
  border: 2px solid var(--ink);
  border-radius: var(--r-md);
  box-shadow: var(--shadow-stamp);
  font-family: var(--font-body);
  font-size: var(--fs-body);
  font-weight: 800;
  color: var(--ink);
  background: var(--card);
  transform: translateY(120%);
  opacity: 0;
  transition: transform 0.25s ease, opacity 0.25s ease;
  pointer-events: none;
}

.users-toast.is-visible {
  transform: translateY(0);
  opacity: 1;
}

.users-toast.is-danger {
  background: var(--error-muted);
  border-color: var(--coral);
  color: var(--coral-deep);
}

.users-toast.is-success {
  background: var(--mint-muted);
  border-color: var(--mint);
  color: var(--ink);
}

.muted { color: var(--ink-muted); 
}
.is-hidden { display: none !important; 
}

/* ─── Empty states ───────────────────────────────────────────────────────
   Two variants that used to collide under one name: a dashed paper card
   (home/minigames) and quiet centered text (users/statistics). */

.empty-state--card {
  padding: 2rem 1.8rem;
  border-radius: var(--r-lg);
  border: 2.5px dashed var(--ink);
  background: var(--white-55);
  color: var(--ink);
  text-align: center;
  font-family: var(--font-body);
  font-weight: 600;
}

.empty-state--card strong {
  display: block;
  font-family: var(--font-display);
  font-size: var(--fs-title);
  margin-bottom: 0.35rem;
}

.empty-state--muted {
  text-align: center;
  padding: 3rem 1rem;
  color: var(--ink-muted);
  font-size: var(--fs-body);
}

/* ─── Overlays ─────────────────────────────────────────────────────────────
   One scrim. .celebration-overlay keeps a separate backdrop child, so it
   shares only the positioning. A modal that opens on top of another uses
   --z-overlay-top (journey feedback over the celebration). */

.overlay,
#loginOverlay,
#noteOverlay,
#notesListOverlay,
#inviteOverlay,
#avatarOverlay,
#profileModal,
#audioCheckOverlay,
#tourOverlay,
#unseededTasksOverlay,
#missingMediaOverlay,
#storyKstOverlay,
.mediagen-overlay,
.trace-overlay,
.journey-feedback-overlay,
#resourceOverlay,
.kst-overlay,
.launch-overlay {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
}

.overlay,
#loginOverlay,
#noteOverlay,
#notesListOverlay,
#inviteOverlay,
#avatarOverlay,
#profileModal,
#audioCheckOverlay,
#tourOverlay,
#unseededTasksOverlay,
#missingMediaOverlay,
#storyKstOverlay,
.mediagen-overlay,
.trace-overlay,
.journey-feedback-overlay,
#resourceOverlay {
  background:
    radial-gradient(circle at 30% 30%, var(--ink-scrim-soft), var(--ink-scrim-deep));
  backdrop-filter: blur(3px);
}

.overlay--flat,
.kst-overlay,
.launch-overlay {
  background: var(--ink-scrim);
  backdrop-filter: none;
}

.journey-feedback-overlay { z-index: var(--z-overlay-top); 
}

/* The celebration paints its scrim on a child, so the overlay itself stays clear. */
.celebration-overlay {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
}

/* ─── Login / note overlays ──────────────────────────────────────────────── */

/* Shared dialog chrome. Width, padding and motion stay on each card. */
.modal-card,
.login-card,
.mediagen-modal,
.kst-modal,
.celebration-card,
.story-modal-card,
.journey-modal-card,
.resource-modal-card,
.trace-modal,
.launch-card,
.cp-celebration-card {
  background: var(--card);
  border: 2.5px solid var(--ink);
  border-radius: var(--r-xl);
  box-shadow: var(--shadow-stamp);
}

.modal-card--deep,
.resource-modal-card { background: var(--paper-deep); 
}

.login-card {
  position: relative;
  width: min(92vw, 380px);
  padding: 1.9rem 1.6rem 1.6rem;
}

.modal-close--ghost,
.kst-modal-close,
.md-help-close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.5rem;
  height: 1.5rem;
  padding: 0;
  border: none;
  border-radius: 50%;
  background: transparent;
  color: var(--ink-muted);
  cursor: pointer;
  line-height: 1;
}

.login-close,
.mediagen-close {
  position: absolute;
  top: 0.7rem;
  right: 0.7rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--card);
  color: var(--ink);
  border: 2px solid var(--ink);
  box-shadow: var(--shadow-stamp-sm);
  cursor: pointer;
  transition: transform 0.18s ease, box-shadow 0.18s ease;
}

/* Mascot medallion straddling the card's top edge. Lives here, not in
   index.css, because every page that can open a login-card modal needs it —
   the audio check runs on the landing page too, which loads no index.css. */
.login-mascot {
  width: 70px;
  height: 70px;
  margin: -3.2rem auto 0.6rem;
  border-radius: 50%;
  background: var(--sun);
  border: 2.5px solid var(--ink);
  display: flex;
  align-items: center;
  justify-content: center;
  box-shadow: var(--shadow-stamp-sm);
}

.login-mascot svg { width: 42px; height: 42px; color: var(--on-bright); 
}

/* The medallion holding one of the Platy drawings with the dark backdrop baked
   in (platy-hi, platy-welcome) instead of the inline SVG: ink ring, picture
   clipped to it. --wave is the close crop of the waving Platy (head and
   raised hand fill the ring) the parents' letter and the children's tour share. */
.login-mascot--ink {
  background: var(--ink);
  overflow: hidden;
  display: block;   /* not the medallion's centring flex: the crop is anchored top-left */
}

.login-mascot--ink img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.login-mascot--wave img {
  width: 165%;
  height: 165%;
  max-width: none;
  margin: -3% 0 0 -17%;
}

/* confirmDialog (ui.js): a question before something that cannot be undone. */
.confirm-medallion {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  height: 52px;
  margin: 0 auto 0.6rem;
  border-radius: 50%;
  background: var(--warning-soft);
  color: var(--warning-text);
}

.confirm-medallion--calm {
  background: var(--mint-active);
  color: var(--ink);
}

.confirm-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.6rem;
}

.login-card h2 {
  font-family: var(--font-display);
  font-variation-settings: "SOFT" 40, "WONK" 1;
  font-weight: 600;
  font-size: var(--fs-display);
  line-height: 1;
  text-align: center;
  margin: 0 0 0.35rem;
  letter-spacing: -0.02em;
}

.login-card p {
  text-align: center;
  font-size: var(--fs-body);
  color: var(--ink-soft);
  margin: 0 0 1.2rem;
}

/* Fine print closing a login card (the privacy notice under the SSO buttons).
   One quiet line: still clear of the 0.6rem that pairs the two provider
   buttons, and with no paragraph bottom margin, so the card's padding alone
   finishes the card — otherwise the two stack into a dead band under the text.
   Scoped to the card because a bare .login-legal loses to .login-card p. */
.login-card .login-legal {
  margin: 1rem 0 0;
  font-size: var(--fs-label);
  line-height: 1.45;
  color: var(--ink-muted);
  text-wrap: balance;
}

/* Fine print needs less room beneath it than a card ending in a button or a
   field: matching the note's own top margin puts equal air on both sides of
   it. Browsers without :has() keep the roomier default. */
.login-card:has(> .login-legal:last-child) {
  padding-bottom: 1rem;
}

/* Kept whole, so the sentence breaks before the link rather than through it. */
.login-card .login-legal a {
  color: var(--coral-deep);
  white-space: nowrap;
}

/* A finger needs the link --tap high: padding grows its box, the negative
   margin keeps the line where it was. */
@media (pointer: coarse) {
  .login-card .login-legal a {
    display: inline-block;
    padding-block: calc((var(--tap) - 1.45em) / 2);
    margin-block: calc((var(--tap) - 1.45em) / -2);
  }
}

/* Coral dashed alert. The login flash and the audio check's warning note wear
   the same skin; only .login-error stays hidden until it has something to say,
   and only it is centred — the note is a paragraph of advice, so it keeps the
   left-aligned body it gets from .audio-check-note. */
.login-error,
.audio-check-note.is-warn {
  background: var(--error-muted);
  color: var(--coral-deep);
  border: 1.5px dashed var(--coral);
  border-radius: var(--r-md);
  font-size: var(--fs-label);
  font-weight: 700;
}

.login-error {
  display: none;
  padding: 0.5rem 0.8rem;
  margin-bottom: 0.6rem;
  text-align: center;
}

.login-error.is-visible { display: block; 
}

.login-success {
  display: none;
  background: var(--mint-muted);
  color: var(--mint);
  border: 1.5px solid var(--mint);
  border-radius: var(--r-md);
  padding: 0.75rem 0.85rem;
  font-size: var(--fs-body);
  font-weight: 700;
  margin-bottom: 0.6rem;
  text-align: center;
}

.login-success.is-visible { display: block; 
}

.note-textarea {
  width: 100%;
  box-sizing: border-box;
  margin-bottom: 0.8rem;
  padding: 0.75rem 0.85rem;
  border: 2px solid var(--ink);
  border-radius: var(--r-md);
  font-family: var(--font-body);
  font-size: var(--fs-body);
  line-height: 1.45;
  resize: vertical;
  min-height: 6rem;
  background: var(--white-85);
}

.note-textarea:focus {
  outline: none;
  box-shadow: var(--shadow-stamp-sm);
}

.notes-card { text-align: left; 
}

.notes-list {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
  max-height: 60vh;
  max-height: 60dvh;
  overflow-y: auto;
  margin-top: 0.5rem;
}

.notes-empty {
  text-align: center;
  color: var(--ink-soft);
  font-size: var(--fs-body);
  padding: 1rem 0;
}

.note-item {
  border: 2px solid var(--ink);
  border-radius: var(--r-md);
  padding: 0.6rem 0.7rem;
  background: var(--white-85);
}

.note-item[data-status="resolved"] {
  opacity: 0.7;
  background: var(--black-04);
}

.note-item-meta {
  font-size: var(--fs-caption);
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--ink-soft);
  margin-bottom: 0.3rem;
}

.note-item-message {
  margin: 0 0 0.5rem;
  font-size: var(--fs-body);
  line-height: 1.4;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
}

.note-item-action { cursor: pointer; 
}

/* ─── Hero mascot + floaters (home / landing) ────────────────────────────── */

.mascot-stage {
  position: relative;
  aspect-ratio: 1 / 1;
  max-width: 520px;
  width: 100%;
  margin-left: auto;
  display: flex;
  align-items: center;
  justify-content: center;
  animation: hero-rise 0.8s cubic-bezier(0.22, 1, 0.36, 1) 0.12s both;
}

.mascot-bubble {
  position: absolute;
  inset: 4%;
  background:
    radial-gradient(circle at 30% 25%, var(--sun-light) 0%, var(--sun) 60%, var(--sun-hot) 100%);
  border: 3px solid var(--ink);
  border-radius: 50%;
  box-shadow: var(--shadow-stamp), inset 0 -10px 0 var(--ink-wash);
  overflow: hidden;
}

.mascot-bubble::after {
  content: "";
  position: absolute;
  top: 12%;
  left: 18%;
  width: 28%;
  height: 14%;
  background: var(--white-70);
  border-radius: 50%;
  filter: blur(6px);
  transform: rotate(-20deg);
}

.floater {
  position: absolute;
  background: var(--card);
  border: 2.5px solid var(--ink);
  border-radius: var(--r-md);
  padding: 0.6rem 0.85rem;
  font-family: var(--font-body);
  font-weight: 800;
  font-size: var(--fs-body);
  color: var(--ink);
  box-shadow: var(--shadow-stamp-sm);
  white-space: nowrap;
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
}

.floater svg { width: 18px; height: 18px; 
}
.floater-badge-img { width: 24px; height: 24px; object-fit: contain; 
}

.floater--a { top: 8%; left: 10%; transform: rotate(-8deg); background: var(--mint); color: var(--on-bright); animation: tilt-a 5s ease-in-out infinite; 
}
.floater--b { top: 38%; right: -8%; transform: rotate(6deg); background: var(--coral); color: var(--on-color); animation: tilt-b 6.5s ease-in-out infinite; 
}
.floater--c { bottom: 6%; left: 8%; transform: rotate(-4deg); background: var(--on-color); animation: tilt-c 5.5s ease-in-out infinite; 
}

@keyframes hero-rise {
  from { opacity: 0; transform: translateY(22px); 
}
  to   { opacity: 1; transform: translateY(0); 
}
}

@keyframes mascot-float {
  0%, 100% { transform: translateY(0) rotate(-1deg); 
}
  50%      { transform: translateY(-14px) rotate(1.5deg); 
}
}

@keyframes tilt-a { 0%, 100% { transform: rotate(-8deg) translateY(0); 
} 50% { transform: rotate(-5deg) translateY(-8px); 
} 
}
@keyframes tilt-b { 0%, 100% { transform: rotate(6deg) translateY(0); 
} 50% { transform: rotate(3deg) translateY(-10px); 
} 
}
@keyframes tilt-c { 0%, 100% { transform: rotate(-4deg) translateY(0); 
} 50% { transform: rotate(-2deg) translateY(-6px); 
} 
}

.hero-copy > * {
  animation: hero-rise 0.68s cubic-bezier(0.22, 1, 0.36, 1) both;
}
.hero-copy > *:nth-child(1) { animation-delay: 0.05s; 
}
.hero-copy > *:nth-child(2) { animation-delay: 0.16s; 
}
.hero-copy > *:nth-child(3) { animation-delay: 0.27s; 
}
.hero-copy > *:nth-child(4) { animation-delay: 0.38s; 
}
.hero-copy > *:nth-child(5) { animation-delay: 0.49s; 
}

@media (prefers-reduced-motion: reduce) {
  /* Mascot targets live in landing.css; listed here for global reduced-motion. */
  .mascot-img,
  .floater--a,
  .floater--b,
  .floater--c,
  .hero-copy > *,
  .mascot-stage { animation: none !important; opacity: 1 !important; transform: none !important; 
}
}

/* ─── Audio check (first-visit speaker + microphone test) ─────────────────
   Opened on demand by js/audio-check.js inside the login-card modal. One lamp
   does both jobs: it glows while Platy speaks, then it listens — the glow
   follows the live microphone level (--level, 0..1) and snaps to green when a
   voice clears the noise floor. Everything else on the card stays quiet.    */

.audio-check {
  width: min(92vw, 420px);
  padding-bottom: 1.25rem;   /* the skip link is the last, quiet row — no deep bottom margin */
  text-align: center;        /* everything sits on the lamp's axis, the skip link included */
}
.audio-check:focus { outline: none; 
}   /* the card takes initial focus (tabindex=-1) */

/* The medallion itself is .login-mascot; this only teaches it to hold a
   picture of Platy instead of the inline SVG the login card puts there. */
.audio-check-mascot { overflow: hidden; 
}

.audio-check-mascot img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* The celebrating Platy throws his hands into the top corners of a square
   drawing — exactly the corners a round medallion crops away. Only that pose
   sits smaller and a touch lower, which brings both hands back inside the ring
   while the body still rests on the bottom edge. */
.audio-check-mascot.is-celebrating img {
  width: 82%;
  height: 82%;
  transform: translateY(3px);
}

/* Platy's line to repeat — the one loud typographic moment on the card. */
.audio-check-line {
  display: block;
  margin: 0.15rem 0 0.35rem;
  font-family: var(--font-hand);
  font-size: var(--fs-display);
  font-weight: 700;
  line-height: 1.1;
  color: var(--coral-deep);
}

/* Two real steps, so two lamps: grey ahead, sun now, green passed, coral
   failed, dashed skipped. */
.audio-check-steps {
  list-style: none;
  display: flex;
  justify-content: center;
  gap: 1.4rem;
  margin: 0 0 1.1rem;
  padding: 0;
  font-size: var(--fs-label);
  font-weight: 800;
  color: var(--ink-muted);
}

.audio-check-steps li {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
}

/* Same ring as .card-status-dot: 12px, 2px ink, colour carries the state. */
.audio-check-steps li::before {
  content: "";
  width: 12px;
  height: 12px;
  border-radius: 50%;
  border: 2px solid var(--ink);
  background: var(--paper-deep);
  transition: background 0.3s ease;
}

.audio-check-steps li.is-current { color: var(--ink); 
}
.audio-check-steps li.is-current::before { background: var(--sun); 
}
.audio-check-steps li.is-ok::before { background: var(--success); 
}
.audio-check-steps li.is-fail::before { background: var(--coral); 
}
.audio-check-steps li.is-skip::before { background: transparent; border-style: dashed; 
}

.audio-check-lamp {
  --level: 0;
  position: relative;
  display: grid;
  place-items: center;
  width: 128px;
  height: 128px;
  margin: 0 auto 0.8rem;
  padding: 0;
  border-radius: 50%;
  border: 2.5px solid var(--ink);
  background: var(--paper-deep);
  color: var(--ink);
  box-shadow: var(--shadow-stamp);
  cursor: pointer;
  transition: transform 0.15s ease, box-shadow 0.15s ease, background 0.3s ease, color 0.3s ease;
}

/* Lift/press comes from .stamp-interactive--lift-lg on the element — the same
   ramp .btn-stamp uses, so the lamp presses like every other stamp. */
.audio-check-lamp:focus-visible { outline: 3px solid var(--sky-deep); outline-offset: 4px; 
}

/* While it only reports the microphone level the lamp is not a button, so it
   must not lift under the cursor either. */
.audio-check-lamp[disabled] { cursor: default; 
}
.audio-check-lamp[disabled]:hover,
.audio-check-lamp[disabled]:active { transform: none; box-shadow: var(--shadow-stamp); 
}

.audio-check-lamp-glow {
  position: absolute;
  inset: 10px;
  border-radius: 50%;
  background: var(--sun);
  opacity: 0;
  transform: scale(calc(0.35 + var(--level) * 0.65));
  transition: opacity 0.25s ease, transform 0.08s linear;
}

.audio-check-lamp-icon { position: relative; z-index: 1; display: inline-flex; 
}
.audio-check-lamp-icon svg { width: 46px; height: 46px; 
}

.audio-check-lamp.is-emitting,
.audio-check-lamp.is-listening { background: var(--sun-light); 
}

.audio-check-lamp.is-emitting .audio-check-lamp-glow {
  opacity: 1;
  animation: audio-check-breathe 1.1s ease-in-out infinite;
}

.audio-check-lamp.is-listening .audio-check-lamp-glow { opacity: 0.9; 
}

.audio-check-lamp.is-ok { background: var(--success); color: var(--on-color); 
}
.audio-check-lamp.is-ok .audio-check-lamp-glow { opacity: 0; 
}
.audio-check-lamp.is-fail { background: var(--error-muted); color: var(--coral-deep); 
}

@keyframes audio-check-breathe {
  0%, 100% { transform: scale(0.55); 
}
  50%      { transform: scale(0.95); 
}
}

/* .login-card p sets the modal's body text; the caption is the lamp's own
   one-line voice, so it speaks up a little. */
.login-card .audio-check-caption {
  min-height: 1.5em;
  margin: 0 0 0.6rem;
  font-size: var(--fs-body);
  font-weight: 700;
  color: var(--ink);
}

.audio-check-note {
  margin: 0 0 0.9rem;
  padding: 0.65rem 0.85rem;
  border: 1.5px dashed var(--line-strong);
  border-radius: var(--r-md);
  background: var(--white-55);
  font-size: var(--fs-label);
  font-weight: 600;
  line-height: 1.4;
  color: var(--ink-soft);
  text-align: left;
}

/* .audio-check-note.is-warn swaps this quiet skin for the coral dashed alert
   it shares with .login-error — see "Login / note overlays" above. */

.audio-check-summary {
  list-style: none;
  display: grid;
  gap: 0.45rem;
  margin: 0;
  padding: 0;
  color: var(--ink);
}

.audio-check-summary li {
  display: flex;
  align-items: center;
  gap: 0.55rem;
  font-size: var(--fs-body);
  font-weight: 800;
}

.audio-check-summary li::before {
  content: "";
  flex-shrink: 0;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  border: 2px solid var(--ink);
  background: var(--coral);
}

.audio-check-summary li[data-result="ok"]::before { background: var(--success); 
}
.audio-check-summary li[data-result="skip"]::before { background: transparent; border-style: dashed; 
}
.audio-check-summary li span { margin-left: auto; font-weight: 600; color: var(--ink-muted); 
}

.audio-check-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.6rem;
  margin-bottom: 0.6rem;
}

.audio-check-actions:empty { display: none; 
}
.audio-check-actions .btn-stamp { padding: 0.75rem 1.3rem; font-size: var(--fs-body); 
}

/* Quiet second thought under the buttons ("didn't hear it? press again"). */
.login-card .audio-check-hint {
  margin: 0 0 0.55rem;
  text-wrap: balance;   /* on a phone it wraps; two even lines beat a lone word */
  font-size: var(--fs-label);
  font-weight: 600;
  color: var(--ink-muted);
}

/* Shared with the children's tour (.tour-skip): same quiet last row. */
.audio-check-skip,
.tour-skip {
  appearance: none;
  border: 0;
  background: none;
  padding: 0.2rem 0.5rem;
  font-family: var(--font-body);
  font-size: var(--fs-label);
  font-weight: 700;
  color: var(--ink-muted);
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}

@media (hover: hover) and (pointer: fine) {
  .audio-check-skip:hover,
  .tour-skip:hover { color: var(--ink);
}
}

/* A quiet link to look at, a full row to a finger. */
@media (pointer: coarse) {
  .audio-check-skip,
  .tour-skip {
    min-height: var(--tap);
    padding-inline: 0.85rem;
  }
}

/* These two set their own display, which would beat the UA's [hidden] rule;
   the summary step hides both. */
.audio-check-lamp[hidden],
.audio-check-steps[hidden] { display: none; 
}

@media (prefers-reduced-motion: reduce) {
  .audio-check-lamp.is-emitting .audio-check-lamp-glow { animation: none; transform: scale(0.8); 
}
  .audio-check-lamp-glow { transition: opacity 0.25s ease; 
}
}

/* ─── Bemutató (children's first-visit tour) ──────────────────────────────
   Opened by js/tour.js inside the login-card modal on the game list: seven
   short cards Platy flips through. The skeleton is the audio check's (ink
   medallion, stamp-ring steps, quiet skip link); what is new is the exhibit —
   a non-interactive copy of the real control the card talks about, so the
   child learns the button by sight before meeting it in a game.            */

/* On small phones a card can be taller than the viewport: let the overlay
   scroll instead of clipping the card at both ends. The card's auto margins
   centre it while it fits and collapse to zero when it does not, so the top
   stays reachable — unlike `align-items: safe center`, this degrades the
   same way in every browser. The padding keeps the medallion's overhang on
   screen at scroll top. Shared with the profiles.html welcome letter. */
#tourOverlay,
#profileModal {
  overflow-y: auto;
  padding: 3.5rem 0;
  box-sizing: border-box;
}

.tour {
  width: min(92vw, 420px);
  margin: auto 0;
  padding-bottom: 1.25rem;   /* the skip link is the last, quiet row */
  text-align: center;
  touch-action: pan-y;       /* horizontal swipes flip the card; vertical still scrolls */
}
.tour:focus { outline: none; 
}   /* the card takes initial focus (tabindex=-1) */

/* A size between the login card's 70px and the parents' 120px letter; the
   ring stays ink on every card (see .login-mascot--ink). */
.tour-mascot {
  width: 96px;
  height: 96px;
  margin-top: -3.9rem;
}

/* Seven cards, seven rings — the audio check's step lamps without labels.
   Grey ahead, sun now, mint read. The read ones are live buttons (going
   back), the rest are disabled and only mark the way. The ring is 14px but
   the button around it is 32px: a child's finger on a tablet needs the room. */
.tour-steps {
  list-style: none;
  display: flex;
  justify-content: center;
  gap: 0.1rem;
  margin: 0 0 0.8rem;
  padding: 0;
}

.tour-steps button {
  display: grid;
  place-items: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  cursor: default;
}

.tour-steps button::before {
  content: "";
  width: 14px;
  height: 14px;
  border-radius: 50%;
  border: 2px solid var(--ink);
  background: var(--paper-deep);
  transition: background 0.3s ease, transform 0.15s ease;
}

.tour-steps .is-current button::before { background: var(--sun); 
}
.tour-steps .is-done button::before { background: var(--mint); 
}
.tour-steps button:not(:disabled) { cursor: pointer; 
}
@media (hover: hover) and (pointer: fine) {
  .tour-steps button:not(:disabled):hover::before { transform: translateY(-2px);
}
}
.tour-steps button:focus-visible { outline: 3px solid var(--sky-deep); outline-offset: -3px; 
}

/* A finger: seven of them share a phone-wide row, each a full --tap square
   (7 × 44px fit a 375px sheet). After the base rule, which would win
   otherwise. A row narrower than seven of them lets each give way rather
   than leave the card. */
@media (pointer: coarse) {
  .tour-steps li {
    min-width: 0;
  }

  .tour-steps button {
    width: var(--tap);
    max-width: 100%;
    height: var(--tap);
  }
}

/* All seven cards share one grid cell; js/tour.js sets the deck's height to
   the active card's, so a short card is not padded out to the tallest one and
   the button below glides rather than jumps between flips. The inactive ones
   stay laid out (clipped) but invisible — visibility also takes them out of
   the accessibility tree and the tab order. */
.tour-deck {
  display: grid;
  overflow: hidden;
  transition: height 0.24s ease;
}

.tour-card {
  --tour-dx: 18px;
  grid-area: 1 / 1;
  align-self: start;
  visibility: hidden;
}

.tour-card.is-active { visibility: visible; 
}
.tour-card.is-active[data-dir="next"],
.tour-card.is-active[data-dir="prev"] { animation: tour-card-in 0.24s ease-out both; 
}
.tour-card.is-active[data-dir="prev"] { --tour-dx: -18px; 
}

@keyframes tour-card-in {
  from { opacity: 0; transform: translateX(var(--tour-dx)); 
}
  to   { opacity: 1; transform: none; 
}
}

.tour-card h2 { margin-bottom: 0.7rem; text-wrap: balance; 
}
.tour-card h2:focus { outline: none; 
}   /* focus moves here on a flip for the announcement only */

/* Bigger and left-aligned than the login card's one-liner: this is read by a
   child, two or three sentences at a time. Control names are set in ink so
   the eye can match them to the exhibit below. */
.login-card .tour-card p {
  text-align: left;
  font-size: 1rem;
  line-height: 1.5;
  color: var(--ink-soft);
  margin: 0 0 0.7rem;
}

.tour-card b { color: var(--ink); font-weight: 800; 
}

/* The exhibit: a dashed tray (the audio check's note skin) holding copies of
   the real controls. Nothing in it reacts to the pointer — it is a picture. */
.tour-exhibit {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: center;
  gap: 0.7rem 0.9rem;
  margin: 0.3rem 0 1rem;
  padding: 0.9rem 0.8rem;
  border: 1.5px dashed var(--line-strong);
  border-radius: var(--r-md);
  background: var(--white-55);
  pointer-events: none;
  user-select: none;
}

/* Copies of the game shell's pills (.game-topbar-back, .game-panel .btn,
   .game-error-report-btn): same padding, ring, stamp shadow and weight. */
.tour-pill {
  display: inline-flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.6rem 1.1rem;
  border-radius: var(--r-full);
  border: 2px solid var(--ink);
  background: var(--card);
  box-shadow: var(--shadow-stamp-sm);
  font-family: var(--font-body);
  font-weight: 800;
  font-size: var(--fs-body);
  color: var(--ink);
  white-space: nowrap;
}

.tour-pill--icon { padding: 0.6rem 0.75rem; 
}
.tour-pill--mint { background: var(--mint); color: var(--on-bright); 
}
.tour-pill--sun { background: var(--sun); color: var(--on-bright); box-shadow: var(--shadow-stamp); 
}
.tour-pill svg { flex-shrink: 0; 
}

/* The lightbulb the playing cards carry (.speaker-btn.sort-card-hint): a
   borderless white disc, as it sits over a card's picture. */
.tour-bulb {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 36px;
  height: 36px;
  border-radius: 50%;
  background: var(--white-92);
  box-shadow: 0 2px 6px var(--black-18);
  font-size: 18px;
  line-height: 1;
}

/* Its bulb is the hint buttons' drawn one (lib/icons.js BULB_ICON). */
.tour-bulb .speaker-icon {
  display: block;
  width: 22px;
  height: 22px;
}

/* Platy's two reactions on the task-end answers (feedback.js TASK_VOTES), on
   the celebration card and on the tour's copy of them. */
.vote-face {
  display: block;
  width: 3rem;
  height: 3rem;
  object-fit: contain;
  flex-shrink: 0;
  pointer-events: none;
}

/* only reached when the media bucket has no faces and the emoji stands in */
.vote-face--fallback {
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 2rem;
  line-height: 1;
}

/* The task-end answers (.celebration-vote) as the child will meet them,
   before one is chosen. */
.tour-vote {
  width: 7rem;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.3rem;
  padding: 0.55rem 0.7rem 0.5rem;
  border: 2px solid var(--ink);
  border-radius: var(--r-md);
  background: var(--card);
  color: var(--ink);
  box-shadow: var(--shadow-stamp-sm);
  font-size: var(--fs-label);   /* 13px: the smallest the player surface goes */
  font-weight: 800;
  line-height: 1.15;
  white-space: nowrap;
}

/* Three of the five effort faces (.jf-face) with their end labels: the two
   edges and the "just right" middle the copy names. */
.tour-face {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.4rem;
  font-size: var(--fs-label);   /* 13px: the smallest the player surface goes */
  font-weight: 800;
  color: var(--ink-soft);
}

.tour-face i {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 3.2rem;
  height: 3.2rem;
  border-radius: 50%;
  border: 2px solid var(--ink);
  box-shadow: var(--shadow-stamp-sm);
}

.tour-face i img { width: 90%; height: 90%; object-fit: contain; 
}
.tour-face--edge i { background: var(--badge-danger-bg); 
}
.tour-face--best i { background: var(--mint-surface); 
}
.tour-face--best { color: var(--ink); 
}

.tour-actions {
  display: flex;
  justify-content: center;
  margin-bottom: 0.6rem;
}

/* Wide enough that "Mehet!" and "Játékra fel!" are one button size; the
   stamp is inline-flex, so the label needs centring inside that width. */
.tour-actions .btn-stamp {
  min-width: 11rem;
  justify-content: center;
  padding: 0.85rem 1.6rem;
  font-size: 1rem;
}

@media (prefers-reduced-motion: reduce) {
  .tour-deck { transition: none; 
}
  .tour-card.is-active { animation: none; 
}
  .tour-steps button:not(:disabled):hover::before { transform: none; 
}
}

/* ─── Phone layer: bottom sheets and form fields ─────────────────────────
   On a phone every modal card docks to the bottom of the screen, where the
   thumb is: full width, the top corners rounded, the ink line on the top and
   the sides, never taller than 92% of what the browser shows, scrolling
   inside itself. The scrim, the close button and the card's own content stay
   as they are on the desktop. A row of actions at the card's foot sticks to
   its bottom edge, clear of the home indicator. Ids carry the specificity a
   page sheet would otherwise beat. Behaviour (focus, Esc, the back gesture)
   is ui.js `openSheet`. */
@media (max-width: 767.98px), (max-height: 500px) and (pointer: coarse) {
  :is(.overlay, #loginOverlay, #noteOverlay, #notesListOverlay, #inviteOverlay,
      #avatarOverlay, #profileModal, #audioCheckOverlay, #tourOverlay,
      #resourceOverlay, .journey-feedback-overlay) {
    align-items: flex-end;
    justify-content: center;
    padding: var(--safe-t) 0 0;
    overflow: hidden;
  }

  /* The scrim keeps its tone but not the blur: a blurred backdrop over the
     whole screen is drawn again on every frame the sheet slides, scrolls
     or the page under it moves (as .overlay--flat). */
  :is(.overlay, #loginOverlay, #noteOverlay, #notesListOverlay, #inviteOverlay,
      #avatarOverlay, #profileModal, #audioCheckOverlay, #tourOverlay,
      #unseededTasksOverlay, #missingMediaOverlay, #storyKstOverlay,
      .mediagen-overlay, .trace-overlay, #resourceOverlay,
      .journey-feedback-overlay) {
    backdrop-filter: none;
  }

  :is(.overlay, #loginOverlay, #noteOverlay, #notesListOverlay, #inviteOverlay,
      #avatarOverlay, #profileModal, #audioCheckOverlay, #tourOverlay,
      #resourceOverlay, .journey-feedback-overlay)
    > :is(.login-card, .modal-card, .resource-modal-card, .celebration-card) {
    box-sizing: border-box;
    width: 100%;
    max-width: none;
    max-height: 92vh;
    max-height: 92dvh;
    margin: 0;
    overflow-y: auto;
    overscroll-behavior: contain;
    border-bottom: 0;
    border-radius: var(--r-xl) var(--r-xl) 0 0;
    box-shadow: none;
    padding-left: max(1.25rem, var(--safe-l));
    padding-right: max(1.25rem, var(--safe-r));
    padding-bottom: calc(1.25rem + var(--safe-b));
    /* Backwards only: once in, the card's transform is free again for a
       pull that takes it down with the finger (ui.js bindSheetSwipe). */
    animation: sheet-up 0.24s cubic-bezier(0.2, 0.85, 0.25, 1) backwards;
  }

  /* The scrim comes in with the card (opacity), and goes with it
     (ui.js slideSheetOut). */
  :is(.overlay, #loginOverlay, #noteOverlay, #notesListOverlay, #inviteOverlay,
      #avatarOverlay, #profileModal, #audioCheckOverlay, #tourOverlay,
      #resourceOverlay, .journey-feedback-overlay) {
    animation: sheet-scrim-in 0.2s ease-out backwards;
  }

  /* The grabber (ui.js openSheet puts it in every phone sheet): a pill at
     the card's top edge, where a pull down closes the sheet. Out of the
     flow: it changes no card's layout. The card is what it stands in. */
  :is(.overlay, #loginOverlay, #noteOverlay, #notesListOverlay, #inviteOverlay,
      #avatarOverlay, #profileModal, #audioCheckOverlay, #tourOverlay,
      #resourceOverlay, .journey-feedback-overlay)
    > :is(.login-card, .modal-card, .resource-modal-card, .celebration-card):has(> .sheet-grabber) {
    position: relative;
  }

  .sheet-grabber {
    display: block;
    position: absolute;
    z-index: 3;
    top: 8px;
    left: calc(50% - 1.5rem);
    width: 3rem;
    height: 5px;
    border-radius: var(--r-full);
    background: var(--ink-muted);
    opacity: 0.55;
    pointer-events: none;
  }

  /* The resource text scrolls in its own column; the card only frames it. */
  #resourceOverlay > .resource-modal-card {
    overflow: hidden;
    padding: 0 0 var(--safe-b);
  }

  /* The medallion would be cut off at the scrolling card's top edge: on a
     sheet it sits inside the card, a little smaller. */
  :is(.login-card, .modal-card) > .login-mascot {
    width: 60px;
    height: 60px;
    margin-top: -0.4rem;
  }

  :is(.login-card, .modal-card) > :is(.tour-mascot, .welcome-mascot) {
    width: 76px;
    height: 76px;
    margin-top: -0.4rem;
  }

  /* The card's foot: the answer row stays on screen however the card
     scrolls, on the card's paper, across its full width. Ending the card, it
     takes over the card's bottom padding (and the home indicator's). */
  :is(.login-card, .modal-card, .celebration-card)
    > :is(.confirm-actions, .tour-actions, .audio-check-actions, .jf-actions, .profile-modal-actions) {
    position: sticky;
    bottom: 0;
    z-index: 1;
    margin-inline: calc(-1 * max(1.25rem, var(--safe-l))) calc(-1 * max(1.25rem, var(--safe-r)));
    padding: 0.75rem max(1.25rem, var(--safe-r)) 0.75rem max(1.25rem, var(--safe-l));
    background: var(--card);
  }

  :is(.login-card, .modal-card, .celebration-card)
    > :is(.confirm-actions, .tour-actions, .audio-check-actions, .jf-actions, .profile-modal-actions):last-child {
    padding-bottom: calc(1rem + var(--safe-b));
  }

  /* The foot's top edge: a rule while the card's content runs on under it,
     so a line or a field cut at the foot reads as "more below", not as
     broken. Where a style can follow a scroll position (scroll-driven
     animations: Safari 26, Chrome 115) the rule shows only while there is
     more to scroll: none on a card that fits, gone at the card's end. An
     older browser always shows it, a quiet divider. The tour's own pinned
     foot (index.css .tour-foot) takes the same edge. */
  :is(.login-card, .modal-card, .celebration-card)
    > :is(.confirm-actions, .tour-actions, .audio-check-actions, .jf-actions, .profile-modal-actions),
  .tour-foot {
    box-shadow: 0 -2px 0 var(--line-strong);
  }

  @supports (animation-timeline: scroll()) {
    :is(.login-card, .modal-card, .celebration-card)
      > :is(.confirm-actions, .tour-actions, .audio-check-actions, .jf-actions, .profile-modal-actions),
    .tour-foot {
      box-shadow: none;
      animation: sheet-foot-edge linear both;
      animation-timeline: scroll(nearest block);
    }
  }

  :is(#loginOverlay, #noteOverlay, #notesListOverlay, #inviteOverlay, #avatarOverlay,
      #profileModal, #audioCheckOverlay, #tourOverlay, .overlay, .journey-feedback-overlay)
    > :is(.login-card, .modal-card, .celebration-card):has(> :is(.confirm-actions, .tour-actions, .audio-check-actions, .jf-actions, .profile-modal-actions):last-child) {
    padding-bottom: 0;
  }

  .login-close,
  .mediagen-close {
    width: var(--tap);
    height: var(--tap);
  }

  /* 16px or more: iOS zooms the page into any field smaller than that. */
  .field-input,
  .field-select,
  .field-textarea,
  .note-textarea,
  .story-combo-search,
  .field-input--sm,
  .role-select {
    font-size: var(--fs-phone-field);
  }

  /* A native select is 26px tall in WebKit and draws its own chrome: the
     field's look, a finger's height and the ink chevron instead. */
  .field-select,
  select.field-input {
    -webkit-appearance: none;
    appearance: none;
    min-height: 48px;
    padding-right: 2.4rem;
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%231b3a5b' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'><path d='m6 9 6 6 6-6'/></svg>");
    background-repeat: no-repeat;
    background-position: right 0.85rem center;
    background-size: 16px 16px;
  }
}

/* A phone held sideways: the sheet keeps a readable measure in the middle. */
@media (max-height: 500px) and (pointer: coarse) {
  :is(.overlay, #loginOverlay, #noteOverlay, #notesListOverlay, #inviteOverlay,
      #avatarOverlay, #profileModal, #audioCheckOverlay, #tourOverlay,
      #resourceOverlay, .journey-feedback-overlay)
    > :is(.login-card, .modal-card, .resource-modal-card, .celebration-card) {
    width: min(100%, 36rem);
  }
}

@keyframes sheet-up {
  from { transform: translateY(2.5rem); opacity: 0; }
  to { transform: none; opacity: 1; }
}

@keyframes sheet-scrim-in {
  from { opacity: 0; }
}

/* Only a phone's sheet has a grabber (ui.js openSheet; the phone rule above
   wins: this one has no specificity). */
:where(.sheet-grabber) {
  display: none;
}

/* Driven by the sheet's scroll (above): the edge stands until the last few
   px of the scroll, then it is gone. */
@keyframes sheet-foot-edge {
  0%, 97% { box-shadow: 0 -2px 0 var(--line-strong); }
  100% { box-shadow: 0 -2px 0 transparent; }
}

@media (prefers-reduced-motion: reduce) and (max-width: 767.98px),
       (prefers-reduced-motion: reduce) and (max-height: 500px) and (pointer: coarse) {
  :is(.overlay, #loginOverlay, #noteOverlay, #notesListOverlay, #inviteOverlay,
      #avatarOverlay, #profileModal, #audioCheckOverlay, #tourOverlay,
      #resourceOverlay, .journey-feedback-overlay)
    > :is(.login-card, .modal-card, .resource-modal-card, .celebration-card) {
    animation: none;
    opacity: 1;
  }

  :is(.overlay, #loginOverlay, #noteOverlay, #notesListOverlay, #inviteOverlay,
      #avatarOverlay, #profileModal, #audioCheckOverlay, #tourOverlay,
      #resourceOverlay, .journey-feedback-overlay) {
    animation: none;
  }
}

/* ─── Switch: a setting turned on or off ──────────────────────────────────
   The pill at the end of a row that is the switch (role="switch",
   aria-checked on the row): off, a hole in the paper; on, mint with the
   knob to the right. Háttérzene and Sötét mód in the game's ⋯ sheet
   (lib/shell.js), Sötét mód in the account menu (auth.js, js/theme.js
   bindDarkSwitch).
   The sizes are border-box, stated here: index.css makes every element
   border-box but not its ::after, so a page with it and one without would
   each put the knob somewhere else. The knob sits 2px inside the rim. */
.switch-pill {
  position: relative;
  flex: none;
  box-sizing: border-box;
  width: calc(2.9rem + 4px);
  height: calc(1.65rem + 4px);
  border: 2px solid var(--ink);
  border-radius: var(--r-full);
  background: var(--paper-deep);
  transition: background 0.15s ease;
}

.switch-pill::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 2px;
  box-sizing: border-box;
  width: calc(1.65rem - 4px);
  height: calc(1.65rem - 4px);
  border: 2px solid var(--ink);
  border-radius: 50%;
  background: var(--card);
  transition: transform 0.15s ease;
}

[aria-checked="true"] > .switch-pill {
  background: var(--mint);
}

[aria-checked="true"] > .switch-pill::after {
  transform: translateX(1.25rem);
}

@media (prefers-reduced-motion: reduce) {
  .switch-pill,
  .switch-pill::after {
    transition: none;
  }
}

/* ─── Themes: the page and what the tokens cannot say ────────────────────
   The palettes are tokens (tokens.css "Themes"); these are the few places
   where a theme changes a component itself. Every rule matches only under
   <html data-theme>, so Világos is untouched. */

/* Sötét's ground: the night with chalk specks and fibres (one plain-alpha
   tile on every device, assets/night-grain-v1.webp from
   scripts/make_night_grain.py), a faint chalk dot grid and the corner glows.
   The cream's multiply grain layer is for paper and only muddies the night,
   so the desktop drops it too: no layer is added anywhere. */
:root[data-theme="dark"] body {
  background-image:
    url("../assets/night-grain-v1.webp"),
    radial-gradient(var(--ink-wash) 1px, transparent 1px),
    radial-gradient(circle at 12% 8%, var(--sun-glow), transparent 45%),
    radial-gradient(circle at 88% 4%, var(--mint-glow), transparent 42%),
    radial-gradient(circle at 80% 94%, var(--coral-glow), transparent 50%),
    linear-gradient(180deg, var(--paper) 0%, var(--paper-deep) 100%);
  background-size: 180px 180px, 22px 22px, auto, auto, auto, auto;
}

:root[data-theme="dark"] body::before {
  display: none;
}

/* The brand mark: an ink tile with a sun P, which on the night would be a
   chalk tile; it turns into the sun with the night P. */
:root[data-theme="dark"] .brand-mark {
  background: var(--sun);
  color: var(--night);
}

/* A disabled stamp lies flat on the night: the paper-deep recipe would be a
   hole in a slate card and its muted ink border would vanish. */
:root[data-theme="dark"] :is(.btn-stamp:disabled, .btn-stamp[aria-disabled="true"]) {
  --btn-bg: var(--slate-soft);
  --btn-fg: var(--chalk-muted);
  border-color: var(--slate-edge);
}

/* The browser's own placeholder grey is too faint on the slate. */
:root[data-theme="dark"] ::placeholder {
  color: var(--ink-muted);
  opacity: 1;
}

/* The third floater is a white chip painted with --on-color, which is the
   night ink in every theme but Világos. */
:root[data-theme] .floater--c {
  background: var(--card);
}

/* The mascot's sun is lit from the top left; --sun-light is a dark sun-tinted
   surface on the night, so the highlight is stated here. */
:root[data-theme="dark"] .mascot-bubble {
  background: radial-gradient(circle at 30% 25%, #fff0b8 0%, var(--sun) 60%, var(--sun-hot) 100%);
}

/* The audio check's lamp, lit: a warm lamp in a dark room, its icon in the
   night ink (the chalk icon would vanish into the sun glow). */
:root[data-theme="dark"] .audio-check-lamp:is(.is-emitting, .is-listening) {
  background: #f6e7b8;
  color: var(--night);
}

/* A toast floats over the page; on the night its tinted wash needs a body. */
:root[data-theme="dark"] .users-toast.is-danger { background: var(--danger-bg-soft); }
:root[data-theme="dark"] .users-toast.is-success { background: var(--mint-surface); }

/* Fields on a section that stays dark in every theme (the landing's partner
   form): there the "on dark" tokens mean chalk, not the night. */
:root[data-theme="dark"] :is(.field-on-dark, .field-input--on-dark) {
  --paper: var(--chalk);
  --on-color: var(--chalk);
  --paper-on-dark: rgba(243, 233, 212, 0.88);
  --paper-on-dark-soft: rgba(243, 233, 212, 0.08);
  --paper-on-dark-mid: rgba(243, 233, 212, 0.14);
  --paper-on-dark-muted: rgba(243, 233, 212, 0.62);
}

/* The phone's select chevron is a picture in the ink; on the night, chalk. */
@media (max-width: 767.98px), (max-height: 500px) and (pointer: coarse) {
  :root[data-theme="dark"] :is(.field-select, select.field-input) {
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%23f3e9d4' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'><path d='m6 9 6 6 6-6'/></svg>");
  }
}
