/* A listening bench: everything quiet except the one green mark that means "this ships". */

:root {
  /* Native controls — the audio players above all — follow the viewer's theme. */
  color-scheme: light dark;
  --bg: #fff;
  --line: #e3e6ea;
  --line-strong: #c8ccd2;
  --soft: #f7f8fa;
  --ink: #14171a;
  --muted: #6b7280;
  --accent: #1f6feb;
  --ship: #178a4c;
  --ship-bg: #eef7f1;
  --error: #b42318;
  --warn: #8a5a00;
  --audio-w: 230px;
  /* One reading measure for the page: prose, the big textareas, the review grid and the config
     cards all end on the same column. A box wider than this reads as a different layout than
     the field under it. */
  --measure: 46rem;
  --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}

* { box-sizing: border-box; }

body {
  margin: 0 auto;
  padding: 1.75rem 1.5rem 6rem;
  max-width: 1120px;
  font: 13.5px/1.5 system-ui, -apple-system, "Segoe UI", sans-serif;
  color: var(--ink);
  background: var(--bg);
}

h1 {
  font-size: 1rem;
  font-weight: 650;
  letter-spacing: -0.01em;
  margin: 0 0 1.25rem;
}

/* The step number is a separate mark from the title: the four sections are a sequence you work
   through in order, so the digit is structure, and the title reads in full ink rather than
   whispering in grey like the captions do. */
h2 {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .1rem .6rem;
  font-size: .8rem;
  font-weight: 650;
  text-transform: uppercase;
  letter-spacing: .07em;
  color: var(--ink);
  margin: 0 0 1.1rem;
}
/* Grey, not the ship green: green on this page means one thing, and it is not "step 2". */
h2 .step {
  font: 650 .72rem/1 var(--mono);
  color: var(--muted);
  /* One digit wide, so the four titles start on the same column down the page. */
  width: .75rem;
}
/* The caption after a title is a sentence, not a heading: it drops the uppercase treatment. */
h2 .muted { text-transform: none; letter-spacing: 0; font-weight: 400; }

h3 {
  font: 650 .7rem/1 var(--mono);
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 0 0 .5rem;
}

section {
  border-top: 1px solid var(--line);
  padding: 1.5rem 0 1.9rem;
}
/* Nothing below it to separate from, and a trailing rule under the last section reads as a
   section that failed to load. */
section:last-child { padding-bottom: 0; }

section.disabled { opacity: .35; pointer-events: none; }
/* !important because `label`, `.row` and `.take` all set display further down the file and
   would otherwise outrank this by source order — a hidden row stayed on screen. */
.hidden { display: none !important; }
code { font: .92em var(--mono); }

/* --- controls --- */

.row { display: flex; flex-wrap: wrap; gap: .6rem; align-items: flex-end; }
.row + .row, table + .row, p + .row { margin-top: .9rem; }

label {
  display: flex;
  flex-direction: column;
  gap: .3rem;
  font-size: .75rem;
  color: var(--muted);
}
/* Under the field's name rather than beside it — the same treatment a column heading's caption
   gets. Run together on one line, in one grey, the name disappears into the sentence. */
label .muted { display: block; font-size: .7rem; }

input, select, textarea {
  font: inherit;
  padding: .35rem .5rem;
  border: 1px solid var(--line-strong);
  border-radius: 5px;
  background: var(--bg);
  color: inherit;
  width: 100%;
  min-width: 0;
}

input[type="radio"] { width: auto; padding: 0; accent-color: var(--ship); }
/* A checkbox beside its text, against `label`'s stacked default. */
.check { flex-direction: row; align-items: center; gap: .35rem; }
.check input { width: auto; padding: 0; accent-color: var(--ship); }
/* Room for a name and a dialect; the gender select sizes itself off its options. */
#meta-name, #meta-dialect, #meta-dialect-new { min-width: 11rem; }
textarea { resize: vertical; }
input[type="number"] { max-width: 7rem; font-family: var(--mono); }
/* A provider id is a fixed-length handle: wide enough for all of it, on both tabs, or you are
   reading the first two thirds of it and trusting the rest. */
