/* =========================================================================
   Cadence — design system
   Plain, quiet, paper-like. Inspired by Notion's neutral palette and
   Ruby on Rails' documentation red. Flat surfaces, 1px rules, no shadows
   except where a floating layer genuinely needs one.
   ========================================================================= */

/* ---------- reset ---------- */
*, *::before, *::after { box-sizing: border-box; }
html, body, h1, h2, h3, h4, p, figure, blockquote, dl, dd, ol, ul { margin: 0; }
/* The UA rule for [hidden] is display:none, but any author display rule (a .btn,
   for example) wins over it. Buttons such as the practice screen's Pause and
   Finish carry the attribute and must actually stay off screen until they apply. */
[hidden] { display: none !important; }
html {
  /* Safari on iOS inflates text in landscape unless it is told not to. */
  -webkit-text-size-adjust: 100%;
  text-size-adjust: 100%;
  /* A grey flash on every tap is the browser's default; the :active rules below
     are the deliberate feedback instead. */
  -webkit-tap-highlight-color: transparent;
}
ol[role="list"], ul[role="list"] { list-style: none; padding: 0; }
img, svg { display: block; max-width: 100%; }
button, input, select, textarea { font: inherit; color: inherit; }
button { background: none; border: 0; cursor: pointer; }
a { color: inherit; }
/* Safari before 15.4 has no :focus-visible, and a ring written only with it
   disappears there. Give every focused control the same ring on :focus, then in
   browsers that understand :focus-visible remove it for a pointer focus so only
   the keyboard path shows it. */
:focus { outline: var(--focus-ring); outline-offset: var(--focus-ring-offset); border-radius: 2px; }
:focus-visible { outline: var(--focus-ring); outline-offset: var(--focus-ring-offset); border-radius: 2px; }
@supports selector(:focus-visible) {
  :focus:not(:focus-visible) { outline: none; }
}

/* ---------- tokens ---------- */
:root {
  color-scheme: light;

  --bg:            #ffffff;
  --bg-subtle:     #f7f7f5;
  --bg-hover:      #f1f1ef;
  --bg-active:     #e9e9e6;
  --surface:       #ffffff;
  --surface-raised:#ffffff;

  --text:          #37352f;
  --text-strong:   #1d1c1a;
  --muted:         #787774;
  --faint:         #9b9a97;

  --border:        #e9e9e7;
  --border-strong: #d3d1cb;

  --accent:        #c8102e;   /* Rails red */
  --accent-hover:  #a20d25;
  --accent-soft:   #fdf2f3;
  --word-mark-bg:  #f9d9df;
  --word-mark-line: #c8102e;
  --accent-border: #f0c9ce;

  --good:          #0f7b6c;
  --good-bg:       #edf3ec;
  --good-border:   #c3ddd6;
  --warn:          #b25e09;
  --warn-bg:       #faece0;
  --warn-border:   #edcfae;
  --bad:           #d44c47;
  --bad-bg:        #fdecec;
  --bad-border:    #f2c9c7;

  --focus:         #c8102e;
  /* One ring for every control: buttons, inputs, selects, chips and dialog
     actions. Change it here, not in each component. */
  --focus-ring:         2px solid var(--focus);
  --focus-ring-offset:  2px;
  /* A focused field is a little warmer, not selected. Light mode can keep the border
     mostly neutral; dark mode needs more accent to stay at 3:1 against its surface. */
  --field-focus-border: var(--border-strong);
  --field-focus-border: color-mix(in srgb, var(--accent) 50%, var(--border-strong));
  --field-focus-ring:   rgba(200, 16, 46, .14);
  --field-focus-ring:   color-mix(in srgb, var(--accent) 20%, transparent);

  /* Safe-area insets, readable from JavaScript where a position is computed
     (the definition popover). Zero everywhere a notch/home indicator does not
     exist, and when env() itself is unavailable. */
  --safe-top:    env(safe-area-inset-top, 0px);
  --safe-right:  env(safe-area-inset-right, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-left:   env(safe-area-inset-left, 0px);

  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI Variable Text", "Segoe UI",
               Roboto, "Helvetica Neue", Arial, "PingFang SC", "Microsoft YaHei", sans-serif;
  --font-serif: "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua",
                Georgia, "Times New Roman", serif;
  --font-mono: ui-monospace, SFMono-Regular, "Cascadia Mono", Consolas, "Liberation Mono", monospace;

  --radius:    3px;   /* Notion's corner */
  --radius-lg: 6px;
  --radius-pill: 999px;

  --measure: 44rem;   /* reading column */
  --page:    60rem;   /* page column   */
  --reading-scale: 1; /* set from Settings */

  --shadow-pop: 0 1px 2px rgba(15,15,15,.05), 0 8px 24px rgba(15,15,15,.12);
}

[data-theme="dark"] {
  color-scheme: dark;

  --bg:            #191919;
  --bg-subtle:     #222222;
  --bg-hover:      #2c2c2c;
  --bg-active:     #353535;
  --surface:       #202020;
  --surface-raised:#252525;

  --text:          #d4d4d4;
  --text-strong:   #efefef;
  --muted:         #9a9a97;
  --faint:         #7c7c79;

  --border:        #2f2f2f;
  --border-strong: #414141;

  --accent:        #e5484d;
  --accent-hover:  #f2666a;
  --accent-soft:   #2a1a1b;
  --word-mark-bg:  #4b1d24;
  --word-mark-line: #ff8b96;
  --accent-border: #4a2528;

  --good:          #4cc2a8;
  --good-bg:       #16241f;
  --good-border:   #23503f;
  --warn:          #e0a458;
  --warn-bg:       #2a2117;
  --warn-border:   #513d22;
  --bad:           #f0736e;
  --bad-bg:        #2b1a1a;
  --bad-border:    #52282a;

  --focus:         #e5484d;
  --field-focus-border: var(--border-strong);
  --field-focus-border: color-mix(in srgb, var(--accent) 70%, var(--border-strong));
  --shadow-pop: 0 1px 2px rgba(0,0,0,.4), 0 8px 24px rgba(0,0,0,.5);
}

/* ---------- base ---------- */
body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-sans);
  font-size: 16px;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  min-height: 100dvh;
}

.skip-link {
  position: absolute; left: -9999px; top: 0;
  background: var(--accent); color: #fff; padding: 8px 14px; z-index: 100;
  border-radius: 0 0 var(--radius) 0;
}
.skip-link:focus { left: 0; }

/* ---------- top bar ---------- */
/* With viewport-fit=cover the page runs under the status bar / notch in an
   installed app and in landscape, so the bar reserves the top inset itself.
   In a normal Safari tab the inset is 0 and this changes nothing. */
.topbar {
  position: sticky; top: 0; z-index: 40;
  padding-top: env(safe-area-inset-top, 0px);
  /* The opaque first background is the fallback for iOS 16.0-16.1, where
     color-mix() does not exist and the declaration below is dropped. */
  background: var(--bg);
  background: color-mix(in srgb, var(--bg) 88%, transparent);
  /* Safari only exposed backdrop-filter unprefixed in 18; iOS 16/17 need the
     prefixed property, and without either the fallback above is opaque. */
  -webkit-backdrop-filter: saturate(180%) blur(10px);
  backdrop-filter: saturate(180%) blur(10px);
  border-bottom: 1px solid var(--border);
}
.topbar-inner {
  max-width: var(--page);
  margin: 0 auto;
  /* Plain padding first: if env() or max() is unavailable the declaration is
     dropped, and the page keeps its original gutter rather than losing it. */
  padding: 0 24px;
  padding: 0 max(24px, env(safe-area-inset-left, 0px)) 0 max(24px, env(safe-area-inset-right, 0px));
  height: 46px;
  display: flex;
  align-items: center;
  gap: 28px;
}
.brand {
  display: inline-flex; align-items: center; gap: 8px;
  text-decoration: none; font-weight: 600; letter-spacing: -0.01em;
  color: var(--text-strong); font-size: 15px;
  flex: none;
}
.brand-mark {
  width: 14px; height: 14px; border-radius: 3px;
  background: var(--accent);
  position: relative;
}
.brand-mark::after {
  content: ""; position: absolute; inset: 4px 4px 4px 4px;
  background: var(--bg); border-radius: 1px;
  /* a small solid reading bar inside the mark */
  inset: 5.5px 3px 5.5px 3px;
}
.nav { display: flex; align-items: center; gap: 2px; margin-left: auto; }
.nav a {
  text-decoration: none; font-size: 14px; color: var(--muted);
  padding: 5px 10px; border-radius: var(--radius);
  transition: background .12s ease, color .12s ease;
  white-space: nowrap;
}
.nav a:hover { background: var(--bg-hover); color: var(--text-strong); }
.nav a[aria-current="page"] { color: var(--text-strong); font-weight: 500; background: var(--bg-hover); }
/* The Me tab keeps one hit area whether it shows the word or the signed-in person. */
.nav a[data-route="me"] {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 39px; height: 30px; box-sizing: border-box;
}
.nav a[data-route="me"] .ico { width: 18px; height: 18px; }

