/* ── Brand font ────────────────────────────────────────────────────────────
   Only Cal Sans is a webfont — it carries the brand on titles. Body/UI text uses
   the SYSTEM font stack (client decision 2026-07-26): one fewer font file to
   download on a page opa/oma often open on mobile data, and it matches the
   native app, which moved body copy to SF in the same change. */
@font-face {
  font-family: "Cal Sans";
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  src:
    local("CalSans-Regular"),
    url("fonts/CalSans-Regular.ttf") format("truetype");
}

/* ── Canonical Heare design tokens (match Heare/Theme.swift) ───────────────── */
:root {
  /* Alle KLEUREN, radii, ruimtes en schaduwen staan in tokens.css. Wat hier
     blijft is wat alleen de recorder kent en geen ontwerptoken is. */
  --nav-height: max(80px, 5rem);
  --check-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23fff' stroke-width='4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 6 9 17l-5-5'/%3E%3C/svg%3E");
}

* {
  box-sizing: border-box;
}

html {
  /* Rubber-band overscroll reveals whatever is behind body — without this,
     the bounce past the top/bottom of the page shows raw white even in
     dark mode, since only body/.flow-shell carry a background. */
  background: var(--room);
}

body {
  margin: 0;
  min-height: 100vh;
  background: var(--room);
  color: var(--title-ink);
  font-family: var(--font-body);
  -webkit-font-smoothing: antialiased;

  /* Een lang woord dat niet past mag afbreken in plaats van het scherm uit te
     lopen. Nederlands zit vol samenstellingen — "verwerkingsverantwoordelijke",
     "infrastructuurleveranciers" — en zodra iemand zijn lettergrootte opschroeft
     past zo'n woord niet meer in de kolom. Dit grijpt alléén in als het woord
     écht niet past; normale tekst merkt er niets van. */
  overflow-wrap: break-word;
}

button,
input,
a {
  font: inherit;
}

.flow-shell {
  min-height: 100vh;
  background: var(--room);
  /* The step-push/step-pop animations slide a full screen-width in from the
     edge; without this the incoming step briefly widens the page and shows a
     horizontal scrollbar/bounce mid-animation. */
  overflow-x: hidden;
}

.nav {
  /* `min-height` in plaats van een vaste hoogte: bij een grotere lettergrootte
     groeien de titel en de knoppen erin, en dan hoort de balk mee te geven in
     plaats van ze eruit te laten steken. Onder de standaardinstelling blijft
     het exact 80 punten. */
  min-height: var(--nav-height);
  display: grid;
  grid-template-columns: minmax(96px, auto) 1fr minmax(96px, auto);
  align-items: center;
  padding: env(safe-area-inset-top) 20px 0;
}

.nav h1 {
  margin: 0;
  font-family: var(--font-display);
  font-size: 1.875rem;
  font-weight: 400;
  line-height: 1.2;
  text-align: center;
}
/* Standalone/browser recorder shows the real Heare wordmark logo, not text. */
.nav-wordmark img { display: block; height: 26px; margin: 0 auto; }

/* Ingebed in de app (iOS of Android) is de schil zelf al Heare, so the recorder
   drops its redundant "Heare" wordmark (the × close stays). */
html.embedded-app .nav h1 {
  display: none;
}

/* …but keep the "Heare" wordmark on the recording screen even in-app (client) —
   it's the one full-screen recorder step where the brand should still show. */
html.embedded-app:has(.step.recording-step.active) .nav h1 {
  display: block;
}

.icon-button {
  /* Groeit mee met zijn eigen glyph — die staat in `rem` en liep er anders
     uit zodra iemand zijn lettergrootte opschroeft. De `max()` houdt de
     ondergrens van 44 punten vast: dat is het kleinste raakvlak dat Apple
     toestaat, en dat mag nooit krimpen. */
  width: max(44px, 2.75rem);
  height: max(44px, 2.75rem);
  border: 0;
  border-radius: var(--radius-round);
  background: transparent;
  color: var(--title-ink);
  font-size: 2.125rem;
  line-height: 1;
}

.step {
  display: none;
  min-height: calc(100vh - var(--nav-height));
  padding: 16px var(--screen-margin) calc(25px + env(safe-area-inset-bottom));
}

.step.active {
  display: grid;
  align-items: start;
}

.content-card {
  width: min(100%, 340px);
  margin: 0 auto;
  padding-top: 0;
}

.eyebrow {
  margin: 0 0 8px;
  color: var(--text-secondary);
  font-size: 0.9375rem;
  font-weight: 700;
}

h2 {
  margin: 0 0 30px;
  font-family: var(--font-display);
  font-size: 2rem;
  font-weight: 400;
  line-height: 1.2;
  color: var(--title-ink);
  /* Zie de toelichting bij dezelfde regel in `landing.css`: tracking hoort bij de
     graad. Deze kop staat op 2rem. */
  letter-spacing: -0.014em;
}

/* Draft-recovery title is constrained so "klaar" wraps to line 2 (frame 10). */
.recovery-title {
  max-width: 300px;
}

label,
.field-label {
  display: grid;
  gap: 9px;
  margin-top: 16px;
  color: var(--text-secondary);
  font-size: 0.9375rem;
  font-weight: 700;
}

.field-label {
  font-family: var(--font-body);
  font-size: 1.0625rem;
  font-weight: 400;
}

.field-label.centered {
  justify-items: center;
  text-align: center;
}

.req {
  color: var(--brand-purple);
}

input {
  width: 100%;
  min-height: 52px;
  border: 1px solid var(--outline-faint);
  border-radius: 12px;
  background: var(--card);
  color: var(--text-medium);
  font-family: var(--font-body);
  font-size: 1.0625rem;
  font-weight: 500;
  outline: 0;
  padding: 0 16px;
  box-shadow: none;
}

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

.primary-button,
.primary-link-button,
.secondary-button,
.destructive-button,
.text-button,
.text-link-button,
.control-circle,
.photo-tile,
.choice-button,
.choice-card {
  cursor: pointer;
}

.primary-button,
.primary-link-button {
  width: 100%;
  min-height: 50px;
  margin-top: 30px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: var(--radius-button);
  background: var(--brand-purple);
  color: var(--on-accent);
  font-family: var(--font-body);
  font-size: 1.0625rem;
  font-weight: 600;
  text-align: center;
  text-decoration: none;
}

.primary-link-button[hidden],
.primary-button[hidden],
.secondary-button[hidden],
.destructive-button[hidden],
.text-link-button[hidden] {
  display: none;
}

.primary-button:disabled {
  opacity: 0.72;
}

.choice-stack {
  display: grid;
  gap: 30px;
  margin-top: 30px;
}

.choice-button {
  width: 100%;
  min-height: 128px;
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 20px 60px 20px 20px;
  border: 0;
  border-radius: var(--radius-button);
  color: var(--title-ink);
  text-align: left;
}

/* Een tint, geen klasse: beide bestonden uit precies één declaratie. */
.choice-button[data-tone="parent"] { background: var(--card-beige); }
.choice-button[data-tone="gift"] { background: rgba(114, 200, 254, 0.15); }

.choice-icon {
  width: 77px;
  height: 77px;
  flex-shrink: 0;
  border-radius: var(--radius-button);
  background: var(--card);
  object-fit: cover;
}

.choice-copy {
  display: grid;
  gap: 5px;
  min-width: 0;
}

.choice-button strong {
  font-family: var(--font-display);
  font-size: 1.375rem;
  font-weight: 400;
  line-height: 1.1;
}

.choice-copy span {
  color: rgba(28, 24, 64, 0.5);
  font-size: 0.8125rem;
  line-height: 1.35;
}

.choice-chevron {
  position: absolute;
  right: 22px;
  top: 50%;
  transform: translateY(-50%);
  display: grid;
  place-items: center;
  color: rgba(28, 24, 64, 0.4);
}

.secondary-button {
  width: 100%;
  min-height: 50px;
  margin-top: 12px;
  border: 0;
  border-radius: var(--radius-button);
  background: var(--card-beige);
  /* #2B2742, niet #1C1840: iOS (`ebonyClay`), Android (`textPrimary`) en
     docs/design.md zeggen alle drie dezelfde inkt — het web was de eenling. */
  color: var(--text-primary);
  font-size: 1.0625rem;
  /* 500, niet 600: de secundaire knop hoort lichter te zijn dan de primaire.
     iOS doet dat al (`.medium` tegen `.semiBold`), het web niet. */
  font-weight: 500;
}

/* De bevestiging van iets onomkeerbaars — "Ja, vergeet mijn adres", "Ja, trek
   mijn toestemming in". Eigen rol, eigen vulling: beige zou hem naast de
   secundaire knop zetten en dat is precies wat hij niet is. Heette tot
   1 oktober `.dark-button`, wat zijn KLEUR benoemde in plaats van zijn rol —
   en daardoor droeg hij ook drie "Opnieuw proberen"-knoppen, terwijl
   "Upload opnieuw" op dezelfde rol al beige was. */
.destructive-button {
  width: 100%;
  min-height: 50px;
  margin-top: 17px;
  border: 0;
  border-radius: var(--radius-button);
  /* #2B2742 (`primaryButtonFill`), niet #1C1840 — zo staat het in
     docs/design.md en zo tekenen iOS en Android hem. */
  background: var(--primary-button-fill);
  color: var(--on-accent);
  font-family: var(--font-body);
  font-size: 1.0625rem;
  font-weight: 600;
}

.text-button,
.text-link-button {
  width: 100%;
  min-height: 50px;
  font-size: 1.0625rem;
  margin-top: 10px;
  display: grid;
  place-items: center;
  border: 0;
  background: transparent;
  color: var(--title-ink);
  font-weight: 600;
  text-align: center;
  text-decoration: none;
}

.body-copy,
.muted {
  color: var(--text-secondary);
  font-size: 1.0625rem;
  line-height: 1.45;
}

/* ── 09 WebView opnemen — active recording ───────────────────────────────── */
.recording-step {
  padding: 0;
}

.recording-step.active {
  display: block;
}

.recording-panel {
  position: relative;
  display: flex;
  flex-direction: column;
  /* lock to the viewport so only the story panel scrolls — never the page */
  height: calc(100dvh - var(--nav-height));
  overflow: hidden;
  padding: 0 var(--screen-margin) calc(14px + env(safe-area-inset-bottom));
  background: var(--room);
  text-align: center;
}

.recording-header {
  margin: 4px auto 0;
  max-width: 332px;
}

.recording-header h2 {
  margin: 0 0 10px;
  text-align: center;
  font-size: 2rem;
}

.recording-subtitle {
  width: 100%;
  max-width: 300px;
  margin: 0 auto;
  color: var(--text-secondary);
  font-family: var(--font-body);
  font-size: 1.0625rem;
  line-height: 1.35;
}

/* 7942-897 has no title/child context line. */
.context { display: none; }

/* Editor body: a flex column that fills the panel; the story panel flexes to
   absorb the slack and scrolls internally (so the page itself never scrolls). */
.record-controls {
  display: flex;
  flex-direction: column;
  align-items: center;
  width: 100%;
  flex: 1;
  min-height: 0;
  gap: 0;
  margin-top: 14px;
}
/* Keep every editor control rigid; ONLY the story panel flexes/scrolls — otherwise
   flexbox shrinks the fixed items (the waveform band collapsed to a few px). */
.record-controls > *:not(.story-text-panel) { flex: 0 0 auto; }

.timer-block {
  display: grid;
  justify-items: center;
  gap: 5px;
  width: 100%;
}

.timer-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 5px;
}

.timer-dot {
  width: 10.4px;
  height: 10.4px;
  border-radius: 50%;
  background: var(--brand-purple);
}

.timer {
  font-family: var(--font-display);
  font-size: 2rem;
  line-height: 1.2;
  color: var(--title-ink);
}