#voice-id, #lab-voice-id { font-family: var(--mono); min-width: 15rem; }
#voice-pick, #probe-voice-pick { min-width: 15rem; }
/* Name, dialect and gender on one line, for every voice in the repo. */
#lab-voice-pick { min-width: 20rem; max-width: var(--measure); }
/* Set by renderVoiceIdCheck rather than by :invalid, so an empty field stays neutral — there
   is no placeholder left to hang :placeholder-shown off. */
#voice-id.bad { border-color: var(--error); }

/* The UA only grays a disabled <select> on its own, so a locked text field looked editable. */
input:disabled, select:disabled, textarea:disabled { opacity: .45; cursor: default; }

:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

button {
  font: inherit;
  padding: .3rem .65rem;
  border: 1px solid var(--line-strong);
  border-radius: 5px;
  background: var(--soft);
  color: inherit;
  cursor: pointer;
  white-space: nowrap;
}

button:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
button:disabled { opacity: .45; cursor: default; }
button.wide { width: 100%; }
button.primary { background: var(--ink); border-color: var(--ink); color: var(--bg); }
button.primary:hover:not(:disabled) { color: var(--bg); opacity: .85; }
button.primary:disabled { background: var(--soft); border-color: var(--line-strong); color: inherit; }
/* Fills in one field, so it is not shaped like the button that starts a session. Still a
   <button>: it does something, it does not go anywhere. */
button.link {
  padding: 0;
  border: 0;
  background: none;
  color: var(--accent);
  font-size: .75rem;
  text-decoration: underline;
  text-underline-offset: 2px;
}
button.link:hover:not(:disabled) { color: var(--accent); text-decoration-thickness: 2px; }

.tab { font-family: var(--mono); }
.tab.active { border-color: var(--ink); background: var(--ink); color: var(--bg); }
.tab.active:hover { color: var(--bg); }

/* A sub-tab sits under a top-level tab, so it stays quieter than the filled block above it —
   but the underlined strip it used to be was a 2px hairline in grey, and nobody saw there was
   a second row of tabs at all. A segmented control instead: the group is a visible box, and
   the selected one is a raised pill inside it. Distinctly not the top row's treatment, so the
   two rows still read as levels rather than peers. Deliberately not also a .tab: sharing that
   class meant every .tab.active rule had to be undone here, and the one in the dark scheme at
   the end of the file undid the undoing. */
.subtab {
  font-family: var(--mono);
  background: none;
  border: 1px solid transparent;
  border-radius: 5px;
  color: var(--muted);
  padding: .3rem .8rem;
}
/* A tint on hover, so the track answers to the pointer anywhere on it rather than only the
   label changing colour. */
.subtab:hover:not(:disabled) { color: var(--ink); background: var(--bg); }
/* Lifted off the track: the border alone left the pill and its groove the same object. The
   monospace face means the weight change costs no width, so nothing shifts on switching. */
.subtab.active, .subtab.active:hover {
  color: var(--ink);
  font-weight: 650;
  background: var(--bg);
  border-color: var(--line-strong);
  box-shadow: 0 1px 2px rgb(0 0 0 / 8%);
}

/* --- config forms --- */

/* One card style for both config forms. They sit in different sections now, so which is
   which is a matter of where you are on the page, not of how the box is drawn. */
fieldset {
  border: 1px solid var(--line);
  border-radius: 6px;
  padding: .5rem .8rem .8rem;
  margin: 1rem 0 .7rem;
  /* Same right edge as the textarea below it: a card running the full page width past three
     fields is a box around empty space. */
  max-width: var(--measure);
}
/* Opens a panel on the probe tab, where the section's own padding is already the gap. */
section > fieldset:first-child { margin-top: 0; }
/* The panel's name in ink, what it maps to in config.json after it in grey: a legend that was
   grey end to end read as a caption for the box rather than its title. */