/* ---------- layout ---------- */
.main {
  flex: 1 0 auto;
  width: 100%;
  max-width: var(--page);
  margin: 0 auto;
  padding: 44px 24px 96px;
  padding: 44px max(24px, env(safe-area-inset-left, 0px)) 96px max(24px, env(safe-area-inset-right, 0px));
}
.main:focus { outline: none; }
.narrow { max-width: var(--measure); }

.page-head { margin-bottom: 32px; }
/* With no eyebrow the title is the first thing; the browser's default h1 margins
   would otherwise leave a gap above it and under a subtitle-less head. */
.page-head > .title:first-child { margin-top: 0; }
.page-head > .title:last-child { margin-bottom: 0; }
.eyebrow {
  font-size: 13px; color: var(--muted); margin-bottom: 10px;
  letter-spacing: .01em;
}
h1.title {
  font-size: 34px; line-height: 1.2; font-weight: 650;
  letter-spacing: -0.022em; color: var(--text-strong);
}
h1.title.sm { font-size: 26px; }
.subtitle { margin-top: 10px; color: var(--muted); font-size: 16px; max-width: 40rem; line-height: 1.6; }

.section { margin-top: 44px; }
.section-head {
  display: flex; align-items: baseline; gap: 12px;
  margin-bottom: 14px;
}
h2.section-title {
  font-size: 13px; font-weight: 600; color: var(--muted);
  letter-spacing: .04em; text-transform: uppercase;
}
.section-head .spacer { flex: 1; }
.section-head a { font-size: 13px; color: var(--muted); text-decoration: none; }
.section-head a:hover { color: var(--accent); text-decoration: underline; }
/* A quiet action at the far end of a section heading; no button chrome. */
.clear-recent {
  background: none; border: 0; padding: 0; margin: 0;
  font: inherit; font-size: 13px; color: var(--muted);
  cursor: pointer; text-decoration: none;
}
.clear-recent:hover { color: var(--accent); text-decoration: underline; }

.rule { height: 1px; background: var(--border); border: 0; margin: 40px 0; }

/* ---------- buttons ---------- */
.btn {
  --btn-bg: var(--surface);
  --btn-fg: var(--text);
  --btn-bd: var(--border-strong);
  --btn-h: 35px;
  --btn-px: 13px;
  display: inline-flex; align-items: center; justify-content: center; gap: 7px;
  box-sizing: border-box;
  height: var(--btn-h);
  padding: 0 var(--btn-px);
  background: var(--btn-bg); color: var(--btn-fg);
  border: 1px solid var(--btn-bd);
  border-radius: var(--radius);
  font-size: 14px; font-weight: 500; line-height: 1;
  text-decoration: none; white-space: nowrap;
  transition: background .12s ease, border-color .12s ease, color .12s ease;
}
.btn:hover { background: var(--bg-hover); }
.btn:active { background: var(--bg-active); }
.btn[disabled], .btn[aria-disabled="true"] { opacity: .45; pointer-events: none; }