.timer-caption {
  margin: 0;
  color: rgba(28, 24, 64, 0.5);
  font-family: var(--font-body);
  font-size: 0.8125rem;
}

/* 7942-897 has no waveform; the canvas stays (hidden) for the JS analyser. */
.waveform { display: none; }

/* Pauze / Opnieuw / Markeer — beige circle controls. */
.transport-row {
  display: flex;
  align-items: flex-start;
  justify-content: center;
  gap: 32px;
}

.transport-control {
  display: grid;
  justify-items: center;
  gap: 2px;
  width: 55px;
}

.control-circle {
  display: grid;
  place-items: center;
  width: 55px;
  height: 55px;
  border: 0;
  border-radius: 50%;
  background: var(--card-beige);
  color: var(--title-ink);
  padding: 0;
}

.control-circle:disabled { opacity: 0.45; }

/* Default: pause glyph visible, resume (play triangle) hidden. */
.control-circle .icon-pause { fill: currentColor; display: block; }
.control-circle .icon-resume { fill: currentColor; display: none; }

/* Paused: the pause control becomes a resume (play) affordance. */
.control-circle.is-resume .icon-pause { display: none; }
.control-circle.is-resume .icon-resume { display: block; }

.control-label {
  font-family: var(--font-body);
  font-size: 0.8125rem;
  font-weight: 400;
  color: var(--title-ink);
  line-height: 1.1;
}

/* JS toggles these classes; keep them inert on the flat controls. */
.control-label.highlight-stop,
.control-label.highlight-resume {
  font-weight: 400;
  color: var(--title-ink);
}

/* Blue marker chip with the count (7944-932). */
.marker-chip {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  margin: 16px auto 0;
  padding: 6px 12px;
  border-radius: var(--radius-button);
  background: rgba(114, 200, 254, 0.5);
  color: #1b6796;
  /* Always reserve the chip's space; toggling visibility (not display) means a
     newly-added marker never shoves the text + Stoppen button below it down. */
  visibility: hidden;
}
.marker-chip.is-visible { visibility: visible; }
/* Same gotcha: `label { display: grid }` was beating [hidden], so the iOS/invite
   "Start opname" gate still showed the already-known name+title fields (a
   duplicate of the native form). Make hiding those labels reliable. */
label[hidden] { display: none; }

.marker-chip svg { display: block; flex: none; }

.marker-chip span {
  font-family: var(--font-body);
  font-size: 0.8125rem;
  color: #1b6796;
}

.cap-notice {
  margin: 16px auto 0;
  max-width: 300px;
  color: var(--brand-purple);
  font-size: 0.875rem;
  font-weight: 600;
  line-height: 1.35;
}

/* Tip! speech-bubble card with a downward tail. */
.tip-card {
  position: relative;
  width: min(332px, calc(100vw - 48px));
  margin: 40px auto 0;
  padding: 16px 24px 18px 17px;
  border-radius: var(--radius-card);
  background: var(--card);
  text-align: left;
  box-shadow: 0 14px 30px -18px rgba(43, 39, 66, 0.35);
}

.tip-card::after {
  content: "";
  position: absolute;
  right: 34px;
  bottom: -10px;
  width: 22px;
  height: 16px;
  background: var(--card);
  clip-path: polygon(0 0, 100% 0, 60% 100%);
}

.tip-heading {
  display: block;
  font-family: var(--font-display);
  font-size: 1rem;
  font-weight: 400;
  line-height: 1.2;
  color: var(--title-ink);
}

.tip-body {
  margin: 8px 0 0;
  color: var(--text-secondary);
  font-size: 0.8125rem;
  line-height: 1.4;
}

.autosave {
  margin: auto 0 0;   /* push the reassurance + Klaar bar to the bottom */
  padding-top: 24px;
  color: var(--text-secondary);
  font-size: 0.8125rem;
}

/* Bottom purple finish bar (7942-949). */
.klaar-bar {
  width: 100%;
  align-self: center;
  margin: 14px 0 0;
  /* Dit IS de primaire knop, met een eigen naam omdat hij in het opnameraster
     ligt. Zelfde maat als `.primary-button` (gemeten op iOS: 50,0pt). */
  height: 50px;
  border: 0;
  border-radius: var(--radius-button);
  background: var(--brand-purple);
  color: #fff;
  font-family: var(--font-body);
  font-size: 1.0625rem;
  font-weight: 600;
  cursor: pointer;
}

/* ── Preview player (brand play/pause; the <audio> is engine-only) ───────── */
.preview-player {
  display: flex;
  align-items: center;
  gap: 14px;
  margin: 10px 0 4px;
}

.preview-player audio {
  display: none;
}

.preview-play {
  flex: none;
  width: 56px;
  height: 56px;
  border: 0;
  border-radius: var(--radius-round);
  background: var(--brand-purple);
  display: grid;
  place-items: center;
  cursor: pointer;
  box-shadow: 0 10px 20px -10px rgba(108, 92, 255, 0.55);
}

/* White Lucide play/pause on the purple preview circle (claim pill keeps purple). */
.preview-play .play-glyph { color: var(--on-accent); }