legend { font-size: .75rem; font-weight: 650; color: var(--ink); padding-inline-end: .3rem; }
legend .muted { font-weight: 400; }
/* Sits directly under the section heading when it moves here, so no leading gap — and enough
   room below it that the language row belongs to the table rather than to this card. */
#generation-slot fieldset { margin: 0 0 1.4rem; }
/* Fields share the card's width rather than each claiming a fixed 200px: at the reading
   measure a fourth 200px column doesn't fit, and a lone field wrapping onto its own row with
   the rest of the card empty beside it reads as a second group of settings. */
.form-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  justify-content: start;
  gap: .7rem 1.2rem;
  align-items: end;
}
/* Ruled off on the left of the settings it generates with: same row, but a knob for this
   session rather than a value that ends up in config.json. */
#candidates-field { border-right: 1px solid var(--line); padding-right: 1.2rem; }
.form-grid .field:has(textarea) { grid-column: 1 / -1; max-width: var(--measure); }
.field a { color: var(--accent); }
/* Under its input, so a long style prompt's count sits where you were already looking. */
/* .warn is defined further down, so it wins the colour when the count goes over. */
.counter { font-family: var(--mono); font-size: .7rem; color: var(--muted); }

/* A language tab and the checkbox that decides whether config.json declares it at all. */
/* The row switches the whole table under it, so it needs air on both sides — pressed against
   the header row it read as one more column heading. */
#lang-tabs { margin-bottom: 1rem; }
/* One pill per language, with the tick inside it: loose beside the button, the checkbox read
   as a separate control rather than as this language's on switch. The button keeps its own hit
   area — ticking decides whether the language ships, clicking decides which rows you see. */
.lang {
  display: flex;
  align-items: center;
  gap: .4rem;
  padding-left: .5rem;
  border: 1px solid var(--line-strong);
  border-radius: 5px;
  background: var(--soft);
}
.lang input { width: auto; padding: 0; accent-color: var(--ship); }
/* The pill draws the box now, so the button inside it draws nothing. */
.lang .tab, .lang .tab.active, .lang .tab:hover:not(:disabled) {
  border-color: transparent;
  background: none;
}
.lang:has(.tab.active) { border-color: var(--ink); background: var(--ink); }
.lang:has(.tab.active) .tab { color: var(--bg); }
.lang.off .tab { opacity: .5; text-decoration: line-through; }

/* Sits above both panels it locks, so it reads as a heading for the pair, not a field. */
#unlock-row { margin-top: 1rem; }

/* --- fillers table --- */

table { width: 100%; border-collapse: collapse; margin-top: .25rem; }
th {
  text-align: left;
  font-size: .72rem;
  font-weight: 650;
  padding: 0 .5rem .4rem;
  border-bottom: 1px solid var(--line-strong);
  vertical-align: bottom;
}
th .muted { display: block; font-weight: 400; font-size: .68rem; }
td { border-bottom: 1px solid var(--line); padding: .4rem .5rem; vertical-align: top; }

.col-text { width: 23%; }
.col-takes { width: auto; }
.col-actions { width: 7.5rem; }
.col-remove { width: 2.5rem; }

.col-actions .status { display: block; min-height: 1.1rem; margin-top: .3rem; }

.remove {
  width: 1.75rem;
  padding: .3rem 0;
  color: var(--muted);
  background: none;
  border-color: transparent;
  font-size: 1rem;
  line-height: 1;
}
.remove:hover { color: var(--error); border-color: var(--error); }

/* One grid shared by every take: pick, number, audio, note. Nothing optional in the
   first three columns, so the radios and players line up down the whole table. */
.takes { display: flex; flex-direction: column; align-items: start; gap: .2rem; }
.take {
  display: grid;
  /* The note column keeps its width whether or not there's a note, so the staged tint is
     the same size on every row instead of ragging along the right edge. */
  grid-template-columns: 1.1rem 1.2rem var(--audio-w) 7rem;
  gap: .5rem;
  align-items: center;
  padding: .15rem .45rem;
  border-radius: 5px;
}
.take.staged { background: var(--ship-bg); box-shadow: inset 2px 0 0 var(--ship); }
.take-n { font: .72rem/1 var(--mono); color: var(--muted); text-align: right; }
.take.staged .take-n { color: var(--ship); font-weight: 650; }
.take-audio audio { display: block; width: 100%; height: 30px; }
.take-note { font-family: var(--mono); font-size: .7rem; }