.btn-primary {
  --btn-bg: var(--text-strong); --btn-fg: var(--bg); --btn-bd: var(--text-strong);
}
.btn-primary:hover { --btn-bg: #000; }
[data-theme="dark"] .btn-primary:hover { --btn-bg: #fff; }

.btn-danger { --btn-bg: var(--accent); --btn-fg: #fff; --btn-bd: var(--accent); }
.btn-danger:hover { --btn-bg: var(--accent-hover); }
[data-theme="dark"] .btn-danger { --btn-fg: #151515; }

.btn-quiet { --btn-bd: transparent; --btn-bg: transparent; color: var(--muted); }
.btn-quiet:hover { background: var(--bg-hover); color: var(--text-strong); }

/* The one size scale: small actions, ordinary controls, and 42 px primary controls. */
.btn-sm { --btn-h: 30px; --btn-px: 9px; font-size: 13px; }
.btn-lg { --btn-h: 42px; --btn-px: 18px; font-size: 15px; }
.btn-icon {
  --btn-h: 38px; --btn-px: 0; width: 38px; padding: 0;
}
.btn-icon.btn-sm { --btn-h: 30px; width: 30px; }
.btn-icon.btn-lg { --btn-h: 42px; width: 42px; }
.btn .ico { width: 15px; height: 15px; flex: none; }
/* The popover's three actions share the small height but use tighter type and
   padding on a phone so Hear it / Keep word / Keep sentence stay on one row. */
@media (max-width: 390px) {
  .btn-compact { --btn-px: 6px; font-size: 12px; gap: 4px; }
}
@media (max-width: 359px) {
  .btn-compact { --btn-px: 4px; font-size: 11px; gap: 3px; }
}

.btn-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
/* The two ways forward, shared by the library and Today. The library's pinned row
   already fits both labels on one phone line by flexing them and tightening type
   below 360 px; Today uses the same class rather than a second arrangement. */
.ways-row { display: flex; flex-wrap: nowrap; gap: 8px; align-items: center; }
.ways-row .btn { flex: 1 1 auto; white-space: nowrap; }
@media (max-width: 360px) {
  .ways-row .btn { padding-inline: 8px; font-size: 13px; }
}

/* ---------- compact select ---------- */
/* A real <select> — on iOS it still opens the native picker — with the UA arrow
   and its floor width removed. It is as wide as the longest option plus this
   chevron, which is what keeps the reader's transport on one phone row. */
.select-compact {
  width: auto;
  max-width: 100%;
  flex: none;
  -webkit-appearance: none;
  appearance: none;
  /* The right padding is the chevron's room, not a second gutter: touch screens
     force 16px text to stop iOS zooming, and this keeps the control inside its
     68px budget there as well as at the designed 13px. */
  padding: 5px 18px 5px 6px;
  font-size: 13px;
  line-height: 1.35;
  color: var(--text);
  background-color: var(--surface);
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background-image:
    linear-gradient(45deg, transparent 50%, var(--muted) 50%),
    linear-gradient(135deg, var(--muted) 50%, transparent 50%);
  background-position:
    calc(100% - 11px) calc(50% + 1px),
    calc(100% - 7px) calc(50% + 1px);
  background-size: 4px 4px, 4px 4px;
  background-repeat: no-repeat;
  cursor: pointer;
}
.select-compact:hover { background-color: var(--bg-hover); }

/* The reader's speed control sits in the row like the quiet Read aloud link: no
   border, no fill, no button box — only the current word and the chevron. The
   chevron is made stronger than the filled control's, because with the border gone
   it is what says "this is a control"; the focus ring below says it to a keyboard. */
.select-quiet {
  border-color: transparent;
  background-color: transparent;
  background-image:
    linear-gradient(45deg, transparent 50%, var(--text-strong) 50%),
    linear-gradient(135deg, var(--text-strong) 50%, transparent 50%);
}
.select-quiet:hover { background-color: transparent; color: var(--text-strong); }
/* The speed word is the same size and colour as the quiet Read aloud link beside it. */
.toolbar select.select-quiet { font-size: 14px; color: var(--muted); }
.select-quiet:focus {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius);
  /* The quiet speed control has no border by design; keep it that way when focused. */
  border-color: transparent;
  box-shadow: none;
}

/* ---------- segment pager ---------- */
/* A long text is read one sitting at a time. This is a counted chip bar, not the
   book window's Previous/Next (that one fetches a different window; this one moves
   inside a text that is already here). It only exists when there is more than one
   segment. */
.segment-pager {
  margin: 18px 0 4px;
  padding: 10px 12px;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--bg-subtle);
}
.segment-pager-head {
  display: flex; flex-wrap: wrap; justify-content: space-between;
  gap: 2px 12px; margin-bottom: 8px;
}
/* Previous/Next stay put; the numbered chips scroll between them. Wrapping 20 chips
   would grow the bar over the article, and an ellipsis would hide segments; a
   horizontal strip keeps every segment reachable and the count fixed above it. */
.segment-pager-controls {
  display: flex; align-items: center; gap: 6px; min-width: 0;
}
.segment-pager-controls > .btn { flex: none; }
.segment-chips {
  display: flex; gap: 6px;
  flex: 1 1 0; min-width: 0;
  overflow-x: auto; overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  scroll-snap-type: x proximity;
  scrollbar-width: thin;
  padding-bottom: 2px;
}
.segment-chips .chip { flex: none; scroll-snap-align: center; }
.segment-pager [aria-disabled="true"] { opacity: .45; pointer-events: none; }
.segment-pager .chip { min-width: 30px; justify-content: center; }
/* The editor's live word count: the same span, made obvious once the paste is over
   the limit. */
[data-wordcount].is-over { color: var(--bad); font-weight: 500; }

/* The add-a-category chip is a destination, not a category: dashed and quiet. */
.chip.chip-add {
  border-style: dashed; color: var(--muted);
  font-weight: 600; padding-inline: 10px;
}
.chip.chip-add:hover { color: var(--accent); border-color: var(--accent-border); }

/* A refused form: the message beside its field, and the summary beside the submit
   row. Both use the app's error colour, not a new one. */
.field-error { color: var(--bad); font-size: 12.5px; line-height: 1.4; margin-top: 6px; }
[data-form-summary] .callout { margin-bottom: 14px; }

/* ---------- collection player ---------- */
/* The sentences and words pages use the reader's .toolbar treatment: fixed to the
   bottom, same background and safe area. The play button takes the room; the three
   icon buttons and the position stay fixed, so the whole transport fits 320 px. */
.collection-player [data-player="toggle"] { flex: 1 1 auto; min-width: 6rem; }
.collection-player .btn-icon { flex: none; }
.collection-player [data-player-position] { flex: none; }
.collection-player [data-player="loop"].is-on {
  background: var(--accent-soft); color: var(--accent); border-color: var(--accent-border);
}
.collection-player .player-loop { position: relative; }
.collection-player .player-loop-badge {
  position: absolute; top: 3px; right: 4px;
  font-size: 9px; font-weight: 700; line-height: 1; color: var(--accent);
}
/* The item the player is on, in the list it is playing. */
.row.is-current { background: var(--accent-soft); box-shadow: inset 3px 0 var(--accent); }
/* A kept sentence is rendered with the reader's word spans, so it can be tapped. */
.sentence-text { font-family: var(--font-serif); font-size: 17.5px; line-height: 1.62; }
.sentence-text p, .sentence-text .sent-block { margin: 0; }

/* ---------- chips ---------- */
.chips { display: flex; flex-wrap: wrap; gap: 7px; }
.chip {
  display: inline-flex; align-items: center; gap: 7px;
  border: 1px solid var(--border); background: var(--surface);
  color: var(--muted); border-radius: var(--radius);
  padding: 5px 11px; font-size: 13.5px; text-decoration: none;
  transition: background .12s ease, color .12s ease, border-color .12s ease;
}
.chip:hover { background: var(--bg-hover); color: var(--text-strong); }
/* The one on-state, used by the library's category filters and the practice
   screen's engine picker. The markup still carries aria-current="true" for a
   screen reader; the class is what paints it. */
.chip.is-on {
  border-color: var(--accent-border); background: var(--accent-soft); color: var(--accent);
  font-weight: 500;
}
.chip .count { color: var(--faint); font-variant-numeric: tabular-nums; font-size: 12.5px; }
.chip.is-on .count { color: inherit; opacity: .7; }

.tag {
  display: inline-flex; align-items: center; gap: 5px;
  font-size: 12px; color: var(--muted); letter-spacing: .01em;
}
.tag-dot { width: 5px; height: 5px; border-radius: 50%; background: var(--border-strong); flex: none; }
.meta-row { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; font-size: 12.5px; color: var(--muted); }
.meta-row .sep { color: var(--border-strong); }

/* ---------- cards ---------- */
.card {
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  padding: 20px 22px;
}
.card-link { display: block; text-decoration: none; color: inherit; }
.card-link:hover { background: var(--bg-subtle); }

.feature {
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  padding: 24px 26px 22px;
  position: relative;
  overflow: hidden;
}
.feature::before {
  content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 2px;
  background: var(--accent);
}
.feature-title {
  font-size: 23px; font-weight: 620; letter-spacing: -0.015em;
  color: var(--text-strong); line-height: 1.25; margin-top: 10px;
}
.feature-byline { margin-top: 6px; font-size: 14px; color: var(--muted); }
.feature-excerpt {
  font-family: var(--font-serif);
  margin-top: 16px; font-size: 17px; line-height: 1.62; color: var(--muted);
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}
.feature-actions { margin-top: 20px; }

/* ---------- list rows ---------- */
.rows { border-top: 1px solid var(--border); }
.row {
  display: flex; align-items: center; gap: 14px;
  padding: 12px 10px; margin: 0 -10px;
  border-bottom: 1px solid var(--border);
  text-decoration: none; color: inherit; border-radius: 0;
  transition: background .12s ease;
}
.row:hover { background: var(--bg-subtle); }
button.row {
  width: auto; font: inherit; background: transparent; border: 0;
  border-bottom: 1px solid var(--border); cursor: pointer; text-align: left;
}
.row-main { min-width: 0; flex: 1; }
/* A row for an action that is available but deliberately understated: same list
   shape and hit area, lighter type, no fill and no warning colour. */
.row-quiet { padding-block: 9px; }
.row-quiet .row-title { font-size: 14px; font-weight: 400; color: var(--text); }
.row-quiet .small { font-size: 12.5px; }
.row-quiet .row-side { color: var(--faint); }
.rows .row.is-returned { animation: word-return 1.8s ease-out; }
@keyframes word-return {
  0%, 100% { background: transparent; box-shadow: none; }
  12%, 62% { background: var(--accent-soft); box-shadow: 0 0 0 2px var(--accent-soft); }
}
@media (prefers-reduced-motion: reduce) {
  .rows .row.is-returned { animation: none; background: var(--accent-soft); }
}

.row-title {
  font-size: 15px; font-weight: 500; color: var(--text-strong);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.row-sub {
  font-size: 13px; color: var(--muted); margin-top: 2px;
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/*
 * A reading row — a title over its author · time line — needs its pieces to be
 * blocks. An inline title ignores overflow and text-overflow, so a long one ran into
 * the side label instead of ending in an ellipsis, and the metadata ran on from the
 * title on the same line. Scoped to these rows: the Me hub's link rows are deliberately
 * a title and a description on one line.
 *
 * The full title still reaches a screen reader (and a mouse, through the row's title
 * attribute); nothing is truncated in JavaScript.
 */
.passage-row .row-title,
.passage-row .row-sub { display: block; }
.row-side { font-size: 12.5px; color: var(--faint); flex: none; display: flex; align-items: center; gap: 10px; }

/*
 * A row with actions (the library) is a container: the link takes what is left after
 * the buttons, so a long title still elides and the buttons never crush it. The link
 * is the whole line the reader taps to read; the buttons are siblings, not children.
 */
.row-link {
  flex: 1; min-width: 0; display: block;
  color: inherit; text-decoration: none;
}
.row-actions { flex: none; display: flex; align-items: center; gap: 6px; }

/* ---------- reader ---------- */
.reader-head { margin-bottom: 28px; }
.reader-title {
  font-size: 30px; line-height: 1.22; font-weight: 650;
  letter-spacing: -0.02em; color: var(--text-strong); margin-top: 10px;
}
.source-line {
  margin-top: 8px; font-size: 13px; color: var(--muted); line-height: 1.5;
  overflow-wrap: anywhere;
}
.note-callout {
  margin-top: 20px; border: 1px solid var(--border); border-left: 2px solid var(--accent);
  border-radius: var(--radius); background: var(--bg-subtle);
  padding: 11px 14px; font-size: 14px; line-height: 1.55; color: var(--text);
}
.note-callout b { font-weight: 600; color: var(--text-strong); }

/* The playback block floats at the bottom of the screen: on a phone that is where a
   thumb is. Sticky rather than fixed, so it stays in the flow — while there is more
   article below the fold it hovers over the text, and at the end of the article it
   settles after it, where it can hide neither the last line nor the continue block.
   The near-opaque blur keeps the text behind it from showing through the buttons, and
   the safe-area padding keeps it clear of a phone's home indicator. */
.toolbar {
  /* Genuinely pinned to the viewport edge, not just sticky in the article: at the
     end of the article a sticky bar would settle after the text and leave the bottom
     edge empty. It is centred on the reading column at the same width as the article,
     so it does not look like a second layout. `.has-toolbar` reserves its height. */
  position: fixed;
  left: 0;
  right: 0;
  bottom: 0;
  width: 100%;
  z-index: 30;
  display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
  /* The bar is the full viewport width; only its content is held to the reading
     column. That is what makes it read as a toolbar rather than a floating panel. */
  padding: 8px 0 calc(8px + env(safe-area-inset-bottom, 0px));
  padding-left: max(16px, env(safe-area-inset-left, 0px), calc((100% - var(--measure)) / 2));
  padding-right: max(16px, env(safe-area-inset-right, 0px), calc((100% - var(--measure)) / 2));
  /* Opaque fallback for iOS 16.0-16.1 (no color-mix), then the translucent
     version with the prefixed blur that iOS 16/17 understand. */
  background: var(--bg);
  background: color-mix(in srgb, var(--bg) 96%, transparent);
  -webkit-backdrop-filter: saturate(180%) blur(12px);
  backdrop-filter: saturate(180%) blur(12px);
  border-top: 1px solid var(--border);
}
.toolbar .grow { flex: 1; }
.toolbar-label { font-size: 12.5px; color: var(--muted); }
/* The speed select is never squeezed: it is what shows the current rate, and a
   flex-shrunk select clips its own text rather than wrapping it. */
.toolbar select { flex: none; font-size: 13px; }
/* The transport is the thumb target: it takes the room the speed control and Read
   aloud leave, with a floor so it never shrinks to an icon-sized sliver. */
.toolbar [data-act="transport"] { flex: 1 1 auto; min-width: 6rem; }

/* The practice action row is the reader's toolbar: Record, then the model and the
   reader's own take as two play/pause buttons. Record is capped at half the row, so
   it cannot crowd them out; the two play buttons share the rest. */
.practice-toolbar { align-items: stretch; }
/* Four controls no longer fit one 320 px line with their labels: Record and
   Practice share the first line, the two play/pause buttons the second. The tap
   targets and every label stay full size; only the arrangement changes. */
.practice-main,
.practice-players {
  display: flex; align-items: stretch; gap: 6px;
  flex: 1 0 100%; min-width: 0;
  min-height: 42px;
}
.practice-toolbar .practice-record,
.practice-toolbar .practice-challenge {
  flex: 1 1 0;
  width: 0;
  max-width: none;
  gap: 5px;
}
.practice-challenge-note { margin: 18px 0 0; max-width: 36rem; }
.practice-toolbar[hidden], .mic-wrap[hidden], [data-practice-results][hidden] { display: none; }
[data-practice-view="results"] .practice-toolbar,
[data-practice-view="results"] .practice-challenge-note { display: none; }
.practice-results { margin-top: 26px; }
/* The results audio controls are the page's transport, so they use the reader's
   pinned-bar treatment: full viewport width, content in the reading column, safe
   area at the bottom, and the page reserves the bar's height. */
.practice-audio-dock {
  position: fixed; left: 0; right: 0; bottom: 0; width: 100%; z-index: 30;
  margin: 0;
  padding: 10px 0 calc(10px + env(safe-area-inset-bottom, 0px));
  padding-left: max(16px, env(safe-area-inset-left, 0px), calc((100% - var(--measure)) / 2));
  padding-right: max(16px, env(safe-area-inset-right, 0px), calc((100% - var(--measure)) / 2));
  background: var(--bg);
  background: color-mix(in srgb, var(--bg) 96%, transparent);
  border-top: 1px solid var(--border);
  -webkit-backdrop-filter: blur(10px);
  backdrop-filter: blur(10px);
}
.practice-audio-inner { width: 100%; }
.practice-audio-bar { display: flex; align-items: center; gap: 10px; }
.practice-audio-seek { flex: 1; min-width: 80px; accent-color: var(--accent); }
.practice-source-row {
  display: flex; flex-wrap: nowrap; gap: 8px; margin-top: 10px;
}
.practice-source-row .btn {
  flex: 1 1 0; width: 0; min-width: 0; padding-inline: 10px; white-space: nowrap;
}
.assessment-summary {
  margin: 14px 0 0; color: var(--muted); font-size: 13.5px; line-height: 1.5;
}
.practice-source-row .btn.is-on {
  background: var(--text-strong); color: var(--bg); border-color: var(--text-strong);
}
.practice-toolbar .practice-play {
  flex: 1 1 0;
  min-width: 0;
  gap: 4px;
  padding-inline: 5px;
  font-size: 12px;
}
.practice-record.is-live {
  --btn-bg: var(--accent-hover);
  --btn-bd: var(--accent);
  --btn-fg: #fff;
  background: var(--btn-bg);
  border-color: var(--btn-bd);
  color: var(--btn-fg);
  transition: none;
}
/* Live is a recording state, not a pointer state. Sticky touch :hover/:active
   must not repaint the countdown while the reader scrolls the passage. */
.practice-record.is-live:hover,
.practice-record.is-live:active,
.practice-record.is-live:focus {
  background: var(--accent-hover);
  border-color: var(--accent);
  color: #fff;
}
@media (hover: none), (pointer: coarse) {
  .practice-record,
  .practice-record:hover,
  .practice-record:active,
  .practice-record:focus {
    background: var(--btn-bg);
    border-color: var(--btn-bd);
    color: var(--btn-fg);
  }
  .practice-record.is-live,
  .practice-record.is-live:hover,
  .practice-record.is-live:active,
  .practice-record.is-live:focus {
    background: var(--accent-hover);
    border-color: var(--accent);
    color: #fff;
  }
}

/* A live soundwave inside the button rather than a ring: four small bars with
   staggered delays. The microphone glyph and the wave share one fixed-width slot and
   swap in place, so at rest the button is just the microphone and the label — no
   empty gap — and its width cannot change when the wave appears. The bars are white at
   85%, quiet enough to sit in a red button. */
.record-glyph {
  position: relative;
  display: inline-block;
  width: 18px;
  height: 16px;
  flex: none;
}
.record-glyph > * {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
}
.record-glyph .glyph-mic {
  transition: opacity .12s ease;
}
.wave {
  align-items: flex-end;
  gap: 2px;
  visibility: hidden;
}
.wave i {
  display: block;
  width: 3px;
  border-radius: 1px;
  background: currentColor;
  opacity: .85;
  transform: scaleY(.45);
  transform-origin: 50% 100%;
}
.wave i:nth-child(1) { height: 6px; }
.wave i:nth-child(2) { height: 12px; }
.wave i:nth-child(3) { height: 8px; }
.wave i:nth-child(4) { height: 14px; }
.practice-record.is-live .glyph-mic { visibility: hidden; }
.practice-record.is-live .wave { visibility: visible; }
.practice-record.is-live .wave i { animation: soundwave .9s ease-in-out infinite; }
.practice-record.is-live .wave i:nth-child(2) { animation-delay: .15s; }
.practice-record.is-live .wave i:nth-child(3) { animation-delay: .3s; }
.practice-record.is-live .wave i:nth-child(4) { animation-delay: .45s; }
@keyframes soundwave {
  0%, 100% { transform: scaleY(.45); }
  50%      { transform: scaleY(1); }
}

/* The limit line sits under the buttons in the same bar, not inside the one-line
   control row. */
.practice-limit {
  margin: 14px 0 0;
  font-size: 12.5px;
  line-height: 1.4;
  color: var(--muted);
  text-align: left;
}

/* The bar is out of flow, so the page reserves its height (plus the home indicator)
   at the end: the last line can always be scrolled clear of it, on both screens. */
.has-toolbar {
  padding-bottom: 68px;
  padding-bottom: calc(68px + env(safe-area-inset-bottom, 0px));
}
/* The results transport is a second fixed bar: reserve its own height instead. */
.practice-page.has-toolbar[data-practice-view="results"] {
  padding-bottom: 164px;
  padding-bottom: calc(164px + env(safe-area-inset-bottom, 0px));
}

.passage {
  font-family: var(--font-serif);
  font-size: calc(20px * var(--reading-scale, 1));
  line-height: 1.82; color: var(--text);
  margin-top: 26px;
  letter-spacing: .002em;
}
.passage p { margin-bottom: 1.15em; }
.passage p:last-child { margin-bottom: 0; }
.passage.is-dim { opacity: .45; }

/* verse keeps the shape it was printed in */
.verse { margin-bottom: 1.15em; }
.verse:last-child { margin-bottom: 0; }
.verse-line { padding-left: calc(var(--indent, 0) * 0.42em); }

/* tappable words */
.w {
  border-radius: 2px;
  cursor: pointer;
  transition: background .1s ease, color .1s ease;
  /* One tap reads from this sentence, two taps open the definition. Without this,
     a double tap is claimed by the browser's double-tap-to-zoom and the second
     tap never arrives as a dblclick. Panning and pinch-zoom still work. */
  touch-action: manipulation;
}
.w:hover { background: var(--accent-soft); color: var(--accent); }
/* On a touch screen there is no hover, so the same tint is the tap feedback. */
.passage .w:active { background: var(--accent-soft); color: var(--accent); }
.sent { border-radius: 3px; }
/* A sentence kept for later is tinted, very lightly and in a neutral grey: the
   reading text should stay quiet, and it must not be mistaken for the mark that
   shows which sentence is being spoken. Defined before that one, so speaking wins
   when a sentence is both kept and being read. */
.sent.is-saved { background: var(--bg-hover); }
.sent.is-speaking { background: var(--accent-soft); }
.sent-block { position: relative; }
.sent-btn {
  position: absolute; left: -34px; top: .38em;
  width: 24px; height: 24px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  color: var(--faint); opacity: 0;
  transition: opacity .12s ease, background .12s ease;
}
.sent-block:hover .sent-btn { opacity: 1; }
.sent-btn:hover { background: var(--bg-hover); color: var(--accent); }
@media (max-width: 780px) { .sent-btn { display: none; } }

/* ---------- practice ---------- */
.practice-stage {
  border: 1px solid var(--border); border-radius: var(--radius-lg);
  background: var(--surface); padding: 26px 28px; margin-top: 22px;
}
.practice-text {
  font-family: var(--font-serif);
  font-size: calc(21px * var(--reading-scale, 1));
  line-height: 1.9;
  color: var(--text-strong);
}
.practice-text p { margin-bottom: 1em; }
.practice-text p:last-child { margin-bottom: 0; }

.v { padding: 1px 1px; border-radius: 2px; transition: background .15s ease, color .15s ease; }
.v-correct { color: var(--text-strong); border-bottom: 2px solid var(--good-border); }
.v-close   { color: var(--warn); border-bottom: 2px dashed var(--warn-border); background: var(--warn-bg); }
.v-miss    { color: var(--bad); border-bottom: 2px solid var(--bad-border); background: var(--bad-bg); }
.v-extra   { color: var(--faint); text-decoration: line-through; }
.v-pending { color: var(--faint); }
.w.is-now { background: var(--accent-soft); box-shadow: 0 0 0 2px var(--accent-soft); }
.w.is-found {
  background: var(--word-mark-bg);
  box-shadow: 0 0 0 2px var(--word-mark-bg);
  text-decoration: underline;
  text-decoration-color: var(--word-mark-line);
  text-decoration-thickness: 2px;
  text-underline-offset: 2px;
}

.mic-wrap { display: flex; flex-direction: column; align-items: center; gap: 12px; margin: 26px 0 6px; }
.practice-page[data-transcribing="true"] .practice-record { opacity: .55; }
.mic-hint { font-size: 13.5px; color: var(--muted); text-align: center; max-width: 30rem; line-height: 1.5; }
.level {
  width: 150px; height: 3px; border-radius: 2px; background: var(--border); overflow: hidden;
}
.level > i { display: block; height: 100%; width: 0; background: var(--accent); transition: width .08s linear; }

.transcript-live {
  font-family: var(--font-serif); font-size: 17px; line-height: 1.6;
  color: var(--muted); min-height: 22px; text-align: center; max-width: 34rem;
}
.transcript-live .interim { color: var(--faint); font-style: italic; }

.legend { display: flex; flex-wrap: wrap; gap: 14px; font-size: 12.5px; color: var(--muted); margin-top: 18px; }
.legend span { display: inline-flex; align-items: center; gap: 6px; }
.legend i { width: 16px; height: 3px; border-radius: 2px; display: block; }
.legend .k-correct { background: var(--good-border); }
.legend .k-close { background: var(--warn-border); }
.legend .k-miss { background: var(--bad-border); }

/* soft summary — deliberately not a game score */
.summary { display: flex; flex-wrap: wrap; gap: 26px; margin-top: 4px; }
.metric { min-width: 92px; }
.metric-num {
  font-size: 27px; font-weight: 620; letter-spacing: -0.02em;
  color: var(--text-strong); font-variant-numeric: tabular-nums; line-height: 1.1;
}
.metric-label { font-size: 12.5px; color: var(--muted); margin-top: 3px; }

.focus-list { list-style: none; padding: 0; margin: 0; }
.focus-item {
  padding: 14px 0; border-bottom: 1px solid var(--border);
  display: flex; gap: 14px; align-items: flex-start;
}
.focus-item:last-child { border-bottom: 0; }
.focus-word {
  font-family: var(--font-serif); font-size: 18px; color: var(--text-strong);
  min-width: 8.5rem; flex: none;
}
.focus-body { min-width: 0; flex: 1; }
.focus-said { font-size: 13px; color: var(--bad); margin-bottom: 4px; }
.focus-tip { font-size: 14px; line-height: 1.6; color: var(--text); }
.focus-tip code {
  font-family: var(--font-mono); font-size: 13px;
  background: var(--bg-subtle); border: 1px solid var(--border);
  padding: 1px 5px; border-radius: var(--radius);
}

.playback { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; margin-top: 18px; }

/* ---------- callouts, empty, misc ---------- */
.callout {
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--bg-subtle); padding: 13px 15px;
  font-size: 14px; line-height: 1.6; color: var(--text);
}
.callout.warn { background: var(--warn-bg); border-color: var(--warn-border); }
.callout.good { background: var(--good-bg); border-color: var(--good-border); }
.callout.info { background: var(--accent-soft); border-color: var(--accent-border); }
.callout b { font-weight: 600; }
.callout .btn { margin-top: 10px; }

.empty {
  border: 1px dashed var(--border-strong); border-radius: var(--radius-lg);
  padding: 34px 24px; text-align: center; color: var(--muted); font-size: 14.5px;
  line-height: 1.6;
}
.empty b { color: var(--text-strong); font-weight: 600; display: block; margin-bottom: 5px; font-size: 15px; }

/* ---------- forms ---------- */
.field { margin-bottom: 20px; }
.field label {
  display: block; font-size: 13px; font-weight: 600; color: var(--text-strong);
  margin-bottom: 6px;
}
.field .hint { font-size: 12.5px; color: var(--muted); margin-top: 5px; line-height: 1.5; }
/* The country code and the number are one control: same height, one baseline. */
.phone-row { display: flex; gap: 8px; align-items: center; }
.phone-row > select, .phone-row > input { height: 42px; box-sizing: border-box; }
.phone-row > input { flex: 1; min-width: 0; }
/* One family for every text entry, not only the first two types. Email, password,
   telephone, URL, number and date fields all look like text fields. */
input[type="text"], input[type="search"], input[type="email"], input[type="password"],
input[type="tel"], input[type="url"], input[type="number"], input[type="date"],
select, textarea {
  width: 100%; background: var(--surface); color: var(--text);
  border: 1px solid var(--border-strong); border-radius: var(--radius);
  padding: 8px 10px; font-size: 14.5px;
  transition: border-color .12s ease, box-shadow .12s ease;
}
/* A field that keeps focus after a tap should look a little warmer, not selected. The
   border is mostly the ordinary border; the ring is 2 px of the accent at low alpha.
   The global red outline stays for fine-pointer keyboard use, but a touch screen gets
   these two quiet signals instead. */
input[type="text"]:focus, input[type="search"]:focus, input[type="email"]:focus,
input[type="password"]:focus, input[type="tel"]:focus, input[type="url"]:focus,
input[type="number"]:focus, input[type="date"]:focus, select:focus, textarea:focus {
  /* Plain fallbacks first for browsers without color-mix(). */
  border-color: var(--border-strong);
  box-shadow: 0 0 0 2px rgba(200, 16, 46, .14);
  border-color: var(--field-focus-border);
  box-shadow: 0 0 0 2px var(--field-focus-ring);
}
@media (pointer: coarse) {
  input[type="text"]:focus, input[type="search"]:focus, input[type="email"]:focus,
  input[type="password"]:focus, input[type="tel"]:focus, input[type="url"]:focus,
  input[type="number"]:focus, input[type="date"]:focus, select:focus, textarea:focus {
    outline: none;
  }
}

/* The Find field's combobox. The wrapper is the positioning context, so the list
   opens over the page instead of pushing the results down. It is deliberately inside
   the input's column rather than the whole row, which is what keeps it inside a
   320 px screen even beside the Search button. */
.find-bar { display: flex; flex-wrap: nowrap; align-items: stretch; gap: 8px; margin-bottom: 8px; }
.find-field { position: relative; flex: 1 1 180px; min-width: 0; }
.find-field input[type="search"] { width: 100%; min-width: 0; display: block; height: 42px; box-sizing: border-box; }
.find-bar > .btn { flex: none; }
.suggest-list {
  position: absolute; z-index: 60; top: calc(100% + 4px); left: 0; right: 0;
  box-sizing: border-box; max-width: 100%; max-height: 46vh; overflow-y: auto;
  margin: 0; padding: 4px; list-style: none;
  background: var(--surface); border: 1px solid var(--border-strong);
  border-radius: var(--radius); box-shadow: var(--shadow-pop);
}
.suggest-list[hidden] { display: none; }
.suggest-option {
  display: flex; align-items: baseline; gap: 10px;
  box-sizing: border-box; min-width: 0; padding: 7px 9px; border-radius: var(--radius);
  font-size: 14px; line-height: 1.35; cursor: pointer;
}
.suggest-option:hover, .suggest-option.is-active { background: var(--bg-hover); }
.suggest-word {
  min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  color: var(--text-strong);
}
.suggest-kind {
  flex: none; margin-left: auto; font-size: 11.5px; color: var(--muted);
  white-space: nowrap;
}
textarea { min-height: 220px; line-height: 1.7; resize: vertical; font-family: var(--font-serif); font-size: 16px; }
select { max-width: 100%; }
input[type="range"] { accent-color: var(--accent); width: 100%; }
.check { display: flex; align-items: flex-start; gap: 9px; font-size: 14px; line-height: 1.5; }
.check input { margin-top: 3px; accent-color: var(--accent); flex: none; }

.switch-row {
  display: flex; align-items: center; justify-content: space-between; gap: 20px;
  padding: 15px 0; border-bottom: 1px solid var(--border);
}
.switch-row:last-child { border-bottom: 0; }
.switch-row .sr-main { min-width: 0; }
.switch-row .sr-title { font-size: 14.5px; font-weight: 500; color: var(--text-strong); }
.switch-row .sr-desc { font-size: 13px; color: var(--muted); margin-top: 3px; line-height: 1.5; }
.switch-row .sr-ctl { flex: none; }
/* The one switch component: a real checkbox for state and accessibility, with a
   track and knob for the hand. The label wraps both, so the whole control toggles. */
.toggle {
  position: relative; display: inline-flex; width: 44px; height: 24px; flex: none;
  cursor: pointer; vertical-align: middle;
}
.toggle input {
  position: absolute; inset: 0; width: 100%; height: 100%; margin: 0;
  opacity: 0; z-index: 2; cursor: pointer;
}
.toggle-track {
  position: absolute; inset: 0; border-radius: 999px;
  background: var(--bg-active); border: 1px solid var(--border-strong);
  transition: background .15s ease, border-color .15s ease;
}
.toggle-thumb {
  position: absolute; width: 18px; height: 18px; left: 2px; top: 2px;
  border-radius: 50%; background: var(--surface);
  box-shadow: 0 1px 2px rgba(15, 15, 15, .18);
  transition: transform .15s ease, background .15s ease;
}
.toggle input:checked + .toggle-track { background: var(--accent); border-color: var(--accent); }
.toggle input:checked + .toggle-track .toggle-thumb { transform: translateX(20px); background: #fff; }
.toggle input:focus-visible + .toggle-track { outline: var(--focus-ring); outline-offset: 2px; }
/* The same quiet switch on the collections, but tighter and without a section rule. */
.source-switch { padding: 10px 0; border-bottom: 0; margin: 10px 0; }
.source-switch .sr-title { font-size: 14px; }
.source-switch .sr-desc .btn { margin-left: 6px; }

/* ---------- dialog ---------- */
/* The native <dialog>, in the app's own clothes. It is centred by the UA, so the
   only sizing here is the phone-safe width and the scrolling ceiling for long
   text. Fits a 390 px viewport with a 16 px gutter either side. */
.cdialog {
  width: min(26rem, calc(100vw - 32px));
  max-width: calc(100vw - 32px);
  /* Landscape on a notched iPhone: keep the card out of the sensor housing. */
  max-width: calc(100vw - 32px - env(safe-area-inset-left, 0px) - env(safe-area-inset-right, 0px));
  max-height: calc(100vh - 32px);
  max-height: calc(100dvh - 32px);
  /* The modal is centred, so it needs the notch and home-indicator insets too,
     not just the visible viewport height. */
  max-height: calc(100dvh - 32px - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px));
  margin: auto;
  padding: 0;
  overflow: hidden;
  background: var(--surface-raised);
  color: var(--text);
  border: 0;
  border-radius: var(--radius-lg);
  box-shadow: var(--shadow-pop);
}
.cdialog::backdrop { background: rgba(15, 15, 15, .48); }
/* The card itself is not the focus target; its buttons are. No outer ring. */
.cdialog, .cdialog:focus, .cdialog:focus-visible { outline: 0 !important; }
.cdialog-card {
  max-height: inherit;
  overflow-y: auto;
  padding: 22px 22px 18px;
}
.cdialog-kicker {
  display: inline-flex; align-items: center; gap: 6px;
  margin-bottom: 7px; font-size: 12px; font-weight: 600;
  letter-spacing: .04em; text-transform: uppercase; color: var(--bad);
}
/* A word and a mark, not only a colour: the danger variant is also role=alertdialog. */
.cdialog-kicker::before {
  content: ""; width: 7px; height: 7px; border-radius: 50%;
  background: var(--bad); flex: none;
}
.cdialog-title {
  font-size: 18px; font-weight: 600; line-height: 1.35;
  color: var(--text-strong); overflow-wrap: anywhere;
}
.cdialog-body {
  margin-top: 8px; font-size: 14.5px; line-height: 1.6;
  color: var(--text); overflow-wrap: anywhere;
}
.cdialog-actions {
  display: flex; flex-wrap: wrap; justify-content: flex-end;
  gap: 8px; margin-top: 20px;
}
/* Long labels wrap inside the dialog instead of overflowing a 390 px screen. */
.cdialog .btn { white-space: normal; }
/* On the dark red, white text is under 4.5:1; a dark label keeps the destructive
   button legible while the fill stays the signal, not an outline. */
[data-theme="dark"] .cdialog-confirm.btn-danger { --btn-fg: #151515; }
.cdialog-cancel[hidden], .cdialog-body[hidden], .cdialog-kicker[hidden] { display: none; }
.cdialog[open] { animation: cdialog-in .12s ease-out; }
@keyframes cdialog-in {
  from { opacity: 0; transform: translateY(4px) scale(.99); }
  to { opacity: 1; transform: none; }
}

/* ---------- popover ---------- */
/* A fixed, zero-sized box at the viewport origin. Its only job is to be the
   containing block for the definition card, so the card can be placed in viewport
   coordinates. While it sat in normal flow at the end of the document, every card
   was positioned relative to the *bottom of the page* while the code computed a
   document offset — the two stacked, and the card opened far below the fold. */
#popover-layer { position: fixed; top: 0; left: 0; width: 0; height: 0; z-index: 60; }

/* One quiet status line while a degraded server voice is retried; it sits above the
   narration bar, never over the controls, and is removed when the service answers. */
.narration-fallback {
  position: fixed; left: 50%; transform: translateX(-50%); z-index: 50;
  width: max-content; max-width: calc(100vw - 32px); margin: 0; padding: 7px 11px;
  border: 1px solid var(--warn-border); border-radius: var(--radius);
  background: var(--warn-bg); color: var(--text);
  font-size: 13px; line-height: 1.4; text-align: center;
  box-shadow: 0 2px 8px rgba(0, 0, 0, .12);
  pointer-events: none;
}
.popover {
  position: absolute; width: min(22rem, calc(100vw - 32px));
  /* Never taller than the screen, so a long definition stays reachable. 100dvh
     follows the visible viewport on iOS; 100vh is the fallback for old Safari,
     where the large viewport can put the bottom of the card off-screen. */
  max-height: calc(100vh - 24px); overflow-y: auto;
  max-height: calc(100dvh - 24px); overflow-y: auto;
  max-height: calc(100dvh - 24px - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px)); overflow-y: auto;
  background: var(--surface-raised);
  border: 1px solid var(--border-strong); border-radius: var(--radius-lg);
  box-shadow: var(--shadow-pop);
  padding: 16px 17px;
  font-size: 14px; line-height: 1.6;
  animation: pop .12s ease-out;
}
@keyframes pop { from { opacity: 0; transform: translateY(-3px); } to { opacity: 1; transform: none; } }
.pop-head { display: flex; align-items: baseline; gap: 9px; flex-wrap: wrap; }
.pop-word {
  font-family: var(--font-serif); font-size: 21px; color: var(--text-strong);
  font-weight: 600; letter-spacing: -0.01em;
}
.pop-ipa { font-family: var(--font-mono); font-size: 12.5px; color: var(--muted); }
.pop-pos { font-size: 12px; color: var(--faint); font-style: italic; }
.pop-defs { list-style: none; padding: 0; margin: 11px 0 0; }
.pop-defs li { margin-bottom: 8px; padding-left: 15px; position: relative; }
.pop-defs li::before {
  content: ""; position: absolute; left: 3px; top: .62em;
  width: 4px; height: 1px; background: var(--border-strong);
}
.pop-defs li:last-child { margin-bottom: 0; }
.pop-ex { color: var(--muted); font-style: italic; font-family: var(--font-serif); }
.pop-syn { margin-top: 11px; font-size: 13px; color: var(--muted); }
.pop-syn b { font-weight: 600; color: var(--text); }
.pop-actions {
  display: flex; gap: 7px; margin-top: 14px; padding-top: 13px;
  border-top: 1px solid var(--border); flex-wrap: nowrap;
}
.pop-note { margin-top: 10px; font-size: 12.5px; color: var(--faint); }

/* ---------- toast ---------- */
.toast {
  position: fixed; left: 50%; bottom: 26px;
  bottom: calc(26px + env(safe-area-inset-bottom, 0px));
  transform: translateX(-50%);
  background: var(--text-strong); color: var(--bg);
  padding: 9px 16px; border-radius: var(--radius);
  font-size: 13.5px; z-index: 90; max-width: calc(100vw - 32px);
  box-shadow: var(--shadow-pop);
  /* A message must never stand between the reader and the action row: the silent-
     take message sits over the pinned Record button, which has to stay pressable. */
  pointer-events: none;
}
.toast[hidden] { display: none; }

/* ---------- footer ---------- */
.footer {
  border-top: 1px solid var(--border); background: var(--bg-subtle);
  flex: none;
}
.footer-inner {
  max-width: var(--page); margin: 0 auto;
  padding: 18px 24px;
  padding: 18px max(24px, env(safe-area-inset-left, 0px)) 18px max(24px, env(safe-area-inset-right, 0px));
  padding: 18px max(24px, env(safe-area-inset-left, 0px)) calc(18px + env(safe-area-inset-bottom, 0px)) max(24px, env(safe-area-inset-right, 0px));
  font-size: 12.5px; color: var(--muted); line-height: 1.6;
}

/* ---------- utilities ---------- */
.stack-sm > * + * { margin-top: 8px; }
.stack > * + * { margin-top: 16px; }
.stack-lg > * + * { margin-top: 28px; }
.muted { color: var(--muted); }
.faint { color: var(--faint); }
.small { font-size: 13px; }
.serif { font-family: var(--font-serif); }
.mono { font-family: var(--font-mono); }
.tnum { font-variant-numeric: tabular-nums; }
.center { text-align: center; }
.hidden { display: none !important; }

/* A button that reads as a link. Used where the action sits under a form or inside a
   sentence, and a full button would give it more weight than it deserves. */
.linkish {
  background: none; border: 0; padding: 0; margin: 0;
  font: inherit; color: var(--accent);
  cursor: pointer; text-align: left;
  /* A plain border first: iOS 16.0-16.1 has no color-mix() and would otherwise
     drop the underline entirely. */
  border-bottom: 1px solid var(--border-strong);
  border-bottom: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
}
.linkish:hover { border-bottom-color: var(--accent); }
.linkish:disabled { color: var(--faint); border-bottom-color: transparent; cursor: default; }

.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;
}
.grid-2 { display: grid; gap: 16px; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); }

/* ---------- statistics ---------- */
/* Three cards of address-days. Two fit beside each other at 390 px; the seven-day
   card takes the full row underneath rather than being squeezed into a third of it. */
.stat-grid { display: grid; gap: 10px; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); margin-top: 18px; }
.stat {
  border: 1px solid var(--border); border-radius: var(--radius-lg);
  background: var(--surface); padding: 14px 15px; min-width: 0;
}
.stat-label { font-size: 12px; font-weight: 600; color: var(--muted); letter-spacing: .02em; }
.stat-value {
  font-size: 30px; line-height: 1.15; font-weight: 620; color: var(--text-strong);
  font-variant-numeric: tabular-nums; margin-top: 5px;
}
.stat-split { display: flex; flex-wrap: wrap; gap: 2px 12px; margin-top: 3px; font-size: 12.5px; color: var(--muted); }
.stat-split b { color: var(--text-strong); font-weight: 600; font-variant-numeric: tabular-nums; }
.stat .small { margin-top: 4px; }
.visit-bar {
  display: inline-block; width: 46px; height: 4px; border-radius: 2px;
  background: var(--border); overflow: hidden; vertical-align: middle;
}
.visit-bar > span { display: block; height: 100%; background: var(--accent); }
/* A real table in intent, a grid in layout. The fourth column is the viewport CSS
   saw; it is compact and never becomes a horizontally scrolling table. */