.preview-time {
  color: var(--text-secondary);
  font-size: 0.9375rem;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* ── Preview scrubber + marker pins ──────────────────────────────────────── */

.scrubber {
  position: relative;
  width: 100%;
  height: 28px;
  margin: 4px 0 6px;
  cursor: pointer;
  touch-action: none;
}

.scrubber-track {
  position: absolute;
  top: 50%;
  left: 0;
  right: 0;
  height: 6px;
  transform: translateY(-50%);
  border-radius: var(--radius-round);
  background: rgba(43, 39, 66, 0.12);
  overflow: hidden;
}
/* A dark tint stays visible on the light page but nearly vanishes against the dark
   one — the seek track becomes hard to find, not unreadable text but still a real
   usability miss on the same screen as the other post-Klaar bugs. */
html.theme-dark .scrubber-track { background: rgba(255, 255, 255, 0.14); }

.scrubber-progress {
  height: 100%;
  width: 0;
  border-radius: var(--radius-round);
  background: var(--brand-purple);
}

.scrubber-markers {
  position: absolute;
  inset: 0;
}

.marker-pin {
  position: absolute;
  top: 50%;
  width: 12px;
  height: 12px;
  margin-left: -6px;
  transform: translateY(-50%);
  padding: 0;
  border: 2px solid var(--card);
  border-radius: var(--radius-round);
  background: var(--title-ink);
  box-shadow: 0 2px 6px -2px rgba(43, 39, 66, 0.5);
}

/* ── 11 WebView afronden — photo + nickname ──────────────────────────────── */
/* Finish step (frame 11): keep the whole column centered. */
[data-step="reader"] h2 {
  text-align: left;
}

.photo-section {
  display: grid;
  justify-items: stretch;
  gap: 10px;
  margin-top: 30px;
}
/* `display: grid` would otherwise beat the [hidden] attribute (same gotcha as the
   marker chip) — picking a known narrator hides the whole photo step. */
.photo-section[hidden] { display: none; }

/* De uitleg hoort tegen zijn label aan te staan, niet op de rasterafstand van
   10px die voor label→tegel bedoeld is. Vandaar de negatieve marge: het label
   is zelf een grid-item en kan de hint niet omvatten zonder extra wikkel. */
.photo-hint {
  margin-top: -6px;
  color: var(--text-secondary);
  font-size: 0.8125rem;
  font-weight: 400;
  line-height: 1.35;
}

.photo-tile {
  position: relative;
  width: 100%;
  height: 145px;
  display: grid;
  place-items: center;
  padding: 16px 16px 12px;
  /* Eén pixel, in de kleur van een invoerveld (klant, 30 september). Stond op
     3px in taupe, en dat las als een apart soort vlak tussen de velden in. */
  border: 1px dashed var(--outline-faint);
  border-radius: 12px;
  /* Wit, niet beige (klant, 29 september). Dit vlak staat tussen twee witte
     invoervelden in; een beige vulling maakte er een derde soort oppervlak van. */
  background: var(--card);
  color: var(--tile-taupe);
  overflow: hidden;
}

.photo-icon {
  display: block;
  color: var(--text-secondary);
}

/* The chosen photo shows as a CIRCLE inside the dashed frame (client request);
   the cropped source is already square, so border-radius 50% gives a clean circle. */
.photo-preview {
  width: 110px;
  height: 110px;
  border-radius: 50%;
  object-fit: cover;
  display: none;
}

.photo-tile.has-photo {
  background: var(--card);
}

.photo-tile.has-photo .photo-icon {
  display: none;
}

.photo-tile.has-photo .photo-preview {
  display: block;
}

/* Pencil affordance to re-pick / re-crop; tapping anywhere on the tile re-opens
   the picker, the pencil just signals it's editable. */
.photo-edit {
  position: absolute;
  right: 12px;
  bottom: 12px;
  display: none;
  place-items: center;
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: var(--brand-purple);
  color: var(--on-accent);
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
}

.photo-tile.has-photo .photo-edit {
  display: grid;
}

.nickname-section {
  display: grid;
  gap: 6px;
  margin-top: 30px;
}
.nickname-section[hidden] { display: none; }

/* Keep the question + required asterisk on one line. */
.nickname-section .field-label {
  display: block;
  margin-top: 0;
}

.nickname-section input {
  margin-top: 0;
}

/* Custom reader-name autocomplete (iOS WKWebView has no <datalist> dropdown). */
.reader-input-wrap { position: relative; }
.reader-suggestions {
  position: absolute;
  left: 0;
  right: 0;
  top: calc(100% + 4px);
  z-index: 20;
  margin: 0;
  padding: 4px;
  list-style: none;
  background: var(--card);
  border: 1px solid rgba(28, 24, 64, 0.12);
  border-radius: 14px;
  box-shadow: 0 10px 28px rgba(28, 24, 64, 0.14);
  max-height: 220px;
  overflow-y: auto;
}
.reader-suggestions[hidden] { display: none; }
.reader-suggestions li {
  padding: 12px;
  border-radius: 9px;
  cursor: pointer;
  font-family: var(--font-body);
  font-size: 0.9375rem;
  color: var(--title-ink);
}
.reader-suggestions li:active,
.reader-suggestions li:hover { background: rgba(114, 200, 254, 0.18); }

/* ── 24 Web keuze na opname — choice cards ───────────────────────────────── */
.choice-stack-cards {
  gap: 30px;
  margin-top: 30px;
}

.choice-card {
  width: 100%;
  display: grid;
  justify-items: center;
  gap: 5px;
  padding: 22px 18px 18px;
  border: 0;
  border-radius: 22px;
  background: var(--card);
  color: var(--title-ink);
  text-align: center;
  box-shadow: 0 14px 30px -20px rgba(43, 39, 66, 0.45);
}

.choice-card .badge {
  width: 58px;
  height: 58px;
  display: grid;
  place-items: center;
  border-radius: var(--radius-round);
}

.badge-peach { background: var(--chip-peach); color: var(--brand-purple); }
.badge-blue { background: var(--chip-ice); color: #2e93c9; }

.choice-card strong {
  margin-top: 7px;
  font-family: var(--font-body);
  font-size: 1.1875rem;
  font-weight: 600;
  color: var(--title-ink);
  line-height: 1.2;
}

.choice-card .card-subtitle {
  color: var(--person-meta);
  font-size: 0.8125rem;
  font-weight: 400;
  line-height: 1.4;
}

/* ── 23 Web afronden — finish mascot (7942:1920) ─────────────────────────── */
.reader-step { position: relative; }
.reader-mascot {
  display: block;
  width: 260px;
  max-width: 80%;
  align-self: flex-start;
  margin: 16px 0 -36px -34px;
  pointer-events: none;
  user-select: none;
}

/* ── 24 Web keuze na opname — who-pays choice cards (7942:1956) ───────────── */
.pay-choice {
  width: 100%;
  display: grid;
  gap: 15px;
  padding: 20px;
  border-radius: var(--radius-button);
  text-align: left;
}
.pay-choice-self { background: var(--card-beige); }
.pay-choice-parent { background: rgba(114, 200, 254, 0.15); }
.pay-choice-badge {
  justify-self: start;
  padding: 3px 8px;
  border-radius: var(--radius-button);
  font-size: 0.8125rem;
  line-height: 1.2;
}
.badge-self { background: rgba(114, 200, 254, 0.15); color: #72c8fe; }
.badge-parent { background: rgba(108, 92, 255, 0.25); color: var(--brand-purple); }
.pay-choice-row { display: flex; gap: 12px; align-items: flex-start; }
.pay-choice-icon {
  width: 77px;
  height: 77px;
  flex-shrink: 0;
  border-radius: var(--radius-button);
  background: var(--card);
  object-fit: cover;
}
.pay-choice-text { display: grid; gap: 5px; }
.pay-choice-text strong {
  font-family: var(--font-display);
  font-size: 1.375rem;
  font-weight: 400;
  line-height: 1.1;
  color: var(--title-ink);
}
.pay-choice-text .card-subtitle {
  font-size: 0.8125rem;
  font-weight: 400;
  line-height: 1.4;
  color: rgba(28, 24, 64, 0.5);
}
.pay-choice-button {
  width: 100%;
  /* De hoogte en de regelhoogte zijn hier hetzelfde getal: zo staat het
     enkele label netjes gecentreerd (display:grid brak het over twee rijen).
     Allebei in `rem` zodat de knop meegroeit met de letter — anders houdt hij
     42 punten en loopt de tekst eruit. `max()` houdt de ondergrens vast. */
  min-height: max(50px, 3.125rem);
  line-height: max(50px, 3.125rem);
  text-align: center;
  white-space: nowrap;
  padding: 0 14px;
  border: 0;
  border-radius: var(--radius-button);
  background: var(--brand-purple);
  color: var(--on-accent);
  font-family: var(--font-body);
  font-size: 1.0625rem;
  font-weight: 600;
  cursor: pointer;
}

/* ── Web invite intro (7942-1326) ────────────────────────────────────────── */
/* Links uitgelijnd, net als de negen andere stappen (klant, 23 september). Stond
   gecentreerd achter een kale sectiekop, terwijl twee blokken eronder er juist weer
   uit braken met `text-align: left` — drie uitlijningen op één kaart, en dát las als
   onrust. De mascotte is hier weg; op "klaar" en "gedeeld" staat hij nog. */
.intro-title { margin: 0 0 12px; }
.intro-copy {
  margin: 0;
  max-width: 300px;
  line-height: 1.4;
}
/* Two short reassurances, checkmarked. A list rather than prose: someone
   scanning before they commit reads marks, not sentences. */
.intro-reassure {
  list-style: none;
  margin: 22px 0 0;
  max-width: 300px;
  padding: 0;
  display: grid;
  gap: 8px;
  width: 100%;
}
.intro-reassure li {
  display: grid;
  grid-template-columns: 20px 1fr;
  gap: 10px;
  align-items: start;
  font-size: 0.9375rem;
  color: var(--text-medium);
}
.intro-reassure li::before {
  content: "";
  width: 20px;
  height: 20px;
  margin-top: 1px;
  background: var(--brand-purple);
  -webkit-mask: var(--check-mask) center / 12px 12px no-repeat;
  mask: var(--check-mask) center / 12px 12px no-repeat;
  border-radius: var(--radius-round);
  opacity: .9;
}

.intro-note {
  display: flex;
  align-items: center;
  gap: 13px;
  width: 100%;
  margin-top: 36px;
}
.intro-note-icon {
  width: 48px;
  height: 48px;
  flex-shrink: 0;
  display: grid;
  place-items: center;
  border-radius: var(--radius-round);
  background: rgba(136, 197, 254, 0.2);
  color: #4aa3e8;
}
.intro-note-text { display: grid; gap: 2px; }
.intro-note-text strong { font-size: 0.9375rem; font-weight: 700; color: var(--title-ink); }
.intro-note-text span { font-size: 0.875rem; color: var(--text-secondary); }
.intro-footer {
  margin: 16px 0 0;
  font-size: 0.8125rem;
  color: var(--text-secondary);
}

/* ── Web invite "Wat wil je voorlezen?" (7942-1984) ──────────────────────── */
.step[data-step="voorlees-choice"] h2 { margin-bottom: 10px; }
.voorlees-sub { margin: 0; }
.voorlees-stack {
  display: grid;
  gap: 16px;
  margin-top: 26px;
}
.voorlees-card {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 13px;
  padding: 18px;
  border: 0;
  border-radius: var(--radius-button);
  text-align: left;
  cursor: pointer;
}
.voorlees-card-heare { background: rgba(136, 197, 254, 0.22); }     /* blue — matches the app (storyBlue @ 0.22); the Heare-original path stands out */
.voorlees-card-eigen { background: var(--card-beige); }            /* beige */
.voorlees-icon {
  width: 64px;
  height: 64px;
  flex-shrink: 0;
  border-radius: var(--radius-button);
  background: var(--card);
  object-fit: contain;
}
.voorlees-text {
  display: grid;
  gap: 4px;
  flex: 1;
  min-width: 0;
}
.voorlees-text strong {
  font-family: var(--font-display);
  font-size: 1.3125rem;
  font-weight: 400;
  line-height: 1.1;
  color: var(--title-ink);
}
.voorlees-text .card-subtitle {
  font-size: 0.8125rem;
  line-height: 1.4;
  color: rgba(28, 24, 64, 0.5);
}
.voorlees-chevron {
  flex-shrink: 0;
  display: grid;
  place-items: center;
  color: rgba(28, 24, 64, 0.4);
}

/* B#4: standalone web (not embedded in the iOS app) — the terminal share screen's
   Done (Klaar) and Close (×) lead nowhere, so hide them. */
html:not(.embedded-app) #doneButton { display: none; }
html:not(.embedded-app):has(.step[data-step="intro"].active) #cancelButton,
html:not(.embedded-app):has(.step[data-step="intent"].active) #cancelButton,
html:not(.embedded-app):has(.step[data-step="share"].active) #cancelButton { visibility: hidden; }

/* Client: the explicit Kopieer/WhatsApp/E-mail pills are redundant — "Deel link"
   opens the system share sheet with every channel. */
#shareActions { display: none; }

/* Checkout cover play/pause glyph swap (the cover is tappable to hear the take). */
.pay-cover-play .glyph-pause { display: none; }
.pay-cover-play.is-playing .glyph-play { display: none; }
.pay-cover-play.is-playing .glyph-pause { display: block; }
.pay-cover { cursor: pointer; }

.chip-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 7px;
  margin-top: 5px;
}

.chip {
  display: inline-flex;
  align-items: center;
  padding: 4px 10px;
  border-radius: 20px;
  font-family: var(--font-body);
  font-size: 0.6875rem;
  font-weight: 600;
  line-height: 1;
}

.chip-orange { background: var(--chip-peach); color: var(--chip-peach-ink); }
.chip-green { background: var(--chip-mint); color: var(--chip-mint-ink); }
.chip-blue { background: var(--chip-ice); color: var(--chip-ice-ink); }

/* ── Payment + share ─────────────────────────────────────────────────────── */
.pay-preview {
  display: grid;
  justify-items: center;
  gap: 12px;
  margin: 8px 0 18px;
  text-align: center;
}

.pay-preview[hidden] {
  display: none;
}

/* Brand cover (Figma 7804:942): PNG tile, duration chip, play. */
.pay-cover {
  position: relative;
  width: 203px;
  height: 202px;
  border-radius: 20px;
  background: var(--card-beige); /* fallback; JS sets the PNG cover per story */
  overflow: hidden;
  /* Figma 7942-2006: flat tile, no shadow. */
}

/* Figma 7942-2006: the gift-checkout title is centred. */
.pay-card h2 { text-align: center; }

/* One of 3 brand colours × 3 shapes, chosen per story (like the native tile). */
/* The PNG cover tile fills the card; its art sits bottom-left (like the app). */
.pay-cover-shape {
  position: absolute;
  inset: 0;
}

.pay-cover-shape img {
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  height: auto;
}

/* Sits over the (possibly colorful) cover artwork, not the plain card surface —
   so, like the catalog's own badge-duration chip, it uses a FIXED dark-translucent
   background with white text rather than an inverting token pair. Inverting would
   put white-on-white in dark mode, the same failure as everything else on this
   screen, just via a different route (my first attempt at this fix did exactly
   that before catching it). */
.pay-cover-chip {
  position: absolute;
  left: 12px;
  top: 12px;
  display: inline-flex;
  align-items: center;
  height: 24px;
  padding: 0 10px;
  border-radius: var(--radius-round);
  background: rgba(28, 24, 64, 0.55);
  color: #ffffff;
  font-size: 0.6875rem;
  font-weight: 600;
}

.pay-cover-play {
  position: absolute;
  right: 12px;
  bottom: 12px;
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.9);
  color: var(--title-ink);   /* play glyph black, not purple (client) */
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
}

.pay-cover-title {
  font-family: var(--font-display);
  font-size: 1.375rem;
  font-weight: 400;
  line-height: 1.2;
  text-align: center;
  color: var(--title-ink);
}

/* Byline row (Figma 7942:2006): small reader avatar + "door {reader}". */
.pay-cover-by {
  display: flex;
  align-items: center;
  gap: 5px;
}

.pay-cover-avatar {
  width: 28px;
  height: 28px;
  flex-shrink: 0;
  border-radius: 50%;
  background-color: var(--card-beige);
  background-size: cover;
  background-position: center;
}

.pay-cover-byline {
  color: var(--text-medium);
  font-size: 1.0625rem;
}

/* Horizontal price pill: prijs · label | description. */
.pay-price {
  display: flex;
  gap: 20px;
  align-items: center;
  width: 100%;
  margin-top: 4px;
  padding: 20px;
  border-radius: var(--radius-button);
  background: var(--card);
  border: 1px solid var(--card-beige);
}

.pay-price-amount strong {
  font-family: var(--font-display);
}

.pay-price[hidden] {
  display: none;
}

.pay-price-amount {
  flex-shrink: 0;
  white-space: nowrap;
  font-size: 0.8125rem;
  font-weight: 700;
  color: var(--person-meta);
}

/* Het prijsbedrag zelf — de tekst die in donkere modus zwart-op-zwart stond
   (client 2026-07-29): een hardgecodeerd donkerblauw #2C2A46 zonder
   thema-variant, op een kaart die zelf donker wordt. */
.pay-price-amount strong {
  font-size: 1.5rem;
  font-weight: 800;
  color: var(--title-ink);
}

.pay-price-desc {
  flex: 1;
  font-size: 0.8125rem;
  line-height: 1.3;
  color: var(--text-medium);
}

.pay-stripe {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  margin-top: 12px;
  color: var(--text-secondary);
  font-size: 0.75rem;
  font-weight: 700;
}

/* EU digital-content consent line under the pay/save button (client legal copy). */
.pay-terms {
  margin: 14px 0 0;
  text-align: center;
  font-size: 0.6875rem;
  line-height: 1.45;
  color: var(--text-medium);
}

.pay-stripe[hidden] {
  display: none;
}

/* The label display rule otherwise beats the [hidden] attribute. */
#emailField[hidden] {
  display: none;
}

/* Kortingscode ("Heb je een code?") — collapsed link between the email field
   and the price block; reveal is a small inline input + apply button rather
   than a full-width form, since it's an optional secondary action. */
.discount-toggle {
  width: auto;
  min-height: 0;
  margin: 14px auto 0;
  padding: 4px 0;
  font-size: 0.875rem;
  font-weight: 700;
  text-decoration: underline;
  color: var(--text-medium);
}

.discount-row {
  margin-top: 14px;
}

.discount-row label {
  margin-top: 0;
}