/* --- review --- */

/* One grid for the whole review, languages and rows as display:contents. The phrase column
   is exactly as wide as the longest phrase, so nothing stretches into dead space, and every
   player lines up — across both languages, not just within one. */
#review {
  display: grid;
  grid-template-columns: 1.5rem minmax(0, max-content) var(--audio-w);
  gap: .3rem .8rem;
  align-items: center;
  max-width: var(--measure);
}
.review-lang, .review-row { display: contents; }
#review h3 { grid-column: 1 / -1; margin: .9rem 0 .1rem; }
#review h3:first-child { margin-top: 0; }
.review-index { font: .72rem/1 var(--mono); color: var(--muted); text-align: right; }
/* dir="auto" plus text-align:start: an Arabic block reads right-aligned, an English one
   left-aligned, and neither gets truncated mid-word. */
.review-phrase { text-align: start; overflow-wrap: anywhere; }
.review-audio audio { display: block; width: 100%; height: 30px; }

/* --- playground --- */

/* The title line owns the repo refresh: it re-reads every list on the page, so it is not
   part of any one tab. */
#header { align-items: center; margin-bottom: 1.25rem; }
#header h1 { margin: 0; }
#pull-status { margin-left: auto; text-align: right; }
/* The rule under the page's tabs belongs to the tab strip, and so is on all three tabs. It
   used to be the first section's own border-top, which the other two pages switch off — so
   only the voice page had a line under the tabs. */
#nav { margin-bottom: 0; padding-bottom: 1.1rem; border-bottom: 1px solid var(--line); }
/* One top gap per page, so every tab starts the same distance under that rule. */
#page-voice, #page-playground, #page-pronunciation { padding-top: 1.5rem; }
#page-voice > section:first-child { border-top: 0; padding-top: 0; }
/* Pushed to the far right: the tabs on the left produce something that ships, this one is a
   listening bench, and the gap says so. */
#nav-playground { margin-left: auto; }
/* The track the pills sit in. Only as wide as its buttons — stretched across the page it
   reads as a toolbar rather than a two-way switch. The provider strip is in here too: picking
   a provider is the same kind of choice one level under the page's own tabs, and drawn as a
   filled block it was indistinguishable from them. */
#lab-tabs, #pd-tabs, #provider-tabs {
  gap: .25rem;
  align-items: stretch;
  width: max-content;
  max-width: 100%;
  padding: .2rem;
  border: 1px solid var(--line);
  border-radius: 7px;
  background: var(--soft);
  margin-bottom: 1.5rem;
}
/* The tab strip's own margin is the gap; the section's top padding on top of it left a
   three-rem hole between the sub-tabs and the panel they switch. */
#page-playground section, #page-pronunciation section { border-top: 0; padding-top: 0; }
/* One shape for every tab's instructions: an h3 over a line or two of grey, held to the
   reading measure. Every tab gets one, so each says what it is for, and none of them
   outweighs the controls underneath. */
.hint { max-width: var(--measure); margin: 0 0 1.4rem; color: var(--muted); font-size: .78rem; }
.hint p { margin: 0; }

/* One measure for every block of prose and the two big textareas, so the page has a single
   reading column rather than a different line length per section. */
.blurb, .wide-field { max-width: var(--measure); }
/* A paragraph explaining a section is not the section's content: grey and a notch down, or it
   outweighs the controls it is describing. A notch above .muted, since it is read, not glanced. */
.blurb { margin: 0 0 1.1rem; color: var(--muted); font-size: .8rem; }
.wide-field { margin: 1.1rem 0; }
#probe-texts, #lab-text { font-size: .8rem; }