.visit-table { border-top: 1px solid var(--border); }
.visit-row {
  display: grid; grid-template-columns: 70px minmax(0, 1fr) 58px 64px;
  gap: 6px; align-items: baseline;
  padding: 9px 2px; border-bottom: 1px solid var(--border);
  font-size: 13px;
}
.visit-head {
  padding-top: 6px; padding-bottom: 6px;
  font-size: 11px; color: var(--faint); text-transform: uppercase; letter-spacing: .03em;
}
.visit-head > span:last-child { text-align: right; }
.visit-day { color: var(--muted); white-space: nowrap; font-size: 12.5px; }
.visit-addr { display: flex; flex-direction: column; min-width: 0; gap: 1px; }
.visit-addr .mono { overflow-wrap: anywhere; color: var(--text); }
.visit-last { text-align: right; color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.visit-view { text-align: right; color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.visit-warning {
  display: block; margin-top: 2px; color: var(--warn);
  font-size: 10.5px; letter-spacing: .02em;
}
.visit-anonymous { font-style: italic; }
@media (max-width: 640px) {
  .stat-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .stat:last-child { grid-column: 1 / -1; }
  .visit-row { grid-template-columns: 56px minmax(0, 1fr) 50px 58px; }
}

/* A third-party link always lands here first. The full URL is text, not a disguised
   label, and wraps rather than widening the page. */
.outbound-page { padding-bottom: 24px; }
.outbound-host {
  margin: 22px 0 0; font-size: 30px; line-height: 1.2; font-weight: 650;
  color: var(--text-strong); overflow-wrap: anywhere;
}
.outbound-url {
  display: block; margin-top: 8px; padding: 11px 12px;
  border: 1px solid var(--border); border-radius: var(--radius);
  background: var(--surface); color: var(--text);
  font-family: var(--font-mono); font-size: 13px; line-height: 1.5;
  overflow-wrap: anywhere; word-break: break-word;
}
.outbound-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 22px; }
@media (max-width: 640px) {
  .outbound-host { font-size: 24px; }
}

/* A browser-forced desktop viewport cannot be fixed by CSS. This note is deliberately
   in normal flow, at the top, so it is readable even in that wrong layout. */
.desktop-layout-notice {
  display: flex; align-items: flex-start; gap: 12px;
  max-width: var(--measure); margin: 14px auto 0;
  padding: 12px 14px; border: 1px solid var(--warn-border);
  background: var(--warn-bg); color: var(--text);
  border-radius: var(--radius); font-size: 16px; line-height: 1.5;
}
.desktop-layout-notice p { flex: 1; margin: 0; }
.desktop-layout-notice .btn { flex: none; }
@media (max-width: 640px) {
  .desktop-layout-notice { margin: 12px 16px 0; }
}

/* ---------- responsive ---------- */
@media (max-width: 640px) {
  .topbar-inner {
    padding: 0 16px;
    padding: 0 max(16px, env(safe-area-inset-left, 0px)) 0 max(16px, env(safe-area-inset-right, 0px));
    gap: 14px; height: 44px;
  }
  .nav { gap: 0; }
  .nav a { padding: 5px 8px; font-size: 13.5px; }
  .main {
    padding: 28px 16px 72px;
    padding: 28px max(16px, env(safe-area-inset-left, 0px)) 72px max(16px, env(safe-area-inset-right, 0px));
  }
  /* One mobile boundary for the whole phone range, not a 420 px near-miss. Large
     phones are 428–430 CSS px wide and were seeing the wordmark and smaller reading
     text while still being phones. */
  .brand-name { display: none; }
  h1.title { font-size: 26px; }
  .reader-title { font-size: 24px; }
  .passage { font-size: calc(20px * var(--reading-scale, 1)); line-height: 1.78; }
  .practice-text { font-size: calc(21px * var(--reading-scale, 1)); }
  .sentence-text { font-size: 18.5px; }
  .dialogue-question { font-size: 20px; }
  .dialogue-invite { padding: 14px; }
  .practice-stage { padding: 18px 16px; }
  .feature { padding: 18px 18px 16px; }
  .feature-title { font-size: 20px; }
  /* Keep the block on one row across the whole phone range, not just at one width:
     the buttons and the quiet practice link sit tighter and the gaps are smaller, so
     "Resume" — the longest label the transport ever shows — still fits beside the
     numeric speed select and the microphone on Read aloud. The select keeps its
     natural width rather than being clipped. Three pixels is what makes 320 and 360
     fit with the icon restored. */
  .toolbar { gap: 3px; }
  .toolbar .btn { padding-inline: 11px; }
  .toolbar .btn-quiet { padding-inline: 6px; }
  .focus-item { flex-direction: column; gap: 6px; }
}
/* At the narrowest phones the row cannot hold the visible word as well, so it goes to
   screen readers only. The label keeps its `for`, so the select keeps its accessible
   name at every width. */
@media (max-width: 359px) {
  .toolbar-label {
    position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
    overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
  }
}
/* ---------- touch devices (iPhone, iPad, Android) ---------- */

/*
 * Safari on iOS zooms the whole page when a form control smaller than 16px takes
 * focus. That is intended accessibility behaviour and it is not something to hide
 * with user-scalable=no (iOS ignores that anyway, and taking zoom away hurts the
 * people it protects). The controls are simply made legible instead. This includes
 * the compact speed/voice selects, whose designed 13px would otherwise zoom.
 */
@media (pointer: coarse) {
  input[type="text"], input[type="search"], input[type="email"], input[type="password"],
  input[type="tel"], input[type="url"], input[type="number"], input[type="date"],
  select, textarea, .select-compact {
    font-size: 16px;
  }
  .toolbar select,
  .toolbar select.select-quiet { font-size: 16px; }

}

/*
 * :hover sticks after a tap on iOS. Each rule that only exists to decorate a mouse
 * hover is undone here, on devices that cannot hover, so a tapped row or word does
 * not stay tinted. Hover-capable devices (including an iPad with a trackpad) are
 * untouched.
 */
@media (hover: none), (pointer: coarse) {
  .nav a:hover { background: transparent; color: var(--muted); }
  .nav a[aria-current="page"]:hover { background: var(--bg-hover); color: var(--text-strong); }
  .section-head a:hover { color: var(--muted); text-decoration: none; }
  .btn:hover { background: var(--btn-bg); }
  .btn-primary:hover { --btn-bg: var(--text-strong); }
  [data-theme="dark"] .btn-primary:hover { --btn-bg: var(--text-strong); }
  .btn-quiet:hover { background: transparent; color: var(--muted); }
  .chip:hover { background: var(--surface); color: var(--muted); border-color: var(--border); }
  .chip.is-on:hover {
    background: var(--accent-soft); color: var(--accent); border-color: var(--accent-border);
  }
  .select-compact:not(.select-quiet):hover { background-color: var(--surface); }
  .card-link:hover, .row:hover { background: transparent; }
  .passage .w:hover { background: transparent; color: inherit; }
  .sent-btn:hover { background: transparent; color: var(--faint); }
  .linkish:hover {
    border-bottom-color: var(--accent);
    border-bottom-color: color-mix(in srgb, var(--accent) 35%, transparent);
  }
}

/* ---------- voice dialogue ---------- */

/* The entry on the practice screen. It is only in the DOM when the server has a
   model configured, so an unconfigured deployment keeps the screen exactly as it
   was. */
.dialogue-invite { padding: 16px 18px; }
.dialogue-privacy-line { margin-top: 8px; color: var(--muted); }
.dialogue-entry { margin-top: 22px; }
.dialogue-page { padding-bottom: 24px; }
.dialogue-current { margin-top: 18px; }

/* The dialogue screen itself: one question, one answer box, the log below. It uses
   the article column and the ordinary section rhythm rather than a fixed toolbar,
   so at 320 px everything wraps instead of overflowing. */
.dialogue-status:empty { display: none; }
.dialogue-status { margin: 4px 0 12px; }
.dialogue-intro { margin-top: 18px; }
.dialogue-privacy {
  margin-top: 0;
  font-size: 14px;
  line-height: 1.55;
}
.dialogue-turns { margin-top: 22px; }
.dialogue-question {
  margin-top: 8px;
  font-family: var(--font-serif);
  font-size: 22px;
  line-height: 1.5;
  color: var(--text-strong);
}
.dialogue-hint { margin-top: 8px; }
.dialogue-controls { margin-top: 16px; }
.dialogue-voice-note { margin-top: 10px; }
.dialogue-answer-label {
  display: block;
  margin-top: 18px;
  margin-bottom: 6px;
  font-size: 13px;
  color: var(--muted);
}
.dialogue-answer {
  display: block;
  width: 100%;
  min-height: 84px;
  padding: 10px 12px;
  border: 1px solid var(--border-strong);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--text);
  /* 16px keeps iOS from zooming the page when the box takes focus. */
  font-size: 16px;
  line-height: 1.5;
  resize: vertical;
}
.dialogue-answer:focus { border-color: var(--accent); }
.dialogue-ended { margin-top: 22px; }
.dialogue-ended-copy { font-size: 16px; color: var(--text); }
.dialogue-log {
  list-style: none;
  padding: 0;
  margin: 0;
}
.dialogue-turn + .dialogue-turn {
  margin-top: 14px;
  padding-top: 14px;
  border-top: 1px solid var(--border);
}
.dialogue-turn-q {
  font-family: var(--font-serif);
  font-size: 17px;
  line-height: 1.55;
  color: var(--text-strong);
}
.dialogue-turn-a {
  margin-top: 4px;
  font-size: 14px;
  line-height: 1.55;
  color: var(--muted);
}
.dialogue-turn-n {
  display: inline-block;
  min-width: 2.2em;
  font-family: var(--font-sans);
  font-size: 11px;
  letter-spacing: .04em;
  text-transform: uppercase;
  color: var(--faint);
}
.dialogue-controls .is-live { --btn-bg: var(--accent-hover); }