.discount-input-group {
  display: flex;
  gap: 8px;
  align-items: center;
}

.discount-input-group input {
  flex: 1;
  text-transform: uppercase;
}

.discount-input-group input::placeholder {
  text-transform: none;
}

/* Same fill as `.destructive-button` — deliberately NOT the purple primary, which
   belongs to "Sla verhaal op" one block below; applying a code is a secondary
   action next to its input. It inherits that button's dark-theme override too
   (see html.theme-dark below): `--title-ink` inverts to white in dark mode, so on
   its own this rule painted white text on a white fill (client 2026-08-20). */
.discount-apply-button {
  flex-shrink: 0;
  min-height: 52px;
  padding: 0 18px;
  border: 0;
  border-radius: var(--radius-button);
  background: var(--title-ink);
  color: var(--on-accent);
  font-family: var(--font-body);
  font-size: 0.9375rem;
  font-weight: 600;
  cursor: pointer;
}

.discount-apply-button:disabled {
  opacity: 0.5;
  cursor: default;
}

.discount-status {
  margin: 8px 0 0;
  font-size: 0.8125rem;
  line-height: 1.4;
}

.discount-status.is-error {
  color: #d64545;
}

.discount-status.is-success {
  color: var(--title-ink);
  font-weight: 700;
}

.discount-row[hidden],
.discount-toggle[hidden] {
  display: none;
}

.claim-link {
  overflow-wrap: anywhere;
  margin: 22px 0 4px;
  padding: 16px;
  border-radius: var(--radius-control);
  background: var(--card-beige);
  color: var(--title-ink);
  font-size: 0.9375rem;
}

/* #fff2ed was one digit off from --card-beige (#fff3e7) — close enough that it was
   clearly meant to be the same warm card, just hardcoded separately and so never
   inverted in dark mode. color: var(--title-ink) DID invert (to white), which is what
   produced the white-on-light-peach bug shared with everything else on this
   screen. Using the actual token fixes both at once. */
.error-box {
  margin: 18px 0 0;
  padding: 16px;
  border-radius: var(--radius-control);
  background: var(--card-beige);
  color: var(--title-ink);
  text-align: left;
}

.error-box strong {
  display: block;
  font-size: 1rem;
}

.error-box p {
  margin: 6px 0 0;
  color: var(--text-secondary);
  font-size: 0.875rem;
  line-height: 1.4;
}

.technical-error {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.75rem !important;
  overflow-wrap: anywhere;
}

/* ── 26 Web cadeau success + claim page — Hearo gift illustration ──────────── */
.gift-illustration {
  position: relative;
  width: 100%;
  height: 196px;
  margin: 6px auto 18px;
  display: grid;
  place-items: center;
}

.gift-illustration .hearo {
  position: relative;
  z-index: 1;
  width: 168px;
  height: auto;
}

.gift-illustration .hearo.with-photo {
  width: 156px;
}

.confetti {
  position: absolute;
  pointer-events: none;
}

.confetti-heart {
  width: 30px;
  height: 28px;
  background: url("/assets/blue-heart.png") center / contain no-repeat;
}

.confetti-star {
  width: 38px;
  height: 38px;
  background: url("/assets/purple-star.png") center / contain no-repeat;
}

.confetti-dot {
  width: 11px;
  height: 11px;
  border-radius: 50%;
}

.confetti-dot-orange { background: var(--brand-purple); }
.confetti-dot-blue { background: var(--story-blue); }

.confetti-tl { top: 16%; left: 9%; transform: rotate(10deg); }
.confetti-tr { top: 20%; right: 8%; transform: rotate(-13deg); }
.confetti-dot.confetti-dot-orange { bottom: 28%; left: 9%; }
.confetti-dot.confetti-dot-blue { bottom: 24%; right: 11%; }

/* Reader's photo (optional) — a small circular "from" badge on the Hearo's
   lower-right, like the in-app narrator chip. */
.gift-illustration .reader-photo {
  position: absolute;
  z-index: 2;
  bottom: 8px;
  right: 19%;
  width: 60px;
  height: 60px;
  border-radius: 50%;
  object-fit: cover;
  border: 3px solid var(--room);
  box-shadow: 0 6px 14px -6px rgba(43, 39, 66, 0.4);
}

/* ── Share success (frame 26) + claim page cards ──────────────────────────── */
.share-card,
.claim-card {
  text-align: center;
}

.share-card h2,
.claim-card h2 {
  text-align: center;
  margin-bottom: 14px;
}

.share-card .body-copy,
.claim-card .body-copy,
.claim-status {
  text-align: center;
}

/* "Wil je horen wanneer Noor luistert?" — het enige dat de voorlezer terugkrijgt.
   Eén beige kaart onder het moment zelf, in plaats van een scheidingslijn met losse
   regels: bovenaan gaat het over het verhaal, deze kaart gaat over haar. De naam van
   het kind is de enige kleur. Het label van het veld is er alleen voor schermlezers:
   de vraag en de voorbeeldtekst zeggen al wat erin hoort (client 2026-09-14). */
.reader-followup {
  margin-top: 24px;
  text-align: left;
}

.reader-followup[hidden],
.reader-followup [hidden] {
  display: none;
}

.reader-followup-card {
  padding: 18px 16px 16px;
  border-radius: var(--radius-card);
  background: var(--card-beige);
}

.reader-followup-ask,
.reader-followup-sent-title {
  margin: 0 0 6px;
  font-family: var(--font-display);
  font-size: 1.5rem;
  font-weight: 400;
  line-height: 1.2;
  color: var(--title-ink);
}

.reader-followup-sent-title {
  display: flex;
  align-items: center;
  gap: 8px;
}

/* Merkpaars op 1.5rem telt als grote tekst, waarvoor 3:1 volstaat; paars op beige
   haalt 4:1. Op de donkere kaart haalt hetzelfde paars dat niet, vandaar daar een
   lichtere tint van dezelfde kleur. */
.reader-followup-name,
.reader-followup-icon {
  color: var(--brand-purple);
}

html.theme-dark .reader-followup-name,
html.theme-dark .reader-followup-icon {
  color: #b3aaff;
}

.reader-followup-icon {
  display: inline-flex;
}

.reader-followup-copy {
  margin: 0;
  font-size: 0.9375rem;
  line-height: 1.45;
  color: var(--text-medium);
}

/* `label` en `input` zijn hierboven ingericht als formulierveld met een zichtbare kop
   (grid, marge, grijze letters). Hier staat alleen het veld. */
.reader-followup-field {
  display: block;
  margin: 14px 0 0;
}

.reader-followup-field input {
  border-radius: var(--radius-control);
}

/* De rechtenverklaring op het startscherm en de detailstap. Zelfde behandeling als
   de juridische regel onder de betaalknop (.pay-terms): klein, gedempt, links mee met
   de rest. Geen vinkje meer — zie de toelichting in record/index.html. */
/* Dezelfde graad en kleur als elke andere kleine tekst onder een knop — vergelijk
   `.intro-footer` (klant, 29 september). Stond op 0.6875rem in een donkerder grijs
   en las daardoor als een derde soort voetnoot naast de twee die er al waren. */
.rights-note {
  margin: 16px 0 0;
  font-size: 0.8125rem;
  line-height: 1.45;
  color: var(--text-secondary);
}

.reader-followup-news {
  display: flex;
  align-items: center;
  gap: 10px;
  margin: 12px 0 14px;
  color: var(--text-medium);
  font-size: 0.875rem;
  font-weight: 500;
  line-height: 1.4;
  cursor: pointer;
}

.reader-followup-news input {
  flex: none;
  width: 18px;
  height: 18px;
  min-height: 0;
  margin: 0;
  padding: 0;
  accent-color: var(--brand-purple);
  cursor: pointer;
}

/* Stemkloon (proef): de stemvraag. Zelfde vorm als het nieuwsvinkje, maar met een
   eigen regel uitleg eronder — dit is een zwaardere vraag, en die hoort je niet te
   verrassen. Standaard uit, en even zwaar als niets doen: geen kleur, geen icoon. */
.reader-voice {
  margin: -2px 0 14px;
  padding: 12px 0 0;
  /* Volgt de inkt mee in het donkere thema. */
  border-top: 1px solid color-mix(in srgb, var(--title-ink) 10%, transparent);
}

.reader-voice-choice {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  color: var(--text-medium);
  font-size: 0.875rem;
  /* Zelfde gewicht als het nieuwsvinkje, en geen eigen marge: de globale
     `label`-regel geeft anders 700 en 16px erboven. */
  margin: 0;
  font-weight: 500;
  line-height: 1.45;
  cursor: pointer;
}

.reader-voice-choice input {
  flex: none;
  width: 18px;
  height: 18px;
  min-height: 0;
  margin: 1px 0 0;
  padding: 0;
  accent-color: var(--brand-purple);
  cursor: pointer;
}

.reader-voice-text {
  display: grid;
  gap: 3px;
}

.reader-voice-text strong {
  color: var(--title-ink);
  font-weight: 600;
}

.reader-voice-more {
  margin: 8px 0 0 28px;
  font-size: 0.8125rem;
  line-height: 1.5;
  color: var(--text-secondary);
}

.reader-voice-more summary {
  cursor: pointer;
  width: fit-content;
  text-decoration: underline;
  text-underline-offset: 3px;
}

.reader-voice-more p {
  margin: 6px 0 0;
}

.reader-voice-note {
  margin-top: 10px;
}

.reader-followup-error {
  margin: 0 0 12px;
  font-size: 0.875rem;
  line-height: 1.45;
  color: var(--title-ink);
}

.reader-followup-error:empty {
  display: none;
}

.reader-followup .reader-followup-save {
  min-height: 52px;
  margin-top: 0;
  font-size: 1.0625rem;
}

/* Bij een cadeau is "Deel link" de hoofdactie; dan is deze knop wit. */
.reader-followup .reader-followup-save.is-quiet {
  background: var(--card);
  color: var(--title-ink);
}

.reader-followup-address {
  margin: 4px 0 0;
  font-size: 0.9375rem;
  font-weight: 600;
  color: var(--title-ink);
  overflow-wrap: anywhere;
}

.reader-followup-edit {
  margin: 12px 0 0;
  padding: 0;
  border: 0;
  background: none;
  color: var(--title-ink);
  font: inherit;
  font-size: 0.9375rem;
  font-weight: 600;
  text-decoration: underline;
  text-underline-offset: 3px;
  cursor: pointer;
}

.reader-followup-hint {
  margin: 12px 0 0;
  text-align: center;
  font-size: 0.8125rem;
  line-height: 1.5;
  color: var(--text-secondary);
}

.reader-followup-hint a {
  color: inherit;
}

/* Alleen voor schermlezers. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Copy / WhatsApp / E-mail row on the gift success screen. */
.share-actions {
  display: flex;
  justify-content: center;
  gap: 10px;
  margin-top: 16px;
}

.share-actions[hidden] {
  display: none;
}

.share-pill {
  flex: 1;
  min-height: 50px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: var(--radius-control);
  background: var(--card);
  color: var(--title-ink);
  font-size: 0.9375rem;
  font-weight: 600;
  text-align: center;
  text-decoration: none;
  box-shadow: 0 6px 14px -8px rgba(43, 39, 66, 0.3);
}

/* ── Claim page — in-browser preview player ───────────────────────────────── */
.claim-shell .step {
  align-items: start;
}

.claim-player {
  display: grid;
  justify-items: center;
  margin-top: 22px;
}

.claim-player[hidden] {
  display: none;
}

.claim-player audio {
  display: none;
}

.play-pill {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  min-height: 56px;
  padding: 0 26px;
  border: 0;
  border-radius: var(--radius-round);
  background: var(--card);
  color: var(--title-ink);
  font-size: 1rem;
  font-weight: 600;
  box-shadow: 0 8px 18px -10px rgba(43, 39, 66, 0.4);
}