/* Newest at the top, and every player on the same column so two takes of the same line are
   read as a comparison. The configs that produced each one ride along on the right: without
   them a stack of players is four identical rows. */
#lab-results {
  display: grid;
  grid-template-columns: 1.6rem minmax(0, max-content) var(--audio-w) minmax(0, 1fr);
  gap: .35rem .9rem;
  align-items: center;
  margin-top: 1.4rem;
}
.lab-result { display: contents; }
.lab-n { font: .72rem/1 var(--mono); color: var(--muted); text-align: right; }
.lab-text { text-align: start; overflow-wrap: anywhere; }
.lab-audio audio { display: block; width: 100%; height: 30px; }
.lab-configs { font-family: var(--mono); font-size: .7rem; overflow-wrap: anywhere; }
/* Counts and their intervals in one grid: the digits line up on the decimal point, which is
   the only way four percentages read as a comparison rather than four sentences. */
.probe-counts, .probe-texts-out, .probe-messages {
  display: grid;
  gap: .25rem .9rem;
  align-items: baseline;
  max-width: var(--measure);
  margin-top: 1rem;
}
.probe-counts { grid-template-columns: 6rem 3rem 4rem max-content; }
.probe-texts-out { grid-template-columns: 2rem minmax(0, 1fr); }
.probe-messages { grid-template-columns: 6rem 3rem minmax(0, 1fr); }
.probe-name { font-family: var(--mono); font-size: .78rem; }
.probe-num { font-family: var(--mono); font-size: .78rem; text-align: right; }
.probe-raw { font-family: var(--mono); font-size: .72rem; color: var(--muted); overflow-wrap: anywhere; }
#probe-result h3 { margin-top: 1.4rem; }

/* --- the repo refresh --- */

/* One fixed layer over everything: it blurs what is behind it and swallows the clicks. The
   `inert` attribute app.js puts on <main> is what stops the keyboard reaching in. */
#busy:not(.hidden) {
  position: fixed;
  inset: 0;
  z-index: 10;
  display: grid;
  place-items: center;
  cursor: progress;
  backdrop-filter: blur(3px);
  background: color-mix(in srgb, var(--bg) 55%, transparent);
}
#busy span {
  padding: .5rem .8rem;
  border: 1px solid var(--line-strong);
  border-radius: 6px;
  background: var(--bg);
  font-size: .8rem;
}

/* --- messages --- */

.muted { color: var(--muted); font-size: .75rem; }
.status { font-size: .72rem; }
.status.busy { color: var(--accent); }
.status.error, .error { color: var(--error); }
.warn { color: var(--warn); }

#banner {
  background: #fdecea;
  color: var(--error);
  padding: .5rem .7rem;
  border-radius: 5px;
  margin: 0 0 1rem;
  font-size: .85rem;
}
#warnings p { margin: .15rem 0; font-size: .8rem; }
#warnings:not(:empty) { margin-bottom: 1rem; }
#notes { max-width: var(--measure); }
#rows-empty { margin: .8rem 0 0; }
/* These sit at the end of a row of controls whose fields are bottom-aligned, so left to
   themselves they hang off the baseline of the inputs rather than reading with them. */
#download-reason, #lab-status, #probe-progress, #pull-status { align-self: center; }
/* One line under the voice row: the link that fills the id in, what is wrong with it, and
   what is loaded. Always rendered, so the row above never shifts when a message appears. */
#voice-hints {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: .3rem 1rem;
  margin: .55rem 0 0;
  min-height: 1.1rem;
  font-size: .75rem;
}
/* An empty span is still a flex item, and still takes a gap: with nothing wrong with the id,
   the status line sat a gap's width in from the field it belongs under. */
#voice-hints span:empty { display: none; }
/* Under the field it belongs to, at the reading measure — it is a sentence, not a caption. */
#meta-dialect-warn { margin: .7rem 0 0; font-size: .8rem; max-width: var(--measure); }
/* #review is a div, so the generic `p + .row` / `table + .row` spacing misses it and the
   download button ends up sitting on the last player. */