@media print {
  .topbar, .footer, .toolbar, .mic-wrap, .skip-link { display: none; }
  .main { padding: 0; max-width: none; }
  .passage { font-size: 12pt; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .001ms !important; transition-duration: .001ms !important; }
  .cdialog[open] { animation: none; }
  /* The live recording state is held still rather than animated away: it must not
     depend on motion the reader has turned off. */
  .practice-record.is-live .wave i {
    animation: none !important;
    transform: none;
  }
}

/* The word whose definition is open. This is the reader's old marked-word tint,
   repeated after the touch-hover reset above so a tap cannot clear it: the reset is
   `.passage .w:hover`, and this rule has the same specificity but comes later. */
.passage .w.is-now,
.w.is-now {
  background: var(--word-mark-bg);
  box-shadow: 0 0 0 2px var(--word-mark-bg);
  text-decoration: underline;
  text-decoration-color: var(--word-mark-line);
  text-decoration-thickness: 2px;
  text-underline-offset: 2px;
}

/* A fixed transport is out of flow; the closing footer line needs its own reserve so
   it can be scrolled clear of the bar, not just the article. */
body.has-fixed-toolbar .footer-inner {
  padding-bottom: calc(86px + env(safe-area-inset-bottom, 0px));
}

/* The library's two ways forward are pinned like the reader's transport. The page
   reserves their height, and the footer borrows the same fixed-bar class. */