.play-glyph {
  display: grid;
  place-items: center;
  line-height: 0;
  color: var(--brand-purple);   /* claim play-pill default; preview overrides to white */
}
.play-glyph .glyph-pause { display: none; }
.play-glyph .glyph-play { margin-left: 2px; }   /* optical centring of the triangle */
.is-playing .glyph-play { display: none; }
.is-playing .glyph-pause { display: block; }

/* Play/pause swap handled by the shared .is-playing glyph rules above. */

.claim-card .primary-link-button {
  margin-top: 22px;
}

/* heare.app/voorlezer — dezelfde kaart als de claimpagina. Eén kolom knoppen, de
   hoofdactie bovenaan en de tekstknoppen rustig eronder. `gap` in plaats van marges:
   verborgen knoppen tellen dan niet mee in de afstand. */
.voorlezer-card .gift-illustration {
  height: 168px;
}

.voorlezer-card .gift-illustration .hearo {
  width: 148px;
}

.voorlezer-address {
  margin: 4px 0 0;
  text-align: center;
  font-weight: 700;
  color: var(--title-ink);
  overflow-wrap: anywhere;
}

.voorlezer-actions {
  display: grid;
  gap: 12px;
  margin-top: 22px;
}

.voorlezer-card .voorlezer-actions > button {
  margin-top: 0;
}

.voorlezer-actions > [hidden] {
  display: none;
}

/* heare.app/voorlezer/stem (stemkloon, proef). Zelfde kaart als /voorlezer. De vraag
   krijgt meer ruimte dan een gewone bevestiging: eerst je eigen opname horen, dan de
   vier dingen die er gebeuren, dan pas kiezen — en ja en nee wegen even zwaar. */
.stem-card .stem-name {
  color: var(--brand-purple);
}

.stem-audio {
  margin: 18px 0 0;
  padding: 12px 14px;
  border-radius: var(--radius-card);
  background: var(--card-beige);
}

.stem-audio[hidden],
.stem-card [hidden] {
  display: none !important;
}

.stem-label {
  margin: 0 0 8px;
  font-size: 0.8125rem;
  font-weight: 600;
  color: var(--text-medium);
}

.stem-audio audio {
  display: block;
  width: 100%;
}

.stem-points {
  margin: 18px 0 0;
  padding: 0 0 0 1.1em;
  display: grid;
  gap: 6px;
  font-size: 0.9375rem;
  line-height: 1.5;
  color: var(--text-medium);
  text-align: left;
}

.stem-card .stem-email {
  margin-top: 18px;
}

.stem-hint {
  margin: 6px 0 0;
  font-size: 0.8125rem;
  line-height: 1.45;
  color: var(--text-secondary);
  text-align: left;
}

.stem-card .stem-only-me {
  margin: 14px 0 0;
  text-align: left;
}

.stem-card .stem-more {
  margin: 10px 0 0;
  text-align: left;
}

.stem-card .stem-error {
  margin: 12px 0 0;
  text-align: left;
}

/* Ja en nee naast elkaar, zelfde knop, zelfde gewicht. */
.stem-choice {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}

.stem-choice > button {
  margin-top: 0;
}

/* Microphone-denied recovery card (never a frozen 0:00 screen). Blocking by
   design (client ask): `position: fixed` + an oversized box-shadow dims the
   WHOLE viewport behind the card (no extra markup needed for a scrim) so the
   waveform/save controls read as unreachable, not just de-emphasised — the
   record/save buttons are ALSO actually disabled underneath (recorder-ui.js),
   this is the "you can't miss it" half. */
.mic-error {
  position: fixed; left: 50%; top: 50%; transform: translate(-50%, -50%);
  width: calc(100% - 2 * var(--screen-margin)); max-width: 340px;
  margin: 0;
  padding: 20px;
  background: var(--card);
  border-radius: 20px;
  box-shadow: 0 10px 28px rgba(0, 0, 0, 0.08), 0 0 0 9999px rgba(0, 0, 0, 0.45);
  text-align: center;
  z-index: 5;
}
.mic-error-title {
  margin: 0 0 6px;
  font-weight: 700;
  font-size: 1.0625rem;
  color: var(--title-ink);
}
.mic-error-body {
  margin: 0 0 16px;
  font-size: 0.9375rem;
  line-height: 1.4;
  color: var(--text-medium);
}
.mic-retry {
  appearance: none;
  border: none;
  cursor: pointer;
  width: 100%;
  min-height: 50px;
  padding: 14px 20px;
  border-radius: var(--radius-button);
  background: var(--brand-purple);
  color: #fff;
  font-weight: 600;
  font-size: 1.0625rem;
}
.mic-retry:active { opacity: 0.9; }

/* ── Saving overlay (Stoppen → preview feedback; blocks the transport) ──
   Shown immediately on tapping "Klaar" — the very first thing a dark-mode
   listener would have seen, and until this fix it was a white-on-white card,
   the first stop on the chain of dark-mode contrast bugs after "Klaar". */
.saving-overlay {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(255, 255, 255, 0.88);
  backdrop-filter: blur(6px);
  -webkit-backdrop-filter: blur(6px);
}
html.theme-dark .saving-overlay { background: rgba(29, 27, 56, 0.88); }
.saving-overlay[hidden] { display: none; }
.saving-card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  padding: 28px 32px;
  background: var(--card);
  border-radius: 22px;
  box-shadow: 0 18px 40px rgba(43, 39, 66, 0.14);
}
.saving-spinner {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  border: 4px solid rgba(108, 92, 255, 0.18);
  border-top-color: var(--brand-purple);
  animation: saving-spin 0.9s linear infinite;
}
@keyframes saving-spin { to { transform: rotate(360deg); } }
.saving-text {
  margin: 0;
  color: var(--title-ink);
  font-weight: 600;
  font-size: 1rem;
}

/* Branded confirm dialog (destructive "Opnieuw" guard). */
.confirm-overlay {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: rgba(43, 39, 66, 0.45);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}
.confirm-overlay[hidden] { display: none; }
.confirm-card {
  width: 100%;
  max-width: 360px;
  display: flex;
  flex-direction: column;
  gap: 10px;
  padding: 28px 24px 18px;
  background: var(--card);
  border-radius: 22px;
  box-shadow: 0 22px 50px rgba(43, 39, 66, 0.22);
  text-align: center;
}
.confirm-title {
  margin: 0;
  font-family: var(--font-display);
  font-size: 1.5rem;
  color: var(--title-ink);
}
.confirm-body {
  margin: 0 0 8px;
  font-size: 1rem;
  line-height: 1.45;
  color: var(--text-medium);
}
.confirm-card .confirm-accept { margin-top: 4px; }

/* ── Photo crop overlay (square crop, circular badge hint) ── */
.crop-overlay {
  position: fixed;
  inset: 0;
  z-index: 70;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(43, 39, 66, 0.6);
  padding: 20px;
}
.crop-overlay[hidden] { display: none; }
.crop-sheet {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 16px;
  width: min(92vw, 380px);
  padding: 22px;
  background: var(--room);
  border-radius: 22px;
  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.28);
}
.crop-title {
  margin: 0;
  color: var(--title-ink);
  font-size: 1.1875rem;
}
.crop-viewport {
  position: relative;
  width: min(76vw, 300px);
  aspect-ratio: 1;
  overflow: hidden;
  border-radius: 16px;
  background: #1d1a2e;
  touch-action: none;       /* the viewport owns the drag */
  cursor: grab;
}
.crop-viewport:active { cursor: grabbing; }
.crop-image {
  position: absolute;
  top: 0;
  left: 0;
  max-width: none;
  user-select: none;
  -webkit-user-select: none;
  pointer-events: none;     /* drags land on the viewport */
}
/* Darkened corners with a clear circle — the photo shows as a round badge. */
.crop-mask {
  position: absolute;
  inset: 0;
  pointer-events: none;
  border-radius: 50%;
  box-shadow: 0 0 0 9999px rgba(43, 39, 66, 0.45);
}
.crop-zoom {
  width: min(76vw, 300px);
  accent-color: var(--brand-purple);
}
.crop-actions {
  display: flex;
  flex-direction: column;
  gap: 4px;
  width: 100%;
}