#review + .row { margin-top: 1.6rem; }

/* --- token gate --- */

/* :not(.hidden), because an id rule setting `display` would otherwise outrank `.hidden`. */
#gate:not(.hidden) { display: flex; flex-direction: column; align-items: flex-start; gap: .7rem; max-width: 17rem; }
#gate h1 { margin: 0; }
#token { font-family: var(--mono); }

/* --- pronunciation --- */

/* Numbers in the margin: size and colour come from the .hint around it. */
.steps { margin: 0; padding-left: 1.2rem; }
.steps li { margin: .1rem 0; }
.steps a { color: var(--accent); }

#pd-pick { min-width: 15rem; }
#pd-target { margin: .9rem 0 1.2rem; }
/* Three short columns, so at full page width the provider ends up alone on the far right.
   Held to the reading measure the name, id and provider read as one line each. */
#pd-target .pd-scroll { max-width: var(--measure); }
/* Narrower than the reading measure on purpose: a PD is almost all Arabic, `dir: auto` puts
   every cell against its own right edge, and at full width a word and its pronunciation end
   up half a screen apart with nothing between them. */
#pd-current { margin: 0 0 1.2rem; max-width: 30rem; }
/* Both panels here start closed, so a bare line of text was all anyone saw of them. A
   bordered chip that lights up under the pointer reads as something to press; the browser's
   own disclosure triangle says which way it is. */
.disclosure > summary {
  cursor: pointer;
  width: max-content;
  max-width: 100%;
  font-size: .8rem;
  padding: .25rem .55rem;
  border: 1px solid var(--line-strong);
  border-radius: 6px;
  background: var(--soft);
}
.disclosure > summary:hover { border-color: var(--ink); }
.disclosure[open] > summary { margin-bottom: .6rem; }
#pd-current p { margin: .3rem 0 0; font-size: .8rem; }
#pd-current th:first-child { width: 50%; }
#pd-target th:first-child { width: 40%; }
#pd-target p, #pd-problems p, #pd-diff p { margin: 0 0 .3rem; font-size: .8rem; }
#pd-problems:not(:empty), #pd-diff:not(:empty) { margin: 1.3rem 0; }
/* #pd-problems and #pd-diff are divs, so the generic `p + .row` / `table + .row` spacing
   misses this row and the download button sat flat against the file picker. */
#pd-download-row { margin-top: 1.3rem; }
#pd-problems th:first-child, #pd-diff th:first-child { width: 5rem; }
.pd-line, .pd-change, .pd-id { font: .72rem/1.5 var(--mono); white-space: nowrap; }
.pd-line { color: var(--muted); text-align: right; }
.pd-id { color: var(--muted); }
/* A whole-sheet diff is thousands of rows. Truncating it hides the rows you wanted to see, so
   it scrolls in place instead, with the header stuck and the counts above and the download
   below staying where they are. box-shadow rather than the th's border: a collapsed border
   scrolls away from a sticky cell. */
.pd-scroll { max-height: 60vh; overflow-y: auto; }
.pd-scroll th {
  position: sticky;
  top: 0;
  background: var(--bg);
  box-shadow: inset 0 -1px 0 var(--line-strong);
}
.pd-added { color: var(--ship); }
.pd-changed { color: var(--accent); }
.pd-removed { color: var(--error); }

@media (prefers-color-scheme: dark) {
  :root {
    --bg: #15171b;
    --line: #2a2e35;
    --line-strong: #3d434b;
    --soft: #21252b;
    --ink: #e7e9ec;
    --muted: #98a0aa;
    --ship: #4ac47f;
    --ship-bg: #17281d;
  }
  #banner { background: #3a1d1a; color: #ffb4ab; }
  button.primary { background: var(--ink); color: #15171b; }
  button.primary:hover:not(:disabled) { color: #15171b; }
  .tab.active { background: var(--ink); color: #15171b; }
  .tab.active:hover { color: #15171b; }
}