.library-page { padding-bottom: 84px; padding-bottom: calc(84px + env(safe-area-inset-bottom, 0px)); }
.library-actions {
  position: fixed; left: 0; right: 0; bottom: 0;
  width: 100%;
  z-index: 30; margin: 0; display: flex; gap: 8px;
  padding: 8px 0 calc(8px + env(safe-area-inset-bottom, 0px));
  padding-left: max(16px, env(safe-area-inset-left, 0px), calc((100% - var(--measure)) / 2));
  padding-right: max(16px, env(safe-area-inset-right, 0px), calc((100% - var(--measure)) / 2));
  background: var(--bg);
  background: color-mix(in srgb, var(--bg) 96%, transparent);
  -webkit-backdrop-filter: saturate(180%) blur(12px);
  backdrop-filter: saturate(180%) blur(12px);
  border-top: 1px solid var(--border);
}
.library-sentinel { height: 1px; }

/* The four player controls are one height at every width. */
.collection-player [data-player] { min-height: 42px; }

/* Three popover actions stay on one line at 390; on a narrower phone they wrap
   rather than overflow. The close action has its own row. */
.pop-actions .btn { flex: 1 1 auto; min-width: 0; white-space: nowrap; gap: 4px; }
.pop-actions .btn.is-kept { opacity: .62; }
.pop-actions .btn.is-kept:hover { opacity: .8; }
.pop-source { margin-top: 2px; }
.pop-actions { gap: 4px; }
.pop-close-row { border-top: 0; padding-top: 0; margin-top: 6px; }
.pop-close-row .btn { flex: 0 0 auto; }

/* Boot skeleton: the same blocks as the first real screen, without any content. */
.boot-skeleton .skeleton-line {
  display: block;
  height: 13px;
  margin: 9px 0;
  border-radius: 999px;
  background: linear-gradient(90deg, var(--surface-2, rgba(127,127,127,.12)), var(--bg-hover, rgba(127,127,127,.2)), var(--surface-2, rgba(127,127,127,.12)));
  background-size: 200% 100%;
  animation: skeleton-sheen 1.4s ease-in-out infinite;
}
.boot-skeleton .skeleton-wide { width: min(78%, 520px); }
.boot-skeleton .skeleton-mid { width: min(55%, 360px); }
.boot-skeleton .skeleton-short { width: min(32%, 210px); }
@keyframes skeleton-sheen {
  0% { background-position: 100% 0; }
  100% { background-position: -100% 0; }
}
@media (prefers-reduced-motion: reduce) {
  .boot-skeleton .skeleton-line { animation: none; }
}