/* ── Heare-story mode: hide markeer, show the read-aloud story panel (7942-1349) ── */
.story-text-panel {
  min-height: 0;            /* allow it to shrink so ONLY it scrolls, not the page */
  width: 100%;
  max-height: none;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  background: var(--reading-surface);
  border-radius: 14px;
  padding: 18px 20px;
  text-align: left;
  color: var(--title-ink);
  font-family: var(--font-display);
  font-size: 1.1875rem;
  line-height: 1.55;
  letter-spacing: 0.03em;
  /* Alleen de laatste paar procent verzachten, zodat de eerstvolgende regel die
     de lezer nodig heeft nooit wegvaagt. */
  -webkit-mask-image: linear-gradient(to bottom, #000 0, #000 96%, transparent 100%);
  mask-image: linear-gradient(to bottom, #000 0, #000 96%, transparent 100%);
}
.story-text-panel p { margin: 0 0 12px; }
.story-text-panel p:last-child { margin-bottom: 0; }

/* Placeholder lines while the real story loads (loadHeareStoryContent /
   selectCatalogStory) — pulsing bars, never borrowed text from another story.
   Same animation as the catalog skeleton, so the two loading moments read as
   one visual language. */
.story-text-skeleton-line {
  height: 16px;
  margin-bottom: 12px !important;
  border-radius: 4px;
  background: rgba(28, 24, 64, 0.08);
  animation: catalog-pulse 1.2s ease-in-out infinite;
}
.story-text-skeleton-line.story-text-skeleton-short { width: 60%; }
.story-text-title.story-text-skeleton-line { width: 75%; height: 19px; }
html.theme-dark .story-text-skeleton-line { background: rgba(255, 255, 255, 0.1); }
.story-text-title {
  font-family: var(--font-display);
  font-size: 1.1875rem;
  margin: 0 0 14px !important;
}
body.heare-story .transport-control--marker,
body.heare-story .marker-chip,
body.heare-story .ed-mark { display: none; }

/* ── Recorder editor (8029-1315): waveform + scrub + replace ──────────────── */
.nav-check { visibility: hidden; }
body:has(.step[data-step="recording"].active) .nav-check { visibility: visible; }
body:has(.step[data-step="recording"].active) .nav h1 { visibility: hidden; }
.nav-check { justify-self: end; }
/* De vink erft de kleur van de knop, zodat hij in beide standen meeschakelt. */
#editorSaveButton {
  width: auto;
  min-width: 88px;
  padding: 0 14px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  background: #d7d3e6;
  border-radius: var(--radius-round);
  /* Was #fff, en dat is op deze lichte vulling 1,46:1 — onleesbaar (testgroep
     2026-08-27). De "nog niets opgenomen"-stand hoort rustig te zijn, niet
     onzichtbaar; het verschil met "klaar om te bewaren" zit in de VULLING die
     paars wordt, niet in het al dan niet kunnen lezen van het woord.

     Een vaste waarde en NIET `var(--title-ink)`: die keert in het donkere thema om
     naar wit (zie de override onderaan dit bestand), en dan sta je weer met wit
     op lichtpaars — precies de val waar `.discount-apply-button` hierboven al
     een opmerking over heeft staan. De vulling van deze knop is in beide
     thema's dezelfde lichte kleur, dus de inkt hoort dat ook te zijn. 11,4:1. */
  color: #1c1840;
  font-size: 0.9375rem;
  font-weight: 700;
  transition: background .15s, color .15s;
}
#editorSaveButton.is-active { background: var(--brand-purple); color: var(--on-accent); }
#editorSaveButton:disabled { cursor: default; }

/* De golfvorm zit in dezelfde grid-cel als de voorleestekst (zie .record-controls
   verderop) en deelt daarmee hetzelfde oppervlak en dezelfde hoogte — vandaar
   volle celbreedte in plaats van de oude edge-to-edge band. */
.wave-view { align-self: stretch; width: 100%; }

.wave-area {
  position: relative; align-self: stretch; width: auto; height: 130px;
  background: var(--reading-surface); border-radius: 14px;
  overflow: hidden; touch-action: none;
}
.wave-canvas { position: absolute; inset: 0; width: 100%; height: 100%; }
/* now-line: fixed at the horizontal center, full band height, dots top & bottom */
.wave-playhead {
  position: absolute; top: 0; bottom: 0; width: 1.5px; margin-left: -0.75px;
  background: var(--brand-purple); pointer-events: none;
}
.wave-playhead::before, .wave-playhead::after {
  content: ""; position: absolute; left: 50%; width: 8px; height: 8px; margin-left: -4px;
  border-radius: 50%; background: var(--brand-purple);
}
.wave-playhead::before { top: -4px; } .wave-playhead::after { bottom: -4px; }
.wave-markers { position: absolute; inset: 0; pointer-events: none; }
/* Page marker (8029-1315): a THIN blue line with a dot at BOTH ends, mirroring the
   now-line — was a thicker 3px bar with only a top dot. */
.wave-pin { position: absolute; top: 0; bottom: 0; width: 2px; margin-left: -1px; background: #72c8fe; }
.wave-pin::before, .wave-pin::after {
  content: ""; position: absolute; left: 50%; width: 7px; height: 7px; margin-left: -3.5px;
  border-radius: 50%; background: #72c8fe;
}
.wave-pin::before { top: -3.5px; }
.wave-pin::after { bottom: -3.5px; }
/* scrolling timeline ticks (canvas), full-bleed under the band */
.wave-ticks {
  display: block; align-self: stretch; width: auto; height: 22px;
  margin: 4px calc(-1 * var(--screen-margin)) 0;
}

.editor-timer {
  margin: 16px 0 4px; text-align: center; font-family: var(--font-display);
  font-size: 2rem; font-weight: 400; color: var(--title-ink); letter-spacing: 1px;
}
/* Vaste rij-verdeling: play 25% / markeer 25% / opnemen altijd 50% — ongeacht
   welke tekst de knop toont (Opnemen, Pauzeer, Ga door, Vervang), zelfde
   hoogte voor alle drie (client, met screenshot-referentie). justify-content:
   center zodat play+opnemen samen als één geheel gecentreerd blijven staan
   wanneer de markeer-knop verborgen is (heareStory/read-along tekst-modus,
   `body.heare-story .ed-mark`) — zonder deze regel bleef flex's default
   flex-start over en schoven de resterende twee knoppen naar links (client). */
.editor-controls { display: flex; align-items: stretch; justify-content: center; gap: 12px; margin: 8px 0 0; width: 100%; }
.editor-controls:empty { display: none; }
.ed-circle {
  flex: 0 0 25%; height: 56px; display: grid; place-items: center; border: 0; border-radius: var(--radius-round);
  background: var(--card-beige); color: var(--title-ink); cursor: pointer;
}
.ed-circle:disabled { opacity: .5; cursor: default; }
.ed-play .ed-glyph-pause { display: none; }
.ed-play.is-playing .ed-glyph-play { display: none; }
.ed-play.is-playing .ed-glyph-pause { display: block; }
.ed-record-slot { flex: 0 0 50%; min-width: 0; display: flex; }
.ed-mic {
  width: 100%; height: 56px; display: grid; place-items: center; border: 0;
  border-radius: var(--radius-round); background: var(--brand-purple); cursor: pointer;
}
.ed-mic-label { display: none; }
.ed-mic.is-recording .ed-mic-glyph { display: none; }
/* While recording the button PAUSES (engine keeps the take, resumable via Ga door),
   so it shows a pause glyph (two bars), not a stop square. Stays the same 50%
   slot / height in every state — it never resizes as its label changes (client). */
.ed-mic.is-recording {
  display: flex; align-items: center; justify-content: center; gap: 6px;
  width: 100%; height: 56px; border-radius: var(--radius-round);
  color: #fff; font-size: 1.0625rem; font-weight: 700;
}
.ed-mic.is-recording::before { content: ""; width: 6px; height: 20px; border-radius: 2.5px; background: #fff; box-shadow: 10px 0 0 #fff; margin-right: 10px; }
.ed-mic.is-recording::after { content: none; }
.ed-mic.is-recording .ed-mic-label { display: inline; }
.ed-pill {
  display: block; width: 100%; height: 56px; padding: 0 26px;
  border: 0; border-radius: var(--radius-round); background: var(--brand-purple-wash); color: var(--brand-purple);
  font-family: var(--font-body); font-size: 1.0625rem; font-weight: 600; cursor: pointer;
}
.ed-pill.is-replace { background: var(--brand-purple); color: var(--on-accent); }
.ed-pill:disabled { opacity: .5; cursor: default; }   /* greyed while the take plays back */
/* class display rules beat the [hidden] attribute — restore it for editor controls.
   !important because html.theme-dark .ed-mic (element+2 classes) outranks this
   otherwise (2 classes) — without it, [hidden] silently lost in dark theme and
   mic + pill rendered at the same time, overflowing their shared 50% slot
   (client report: "Opnemen" and "Ga door" both visible side by side). */
.ed-mic[hidden], .ed-pill[hidden], .ed-circle[hidden] { display: none !important; }

/* ── v1.1 guided story selection (flow=v2) ────────────────────────────────────
   Catalog browse + book search. Light system, same tokens as the rest of the
   recorder — the accent (--brand-purple #6c5cff) already matches the Figma redesign;
   only the dark surface treatment is deferred (whole-flow decision). */

/* The step is a grid whose single column is content-sized by default, so the
   horizontally scrolling tile row would widen the whole page. Cap the column at
   the viewport and let the row scroll inside it. */
[data-step="catalog"].step.active { grid-template-columns: minmax(0, 1fr); }

.catalog-rows {
  display: grid;
  gap: 30px;
  margin-top: 30px;
  min-width: 0;
}

.catalog-row { min-width: 0; }

.catalog-tile-enter { animation: catalog-fade-in 380ms cubic-bezier(0.32, 0.72, 0, 1) both; }
@keyframes catalog-fade-in {
  from { opacity: 0; transform: translateY(14px) scale(0.97); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}
@media (prefers-reduced-motion: reduce) {
  .catalog-tile-enter { animation: none; }
}

.catalog-row h3 {
  margin: 0 0 14px;
  font-family: var(--font-display);
  font-size: 1.3125rem;
  font-weight: 400;
  color: var(--title-ink);
}

/* Tiles scroll horizontally and bleed past the 340px column so the row reads as
   scrollable without a scrollbar. */
.catalog-tiles {
  display: flex;
  align-items: flex-start;   /* covers stay flush top even when a title wraps to 2 lines */
  min-width: 0;
  gap: 10px;
  overflow-x: auto;
  scrollbar-width: none;
  margin-right: calc(-1 * var(--screen-margin));
  padding-right: var(--screen-margin);
  -webkit-overflow-scrolling: touch;
}

.catalog-tiles::-webkit-scrollbar { display: none; }

.catalog-tile {
  width: 140px;
  flex-shrink: 0;
  border: 0;
  padding: 0;
  background: transparent;
  text-align: left;
  cursor: pointer;
}

.catalog-tile:disabled { opacity: 0.6; }

.catalog-cover {
  /* Vierkant (klant, 30 september). Stond op 140×160 — een cover is overal in het
     product vierkant, van de thuistegels tot het bewaarscherm. */
  width: 100%;
  aspect-ratio: 1;
  position: relative;
  border-radius: var(--radius-card);
  background: rgba(136, 197, 254, 0.22);
  overflow: hidden;
}

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

.catalog-duration {
  position: absolute;
  top: 10px;
  left: 10px;
  padding: 3px 8px;
  border-radius: var(--radius-round);
  background: rgba(28, 24, 64, 0.45);
  color: var(--on-accent);
  font-size: 0.6875rem;
  font-weight: 500;
}

.catalog-tile-title {
  display: block;
  margin-top: 10px;
  font-family: var(--font-display);
  font-size: 0.9375rem;
  font-weight: 400;
  line-height: 1.35;
  color: var(--title-ink);
}

.catalog-tile.skeleton .catalog-cover { animation: catalog-pulse 1.2s ease-in-out infinite; }
.catalog-tile.skeleton .catalog-tile-title {
  width: 80%;
  height: 14px;
  border-radius: 4px;
  background: rgba(136, 197, 254, 0.22);
}

/* Stand-in for the row's h3 heading while categories are still loading — same
   box, no text, so 3 skeleton rows read as 3 categories, not 3 anonymous grids. */
.catalog-row-title-skeleton {
  width: 140px;
  height: 21px;
  margin: 0 0 14px;
  border-radius: 4px;
  background: rgba(136, 197, 254, 0.22);
  animation: catalog-pulse 1.2s ease-in-out infinite;
}

@keyframes catalog-pulse { 50% { opacity: 0.5; } }

.catalog-error { margin-top: 20px; }

/* ── Boek-detail (vóór opnemen) — zelfde kleurtaal als de catalogus-tegels
   hierboven (cover-tint + donkere pil), maar los i.p.v. tegel, gecentreerd. */
.story-detail-card { text-align: center; }

.story-detail-cover {
  width: min(211px, 60%);
  aspect-ratio: 1;
  margin: 0 auto 20px;
  border-radius: var(--radius-card);
  background: rgba(136, 197, 254, 0.22);
  overflow: hidden;
}

html.theme-dark .story-detail-cover { background: rgba(108, 92, 255, 0.22); }

.story-detail-cover img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

.story-detail-title {
  margin: 0 0 4px;
  font-family: var(--font-display);
  font-size: 1.25rem;
  font-weight: 400;
  line-height: 1.2;
  color: var(--title-ink);
}

.story-detail-attribution {
  margin: 0 0 14px;
  color: var(--text-secondary);
  font-size: 0.75rem;
}

.story-detail-tags {
  display: flex;
  justify-content: center;
  gap: 8px;
  margin-bottom: 18px;
}

.story-detail-tag {
  padding: 3px 8px;
  border-radius: var(--radius-round);
  background: rgba(28, 24, 64, 0.45);
  color: var(--on-accent);
  font-size: 0.75rem;
  font-weight: 500;
}

.story-detail-tags[hidden],
.story-detail-tag[hidden] { display: none; }

.story-detail-description {
  margin: 0 0 4px;
  color: var(--text-secondary);
  font-size: 0.9375rem;
  line-height: 1.4;
  /* Max 2 regels, zelfde als de native omschrijving. */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* "Bekijk verhaal" — een link, geen knop: de actie op deze stap blijft
   "Start opname". Layout thema-neutraal; kleur komt uit --brand-purple. */
.story-read-link {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  margin: 16px auto 0;
  padding: 6px 4px;
  border: 0;
  background: transparent;
  color: var(--brand-purple);
  font-family: var(--font-body);
  font-size: 1rem;
  font-weight: 600;
}

.story-read-link[hidden] { display: none; }

/* De leespagina zelf: hetzelfde leesoppervlak en dezelfde typografie als het
   tekstpaneel in de opnamestand, zodat het één verhaal blijft — alleen hier
   rustig op een eigen scherm i.p.v. naast de opnameknoppen. */
.story-read-title {
  margin: 0 0 16px;
  font-family: var(--font-display);
  font-size: 1.5625rem;
  font-weight: 400;
  line-height: 1.2;
  text-align: left;
  color: var(--title-ink);
}

.story-read-body {
  background: var(--reading-surface);
  border-radius: 14px;
  padding: 18px 20px;
  text-align: left;
  color: var(--title-ink);
  font-family: var(--font-display);
  font-size: 1.1875rem;
  line-height: 1.55;
  letter-spacing: 0.03em;
}

.story-read-body p { margin: 0 0 12px; }
.story-read-body p:last-child { margin-bottom: 0; }

html.theme-dark .story-read-body { color: var(--reading-ink); }

.story-detail-credits { margin-top: 20px; text-align: center; }

.story-detail-credits-heading {
  margin: 0 0 4px;
  font-size: 0.75rem;
  font-weight: 600;
  color: var(--text-secondary);
}

.story-detail-credits-text {
  margin: 0;
  font-size: 0.75rem;
  line-height: 1.4;
  color: var(--text-secondary);
}

.story-detail-credits-text a {
  color: inherit;
  text-decoration: underline;
}

.catalog-error[hidden] { display: none; }

.book-search-field {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 52px;
  margin-top: 20px;
  padding: 0 16px;
  border: 1px solid rgba(0, 0, 0, 0.03);
  border-radius: 12px;
  background: var(--card);
  color: var(--text-low);
}

.book-search-field input {
  flex: 1;
  min-height: 0;
  border: 0;
  padding: 14px 0;
  background: transparent;
}

.book-search-field input::-webkit-search-cancel-button { display: none; }

.book-search-clear {
  border: 0;
  padding: 4px;
  background: transparent;
  color: var(--text-secondary);
  font-size: 1.375rem;
  line-height: 1;
  cursor: pointer;
}

.book-results-label {
  margin: 26px 0 4px;
  font-family: var(--font-display);
  font-size: 1.3125rem;
  font-weight: 400;
  color: var(--title-ink);
}

.book-results-label[hidden] { display: none; }

.book-row {
  width: 100%;
  position: relative;
  display: grid;
  gap: 2px;
  padding: 14px 34px 14px 0;
  border: 0;
  border-bottom: 1px solid rgba(28, 24, 64, 0.08);
  background: transparent;
  text-align: left;
  cursor: pointer;
}

.book-row strong {
  font-size: 1.0625rem;
  font-weight: 600;
  color: var(--title-ink);
}

.book-row-author {
  color: var(--text-secondary);
  font-size: 0.875rem;
}

.book-row-badge {
  justify-self: start;
  margin-top: 6px;
  padding: 3px 8px;
  border-radius: var(--radius-round);
  background: rgba(108, 92, 255, 0.12);
  color: var(--brand-purple);
  font-size: 0.75rem;
  font-weight: 500;
}

.book-row .choice-chevron { right: 4px; }

.book-fallback { margin-top: 30px; }

/* ══ v1.1 dark theme (flow=v2) ═══════════════════════════════════════════════
   Client decision 2026-07-26: the v1.1 design IS the dark theme; today's look
   stays as the light theme. Everything below is a token override on
   <html class="theme-dark"> plus the few surfaces that need a different
   treatment on dark — component rules stay shared between both themes. */

/* Light falls from above the bed — the page is a touch lighter at the top. */
html.theme-dark body,
html.theme-dark .flow-shell {
  background: linear-gradient(to bottom, #26234a, var(--room) 42%);
  background-attachment: fixed;
}

html.theme-dark .choice-button[data-tone="parent"] { background: rgba(108, 92, 255, 0.15); }
html.theme-dark .choice-button[data-tone="gift"] { background: rgba(114, 200, 254, 0.12); }
html.theme-dark .choice-copy span,
html.theme-dark .choice-chevron { color: rgba(255, 255, 255, 0.5); }
html.theme-dark .voorlees-card-heare { background: rgba(114, 200, 254, 0.12); }
html.theme-dark .voorlees-card-eigen { background: rgba(108, 92, 255, 0.15); }
html.theme-dark { --outline-faint: rgba(255, 255, 255, 0.06); }
html.theme-dark input { border-color: var(--outline-faint); color: #ffffff; }
html.theme-dark .book-search-field { border-color: rgba(255, 255, 255, 0.06); }
html.theme-dark .book-row { border-bottom-color: rgba(255, 255, 255, 0.08); }
html.theme-dark .catalog-cover { background: rgba(108, 92, 255, 0.22); }
html.theme-dark .secondary-button { background: rgba(255, 255, 255, 0.08); color: #ffffff; }
html.theme-dark .destructive-button,
html.theme-dark .discount-apply-button { background: rgba(255, 255, 255, 0.1); }

/* ── Nav: back arrow, not a cross ──────────────────────────────────────────
   Figma 527-3047 shows the arrow on every screen of the guided flow; the × is
   only for a step that actually leaves the recorder. */
.icon-button .nav-glyph-back { display: none; }
.icon-button.is-back .nav-glyph-back { display: block; }
.icon-button.is-back .nav-glyph-close { display: none; }
#cancelButton { display: grid; place-items: center; }

/* ── iOS-style push / pop ──────────────────────────────────────────────────
   UIKit's own easing; the incoming screen carries the motion. */
/* A 28px nudge read as a fade with barely-there motion, not a push (client
   report: "alles verschijnt gewoon in één keer"). A real UIKit push slides the
   full screen width — this now does the same, so the incoming step visibly
   swipes in from the trailing/leading edge instead of just brightening. */
@keyframes step-push-in {
  from { opacity: 0; transform: translate3d(100%, 0, 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}

@keyframes step-pop-in {
  from { opacity: 0; transform: translate3d(-100%, 0, 0); }
  to   { opacity: 1; transform: translate3d(0, 0, 0); }
}

.step.step-push { animation: step-push-in 380ms cubic-bezier(0.32, 0.72, 0, 1) both; }
.step.step-pop  { animation: step-pop-in 340ms cubic-bezier(0.32, 0.72, 0, 1) both; }

@media (prefers-reduced-motion: reduce) {
  .step.step-push,
  .step.step-pop { animation: none; }
}

/* ── Recorder, dark (Figma 522-2763 / 520-2488 / 523-2815) ─────────────────
   The chosen story titles the screen, the text to read aloud is the centre of
   gravity, and the controls are two pills: white = play, purple = the verb. */

html.theme-dark .recording-panel { background: transparent; }

html.theme-dark .recording-header h2 {
  color: #ffffff;
  font-size: 2rem;
}

html.theme-dark .recording-subtitle {
  color: rgba(255, 255, 255, 0.5);
  font-size: 1.0625rem;
}

/* Surface, radius, typografie en masker staan in de gedeelde .story-text-panel
   (beide thema's delen de layout); hier alleen nog de inkt die per thema
   verschilt. */
html.theme-dark .story-text-panel {
  border: 0;
  color: var(--reading-ink);
}

html.theme-dark .story-text-panel .story-text-title {
  font-family: var(--font-display);
  color: #ffffff;
}

/* Everything not yet read sits back; the reader's eye stays on the top lines. */
html.theme-dark .story-text-panel p { color: var(--reading-ink-upcoming); }
html.theme-dark .story-text-panel p:nth-of-type(-n + 4) { color: var(--reading-ink); }

html.theme-dark .editor-timer {
  font-family: var(--font-display);
  font-size: 2rem;
  color: #ffffff;
}

/* Same 25/25/50 row as the light theme (shared .ed-circle / .ed-record-slot
   rules) — dark only recolors, sizing comes from the shared rules. */
html.theme-dark .editor-controls .ed-play {
  background: #ffffff;
  color: var(--room);
}

html.theme-dark .editor-controls .ed-play:disabled { opacity: 0.45; }

html.theme-dark .ed-mic {
  border-radius: var(--radius-round);
  background: var(--brand-purple);
  gap: 8px;
}

html.theme-dark .ed-mic-label {
  display: inline;
  color: #ffffff;
  font-size: 1.0625rem;
  font-weight: 600;
}

/* De Figma-pil is tekst, geen icoon: met een microfoon bóven het woord wordt een
   pil van 55px krap en leest hij als twee dingen. */
html.theme-dark .ed-mic .ed-mic-glyph { display: none; }

html.theme-dark .ed-mic {
  display: flex;
  align-items: center;
  justify-content: center;
}

html.theme-dark .ed-pill {
  border-radius: var(--radius-round);
  background: var(--brand-purple);
  color: #ffffff;
  font-weight: 600;
}

html.theme-dark .marker-chip {
  background: rgba(255, 255, 255, 0.1);
  color: rgba(255, 255, 255, 0.7);
}

html.theme-dark .marker-chip span {
  color: rgba(255, 255, 255, 0.85);
}

html.theme-dark .wave-area,
html.theme-dark .wave-ticks { opacity: 0.85; }

/* The wordmark is a dark-ink SVG; invert it on the dark theme. */
html.theme-dark .nav-wordmark img { filter: invert(1) brightness(1.6); }

/* Subtitles inside cards are hard-coded ebony-on-light in several places; on the
   dark theme they need the inverted muted ink instead. */
html.theme-dark .choice-copy span,
html.theme-dark .voorlees-text .card-subtitle,
html.theme-dark .choice-card .card-subtitle,
html.theme-dark .pay-choice-text .card-subtitle,
html.theme-dark .body-copy,
html.theme-dark .muted {
  color: rgba(255, 255, 255, 0.55);
}

html.theme-dark .voorlees-chevron,
html.theme-dark .choice-chevron {
  color: rgba(255, 255, 255, 0.45);
}

/* ── Recorder layout, beide thema's (Figma 522-2763) ───────────────────────
   Figma order is: title → text to read → timeline → timer → two pills side by
   side. The DOM order differs, so this grid re-places the same elements with
   grid areas — no DOM or recorder-logic changes, so the engine keeps working
   exactly as it does today.

   Stond tot 2026-08-18 alleen achter `html.theme-dark`, waardoor LIGHT nog op de
   oude flex-kolom viel en de voorleestekst onderaan het scherm belandde in plaats
   van bovenaan ("dit is oud", client). Layout is nu thema-neutraal; alleen
   KLEUREN verschillen nog per thema. */
.record-controls {
  display: grid;
  grid-template-columns: 155px 155px;
  grid-template-areas:
    "story story"
    "wave  wave"
    "ticks ticks"
    "timer timer"
    "controls controls";
  justify-content: center;
  align-content: start;
  gap: 14px 15px;
  padding-bottom: 12px;
}

.story-text-panel { grid-area: story; margin: 0; max-height: 46vh; }
.editor-timer { grid-area: timer; justify-self: center; margin: 6px 0 2px; }
/* opnemen/play/mark live in .editor-controls's own 25/25/50 row now (shared
   rules above) — spans the full grid width instead of a separate "rec" column. */
.editor-controls { grid-area: controls; margin: 0; width: 100%; }

.marker-chip { grid-column: 1 / -1; justify-self: center; }
.story-text-panel { align-self: start; }

/* In story mode the markers are hidden anyway; keep the grid tight. */
body.heare-story .marker-chip { display: none; }

/* Keep the waveform inside its grid cell — the old light theme let it bleed past
   the screen margin with a negative margin, which inside this grid overlapped the
   story text. */
.wave-area {
  margin: 0;
  width: auto;
  align-self: stretch;
}

/* Figma's left control is a WHITE pill (dark glyph), including while it is still
   disabled — a grey pill reads as broken rather than "nothing to play yet". */
html.theme-dark .editor-controls .ed-play:disabled {
  opacity: 1;
  background: #ffffff;                 /* stays the white pill from the Figma */
  color: rgba(29, 27, 56, 0.32);       /* only the glyph reads as "nothing yet" */
}

/* The panel's row must be allowed to shrink, or a long story overflows its grid
   row and slides under the waveform. minmax(0,1fr) + min-height:0 is what lets
   the text scroll INSIDE its row instead of spilling out of it. */
.record-controls {
  grid-template-rows: minmax(0, 1fr) auto auto auto auto;
  min-height: 0;
}

.story-text-panel {
  min-height: 0;
  max-height: none;
  overflow-y: auto;
}

/* minmax(0,1fr) only constrains a row when the grid itself has a definite height.
   The recorder step is content-sized, so give it a real height via the flex
   chain — then the story row can shrink and scroll internally. */
.step.recording-step.active {
  display: flex;
  min-height: 0;
  height: calc(100dvh - var(--nav-height));
  padding-bottom: calc(12px + env(safe-area-inset-bottom));
}

.recording-panel {
  display: flex;
  flex-direction: column;
  flex: 1;
  min-height: 0;
  width: min(100%, 340px);
  margin: 0 auto;
}

.record-controls {
  flex: 1;
  min-height: 0;
}

/* align-self:start sizes the panel to its content, which is exactly what makes it
   overflow its row — it must stretch to the row and scroll inside it. */
.story-text-panel {
  align-self: stretch;
  height: 100%;
}

/* ── Meelezen (flow=v2) ────────────────────────────────────────────────────
   Twee woordstates, allebei afgeleid van hoe ver de lezer zelf gescrold heeft.
   Bewerken zit in de golfvorm, dus hier valt niets te selecteren. */

.ra-host { position: relative; }

.ra-section {
  margin: 0 0 14px;
  color: var(--reading-ink-upcoming);
}

.ra-word { transition: color 240ms ease; }

/* Gelezen tekst is de sterkste inkt, nog te lezen tekst blijft LEESBAAR. Dat
   laatste is de kern: op 20% dekking kon de lezer niet verder zodra hij vooruit
   las, en dan blokkeert de hulp precies waar hij zou moeten helpen. Both tokens
   flip with the theme — this used to be hardcoded white, invisible the moment
   a light session reached it (client report, 2026-07-31: "De drie biggetjes"
   read as a near-blank card). */
.ra-word.is-read { color: var(--ra-word-read); }
.ra-word.is-upcoming { color: var(--ra-word-upcoming); }

.record-controls {
  grid-template-areas:
    "story story"
    "toggle toggle"
    "timer timer"
    "controls controls";
  grid-template-rows: minmax(0, 1fr) auto auto auto;
}

.view-toggle { grid-area: toggle; justify-self: center; }

/* Tekst en golfvorm delen ÉÉN cel, zodat ze per definitie even hoog zijn: wisselen
   toont hetzelfde vak, alleen anders weergegeven. */
.story-text-panel,
.wave-view { grid-area: story; }

.wave-view {
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-height: 0;
  /* Net als het tekstpaneel: uitrekken tot de rij, anders krimpt de weergave tot
     zijn eigen inhoud en zijn de twee views niet meer even hoog. */
  align-self: stretch;
  height: 100%;
}

.wave-view[hidden] { display: none; }

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


/* ── Native tekstselectie uit ──────────────────────────────────────────────
   De blauwe OS-selectie en het "Kopiëren/Opzoeken"-menu schieten los bij het scrollen
   door de verhaaltekst en laten de app kapot lijken. Uit dus — behalve waar iemand
   tekst juist MOET kunnen kopiëren of bewerken. */

body {
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}

/* De claim-link is de handmatige uitweg als de deel-knop faalt; invoervelden
   moeten bewerkbaar blijven; en een technische fout wil je kunnen doorsturen. */
input,
textarea,
.claim-link,
.technical-error {
  -webkit-user-select: text;
  user-select: text;
  -webkit-touch-callout: default;
}


/* ── Weergave-toggle ───────────────────────────────────────────────────────
   Alleen zichtbaar als er tekst is om naartoe te wisselen. */

.view-toggle {
  display: inline-flex;
  gap: 4px;
  padding: 4px;
  border-radius: var(--radius-round);
  background: var(--toggle-bg);
}

.view-toggle[hidden] { display: none; }

.view-toggle-btn {
  width: 46px;
  height: 34px;
  display: grid;
  place-items: center;
  border: 0;
  border-radius: var(--radius-round);
  background: transparent;
  color: var(--toggle-ink);
  cursor: pointer;
  transition: background 200ms ease, color 200ms ease;
}

.view-toggle-btn.is-active {
  background: var(--brand-purple);
  color: #ffffff;
}

/* ── Opname-indicator ──────────────────────────────────────────────────────
   Alleen in de tekstweergave, waar geen golfvorm laat zien dat er iets binnenkomt.
   Ademt mee met het niveau in plaats van blind te knipperen: stilte en een dode
   microfoon zijn zo zichtbaar. */

.editor-timer { display: flex; align-items: center; justify-content: center; gap: 10px; }

.rec-dot {
  width: 11px;
  height: 11px;
  border-radius: 50%;
  background: var(--brand-purple);
  transition: transform 120ms ease-out, opacity 120ms ease-out;
}

.rec-dot[hidden] { display: none; }

/* Bij beperkte beweging schrijft `recorder-ui.js` geen schaal meer op deze stip
   maar twee opaciteitsstanden — hoort je wel / hoort je niet. Een overgang is
   daar overbodig, en het globale vangnet onderaan dit bestand zet hem toch al
   uit; dit staat er voor het geval iemand dat blok ooit inperkt. */
@media (prefers-reduced-motion: reduce) {
  .rec-dot { transition: none; }
}

/* Doorgestreept prijs-anker naast de introductieprijs. Bewust gedempt: het
   bedrag dat wordt afgeschreven moet het grootst en donkerst zijn — Apple
   keurt paywalls af waar de marketingprijs de echte overschreeuwt. */
.price-was {
  color: var(--text-secondary);
  font-weight: 600;
  margin-right: 2px;
}

/* De rij met wat je zelf typte. Hij LEENT .book-row, dus vorm, hoogte en chevron
   zijn gelijk aan een echte treffer; het verschil is wat er ontbreekt (auteur,
   badge). Geen eigen kleur meer: die gestippelde paarse knop maakte het scherm
   onrustig terwijl dit gewoon een keuze in de lijst is. */
.book-typed[hidden] { display: none; }

/* De echte boekomslag op de bewaarkaart: vullend en bijgesneden, zodat een
   staand omslag hetzelfde vierkant vult als de gegenereerde tegels. */
.pay-cover-photo {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* ── Indrukreactie ─────────────────────────────────────────────────────────
   De recorder had vier `:active`-regels, waarvan één op `.record-orb` — een
   selector die in geen enkel HTML-bestand voorkwam. Dat hele blok is hierboven
   weggehaald. De knoppen die er wél toe doen, te beginnen met de opnameknop
   zelf, gaven niets terug bij het aanraken.

   `:not(:disabled)` waar een knop uit kan staan: een knop die niets doet hoort
   ook niet te reageren. */
.ed-mic,
.ed-pill,
.primary-button,
.primary-link-button,
.play-pill,
.share-pill,
.marker-pin {
  -webkit-tap-highlight-color: transparent;
  /* De BASISovergang, en die ontbrak. De snelle 100 ms staat in `:active`, dus
     indrukken gaat meteen; maar zonder deze regel had loslaten géén overgang en
     sprong de knop in één beeld terug. Geen van deze klassen heeft elders een
     transition, dus hier overschrijft hij niets. */
  transition: transform 160ms var(--ease-out);
}

.ed-mic:active,
.ed-pill:active,
.primary-button:active:not(:disabled),
.primary-link-button:active,
.play-pill:active,
.share-pill:active {
  transform: scale(0.96);
  transition: transform 100ms ease-out;
}

/* De markeerpin plaatst zichzelf met `translateY(-50%)`; die moet mee, anders
   springt hij naar beneden zodra je hem aanraakt. */
.marker-pin:active {
  transform: translateY(-50%) scale(0.9);
  transition: transform 100ms ease-out;
}

/* Deze pagina laadt `landing.css` niet, en heeft dus ook niet de globale
   reduced-motion-reset die daar staat. Wat hierboven aan beweging bijkomt, dekken
   we hier zelf af. */
@media (prefers-reduced-motion: reduce) {
  .ed-mic:active,
  .ed-pill:active,
  .primary-button:active:not(:disabled),
  .primary-link-button:active,
  .play-pill:active,
  .share-pill:active {
    transform: none;
    opacity: 0.75;
  }

  .marker-pin:active {
    transform: translateY(-50%);
    opacity: 0.75;
  }
}

/* ── Beperkte beweging: het vangnet dat deze pagina's misten ───────────────
   Het globale blok staat in `landing.css`, en de recorder, de claimpagina en de
   auth-callback laden dat bestand niet — die dragen alleen dit. Alles wat hier
   beweegt viel dus buiten de instelling, inclusief drie oneindige lussen: de
   wachtspinner en de skeletonpuls op zowel de catalogus als de voorleestekst.

   `animation-iteration-count: 1` staat hier wél en in `landing.css` niet, en dat
   is met opzet. Een oneindige animatie die je alléén tot 0,01ms verkort blijft
   eeuwig doorlopen; elk beeldje bemonstert dan een willekeurige fase en het
   resultaat FLIKKERT in plaats van dat het stilstaat — erger dan de animatie
   zelf. `landing.css` heeft geen enkele oneindige animatie, dus daar speelt het
   niet en hoeft die regel er ook niet bij. */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    scroll-behavior: auto !important;
    transition-duration: 0.01ms !important;
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
  }

  /* De enige uitzondering: de wachtspinner blijft draaien. Een bevroren spinner
     leest als vastgelopen, en dat is erger dan de beweging — iOS laat zijn eigen
     activity indicator bij deze instelling ook gewoon doordraaien. De drie
     skeletonpulsen hierboven juist niet: een stilstaand grijs blok zegt precies
     even goed "aan het laden". */
  .saving-spinner {
    animation-duration: 0.9s !important;
    animation-iteration-count: infinite !important;
  }
}

/* ── Verminderde doorzichtigheid ───────────────────────────────────────────
   Twee vlakken in de recorder leggen zich met een vervaging over de inhoud, en
   allebei dragen ze tekst. Bij deze instelling worden ze dekkend en vervalt de
   blur — dat is precies wat de instelling vraagt, en het kost niets: de kaart
   die erop staat was al ondoorzichtig.

   `html.theme-dark .saving-overlay` staat er apart bij omdat de bestaande
   donkere regel specifieker is dan een kale `.saving-overlay`; zonder die
   tweede helft zou hij in het donkere thema gewoon blijven staan. */
@media (prefers-reduced-transparency: reduce) {
  .saving-overlay,
  html.theme-dark .saving-overlay {
    background: var(--card);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }

  /* Een scrim, geen oppervlak: die hoort niet wit te worden maar bijna dicht,
     zodat de kaart erboven nog steeds als een kaart leest. */
  .confirm-overlay {
    background: rgba(43, 39, 66, 0.96);
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
  }
}

/* ── Overlays: niet meer in één beeld ──────────────────────────────────────
   De opslag- en de bevestigingslaag verschenen en verdwenen in één beeld: een
   schermvullende verduistering die zomaar in beeld sprong. Nu een korte
   vervaging, en de kaart erop komt van net iets kleiner. Gecentreerd, want een
   modal hoort bij geen knop.

   `@starting-style` levert de beginwaarde bij het tonen. `allow-discrete` op
   `display` houdt het element zichtbaar tot het vervagen klaar is, zodat óók
   weggaan een overgang heeft. Een browser die dit niet kent toont en verbergt
   precies zoals voorheen — hij slaat de hele declaratie over. */
.saving-overlay,
.confirm-overlay {
  opacity: 1;
  transition: opacity 200ms var(--ease-out), display 200ms allow-discrete;
}

.saving-overlay[hidden],
.confirm-overlay[hidden] {
  opacity: 0;
  transition-duration: 150ms;   /* weggaan sneller dan komen */
}

.saving-card,
.confirm-card {
  transition: transform 200ms var(--ease-out), opacity 200ms var(--ease-out);
}

@starting-style {
  .saving-overlay:not([hidden]),
  .confirm-overlay:not([hidden]) {
    opacity: 0;
  }

  .saving-overlay:not([hidden]) .saving-card,
  .confirm-overlay:not([hidden]) .confirm-card {
    opacity: 0;
    transform: scale(0.97);
  }
}
