/* A sentence's status, drawn as one visual language: a tint on the sentence's own box, a
   dot beside it, and what each of the three reading views shows of the two. A page links
   this file and mounts sentences carrying `class="sentence"` and `data-status`, which
   public/lib/sentence-status.mjs writes; the reading area it mounts them in is
   `#reading-area`. The studio is the one surface that does today, and its fixtures under
   public/test/ link this file for the same reason they link document.css.

   Its own file rather than studio.html's <style> block, for the two reasons document.css
   states: rules a second surface would have to copy drift apart, and a page's inline
   <style> is outside the stylelint gate — which is where the sync block's own
   no-descending-specificity error was caught.

   Status only. The words inside a sentence are studio.html's: the search and live-reading
   highlights, the misread-word marks, the recording border, and the announcement and
   silenced badges, together with what the clean view hides of those. `--_dot-ink`, the ink
   level an empty dot takes, is the studio body's hover state and is declared there too;
   the dot rule below carries the resting value as its fallback so this file paints
   correctly on a surface that sets no such state.

   Rules are ordered so specificity ascends within each target element, which is what
   stylelint's no-descending-specificity asks for and why the flat render's rhythm sits
   after the tints rather than beside the host.
   @tt-about recording-pipeline
   @tt-about reading-surface */

/* ---- The sentence host ---- */

/* Every recordable sentence's host element, in both renders: the rendered document's
   own hosts — a heading, a list item, a table cell, a sentence span inside a paragraph —
   and the flat render's rows. The class is the document's own vocabulary: its
   multi-sentence paragraphs arrive with span.sentence already, and the reader completes
   it on single-sentence blocks. A host keeps the document's box, no padding or margin of
   its own, so the block elements supply the rhythm.

   The tint is the colour only, never the `background` shorthand: the unfinished-sync
   states below lay a `background-image` over it — the travelling sheen and the paused
   stripes — and the shorthand would clear that layer. */
.sentence {
  --_status: transparent;
  --_status-bg: var(--_status);
  --_status-bg-mix: 8%;
  position: relative;
  background-color: color-mix(in srgb, var(--_status-bg) var(--_status-bg-mix), transparent);
  transition: opacity 200ms ease;
}
.sentence[data-dim="rerecord"] { opacity: 0.3; }

/* ---- The settled status tints ---- */

.sentence[data-status="recorded"],
.sentence[data-status="review-approved"] { --_status: var(--status-recorded); }
/* Heard through by the live recogniser while the take is still running: the recorded hue,
   lighter, because nothing is saved yet and the take's own status takes over at Stop. */
.sentence[data-status="heard"]           { --_status: var(--status-recorded); --_status-bg-mix: 5%; }
.sentence[data-status="error"],
.sentence[data-status="review-rejected"] { --_status: var(--status-error); }
/* Use the mismatch color for refused, stale or held recordings that need the narrator’s attention. Reserve the syncing color for work still in progress. */
.sentence[data-status="parked"]          { --_status: var(--status-mismatch); --_status-bg-mix: 12%; }
.sentence[data-status="mismatch"]        { --_status: var(--status-mismatch); --_status-bg-mix: 12%; }
/* Previous audio counts as recorded, so it is tinted like one, but in grey: a grey dot, and
   a background that mixes black (white in dark mode), apart from both the recorded green
   and the selected sentence's blue. */
.sentence[data-status="previous-audio"]  {
  --_status: var(--status-previous);
  --_status-bg: var(--status-previous-bg);
  --_status-bg-mix: 14%;
}

/* ---- Unfinished final sync ---- */

/* Shown by the sentence's own background so it reads in every view, including the ones
   that hide the status dot. Queued is a tint alone; a running decode adds a sheen that
   travels across the sentence; paused adds still stripes; failed is the error tint.
   Under reduced motion the sheen is replaced by the same still stripes as paused, so the
   state is still told apart from queued, in the spirit of the reveal flash in /ui/ui.css.

   Each state states its own mix here and nowhere else. The clean view, which zeroes every
   other status tint, exempts these four rather than setting their mixes a second time. */
.sentence[data-status="sync-queued"] {
  --_status: var(--status-syncing);
  --_status-bg-mix: 10%;
}

.sentence:is([data-status="syncing"], [data-status="sync-transcribing"]) {
  --_status: var(--status-syncing);
  --_status-bg-mix: 18%;
  background-image: linear-gradient(100deg,
    transparent 35%, color-mix(in oklch, var(--_status) 28%, transparent) 50%, transparent 65%);
  background-size: 200% 100%;
  background-repeat: no-repeat;
  animation: sync-sheen 2.4s var(--ease-in-out-3) infinite;
}

.sentence[data-status="sync-paused"] {
  --_status: var(--status-mismatch);
  --_status-bg-mix: 12%;
  background-image: repeating-linear-gradient(135deg,
    transparent 0 6px, color-mix(in oklch, var(--_status) 14%, transparent) 6px 8px);
}

.sentence[data-status="sync-failed"] {
  --_status: var(--status-error);
  --_status-bg-mix: 16%;
}

/* ---- What the model heard, against where the audio is ----

   A sentence carries two markers, written independently: `data-transcribed` says this machine
   holds what the model heard for it, and `data-status` says how far the recording has got. The
   attribute is named for the final decode rather than for hearing, because `data-status="heard"`
   already means something else — the live recogniser passed this sentence during a take that is
   still running. They are separate markers because they come apart — a narrator reading without a connection can hold the whole
   transcript while nothing has been uploaded at all, and nothing a narrator does is gated on the
   upload having run. The four combinations:

     no words, unfinished sync    the tints above alone — queued, running, paused or failed
     words, unfinished sync       those, plus the rule below; what a session spends most of
     words, sync over             the recorded tint and no rule: the status says everything
     no words, sync over          a server take this machine holds no transcript for

   Double, because the underline styles inside a sentence are a vocabulary already spoken for,
   word by word: solid is an emphasis annotation, a search hit and the word cursor, dotted is the
   book's own link, wavy a confirmed mismatch, dashed a provisional one, and strikethrough a word
   the recording never reached (studio.html). Double is the one left; this is also the only mark
   spanning a whole sentence rather than picking words out of it, and no word mark is drawn in
   the syncing hue. A shape as well as a colour, which /ui/docs/about-accessibility.html § Color
   asks of any state that has to be told apart.

   The offset clears the word marks' own --size-1, so a mismatch keeps its wavy line legible
   inside a sentence carrying this one. Drawn on the sentence rather than as a box edge, so it
   follows the text on every line of a paragraph instead of ruling the bottom of the block. */
.sentence[data-transcribed]:is([data-status="sync-queued"], [data-status="syncing"],
    [data-status="sync-transcribing"], [data-status="sync-paused"], [data-status="sync-failed"]) {
  text-decoration: underline double var(--status-syncing);
  text-underline-offset: var(--size-2);
  text-decoration-thickness: 1px;
}

@keyframes sync-sheen {
  from { background-position: 150% 0; }
  to { background-position: -50% 0; }
}

@media (prefers-reduced-motion: reduce) {
  .sentence:is([data-status="syncing"], [data-status="sync-transcribing"]) {
    animation: none;
    background-image: repeating-linear-gradient(135deg,
      transparent 0 6px, color-mix(in oklch, var(--_status) 14%, transparent) 6px 8px);
    background-size: auto;
  }
}

/* ---- The flat render's rhythm ---- */

/* The rows the reading area holds directly, when the chapter is shown as a list of
   sentences rather than as the printed document. */
#reading-area > .sentence { padding-left: var(--size-4); margin-block: 0; }
#reading-area > .sentence + .sentence { margin-top: var(--size-1); }
#reading-area > .sentence.paragraph-start { margin-top: var(--size-4); }

/* ---- The status dot ---- */

/* The dot's margin placement: absolute against its own sentence, at the start of the
   host's box. A flat row gives it the row's padding to sit in; a document host keeps the
   document's box, so there the dot hangs outside it — left of the column for a paragraph
   or a heading, and into the strip a list reserves for an item's marker. It follows the
   block it belongs to, not the column edge ([[reading-surface]]).

   A list reserves 28 px before an item's text (--size-6, document.css) and the dot covers
   that strip from 24 px to 14 px before the text, so a marker wider than the 14 px left
   over — a decimal `10.`, a lower-roman `ii.` — is drawn partly under the dot. The
   overlap is accepted: the item is still readable and the dot stays beside the block it
   marks. Do not move the dot out into the reading column's own margin, and do not widen
   the list's start padding to give the two a strip each
   ([[reading-surface]], History 2026-09-17). */
.sentence-status-dot {
  all: unset;
  position: absolute;
  left: 0;
  top: 0.35em;
  width: 0.625rem;
  height: 0.625rem;
  border-radius: var(--radius-round);
  cursor: pointer;
  background: var(--_status);
}
.sentence-status-dot:focus-visible {
  outline: var(--border-size-2) solid var(--brand);
  outline-offset: var(--border-size-2);
}
.document .sentence-status-dot { left: calc(-1 * var(--size-5)); }
/* Every sentence carries a dot, including one with nothing to report: the dot is the
   manuscript's keyboard handle (#reading-area's roving tabindex walks it, Enter on it
   opens the sentence menu), so gating it on a status leaves a freshly imported chapter
   with no tab stop at all. It has no --_status to take a colour from, so it takes the
   text ink at --_dot-ink instead: near-invisible at rest, so an unannotated chapter does
   not read as a column of bullets, and light grey while the pointer is anywhere over
   .studio-body. See [[editor-annotation-model]]. */
.sentence:not([data-status]) .sentence-status-dot {
  background: color-mix(in oklch, var(--text-2) var(--_dot-ink, 12%), transparent);
}
/* An informational status takes the text ink rather than a hue of its own. The
   --status-* ramp carries five hues that each say something about a recording, and this
   status says the opposite: the recording produced nothing for this sentence, so it is
   unrecorded and the tooltip carries the reason. Full strength rather than the
   near-invisible --_dot-ink resting value above, because unlike an unannotated sentence
   this one has something to report. */
.sentence[data-status="info"] .sentence-status-dot {
  background: var(--text-2);
}
/* A paused decode squares the dot off, so the state has a shape and not only a hue. */
.sentence[data-status="sync-paused"] > .sentence-status-dot {
  border-radius: 0;
}
/* A take the chosen final model would read better draws its dot as a ring in the same hue,
   filled once that model has read it: a mark in the margin, which leaves the text alone for
   a narrator who goes on working ([[recording-pipeline]] § *"A larger model may read a filed
   take again"*). The overview header counts the same sentences. */
.sentence[data-unsynced] > .sentence-status-dot {
  background: transparent;
  box-shadow: inset 0 0 0 var(--border-size-2) var(--_status);
}
/* Only the states where work is still running pulse, and only for a reader who has not
   asked for less motion. The preference is answered by the positive query around the rule,
   not by an `animation: none` rule under a `reduce` query, because such a rule has to
   reach this selector's specificity to win and a broader one — `.sentence
   .sentence-status-dot`, say — silently would not. That is also the form /ui/ui.css uses;
   see /ui/docs/styles-motion.html. */
@media (prefers-reduced-motion: no-preference) {
  .sentence:is([data-status="error"], [data-status="syncing"], [data-status="sync-transcribing"]) .sentence-status-dot {
    animation: pulse 1.5s ease-in-out infinite;
  }
}

/* The inline placement: the second and later sentences of a block, whose margin the
   first one has, and every sentence in a table cell or caption, where there is no margin
   to sit in. The dot sits before the sentence's text, between the words.

   Positioned `relative` with `inset: auto` rather than `static`: the dot lays out in the
   flow either way, but only a positioned dot is the containing block for its own
   `::after`, and the click pulse is drawn there (`.attention-pulse` in /ui/ui.css, inset
   to its host). `.sentence` is `position: relative`, so a static dot hands the ring the
   sentence's whole text box and the pulse rings the words instead of the handle. The
   library sets that `position: relative` through a zero-specificity `:where()` so an app
   can place the host, and this rule outranks it, so the dot carries the value itself.
   `inset: auto` drops the `top` the page-break placement below would otherwise leave on a
   dot this rule has returned to the flow.

   The two placements are one block, so they share a rule, and the rule sits near the end
   of this section although its cell selector, at 0-2-1, ranks below the per-status paint
   rules above it — `.sentence:not([data-status]) .sentence-status-dot` at 0-3-1 is the one
   stylelint names. The dot is placed by where it sits in the document and painted by what
   its sentence's status is; the two axes set disjoint properties — position, inset,
   display, margin and alignment here, background, border-radius and animation there — so
   which comes first decides nothing between them. Ordering them by specificity would mean
   writing this block twice. */
/* stylelint-disable-next-line no-descending-specificity */
.document :is(td, th, caption) .sentence-status-dot,
.document :is(span, a).sentence:not(:first-child) > .sentence-status-dot {
  position: relative;
  inset: auto;
  display: inline-block;
  margin-inline-end: var(--size-1);
  vertical-align: middle;
}

/* A page break inside a list item takes a line of its own, drawn across the item from an
   inset (--pagebreak-inset, document.css), and its dot sits in that inset at the item's own
   start edge, which is the column where the narrator finds every other sentence handle. The
   inline placement above would draw it on the rule itself, beside the page number, where it
   reads as part of the printed page marker rather than as the studio's own control. A page
   break inside a table cell draws its rule across the whole reading column, so its dot
   takes the cell placement like every other cell sentence: in the marker's own box, at the
   cell's edge, which is where the rule shows which cell the break belongs to.

   This rule and the inline placement above are tied at 0-4-1 — `.document`, `.sentence`,
   `.pagebreak` and `.sentence-status-dot` here, against `.document`, `.sentence`,
   `:first-child` and `.sentence-status-dot` there — so source order decides between them
   and this rule has to stay after that one. Both match whenever a page break is itself a
   sentence host and is not the first child of its list item, which is the ordinary case for
   a break in the middle of an item's text.
   `public/test/studio-reader/pagebreak-dot-wpt.spec.mjs` measures where the dot is drawn
   in a browser, so moving this block back above the other one turns that test red instead
   of quietly restoring the old placement. */
.document li .sentence.pagebreak > .sentence-status-dot {
  position: absolute;
  display: block;
  left: calc(-1 * var(--pagebreak-inset));
  margin-inline-end: 0;
}

/* ---- The view control's three states ----
   `data-view` on #reading-area mirrors StudioState.reading_view; `dots` is the rendering
   above and needs no rule. `marks` and `clean` keep the dot in the DOM as a handle that
   paints nothing and takes no room from the words — still the roving tab stop, still
   named, still the Enter target, and a pointer target too, which a zero-width box was
   not: a left click is how the sentence menu opens, and in these two views only
   right-click reached it.

   The handle is --size-1 wide and one line tall, and the inline placement's
   `margin-inline-end: var(--size-1)` pays for that width, so its advance is what the bare
   margin's was and the words sit exactly where they did while it had no width at all. The
   absolute placements carry no margin and lay out unchanged either way.

   Unpainted through `background`, not `opacity: 0` or `visibility: hidden`: the element
   has to stay paintable so :focus-visible can draw a vertical bar in the dot's place. Not
   .visually-hidden either — that recipe takes the element out of flow and clips its paint,
   so it could draw no bar and would have no place left to draw it at. The bar is a static
   border; the status pulse is dropped too, so nothing animates on focus. */

/* `clean` shows the printed document with only the current sentence marked: no status
   tint, no mismatch or live-reading marks, no struck unrecorded tail. The document's own
   formatting and the editor's annotations stay, and so do playback and the active
   sentence, whose rules are untouched. What the view hides of the words themselves is in
   studio.html, beside the rules that draw them.

   Unfinished final sync is exempt: it is not a status mark but work still running on the
   narrator's recording, and the dot that would otherwise carry it is hidden here, so the
   tint and the sheen stay. Exempting the five `sync` values — `syncing` included, since it
   begins with the same four letters — leaves each state's own mix in force, which is why
   those four figures are written once and only in the rules above. */
#reading-area[data-view="clean"] .sentence:not([data-status^="sync"]) { --_status-bg-mix: 0%; }
#reading-area:is([data-view="clean"], [data-view="marks"]) .sentence-status-dot {
  inline-size: var(--size-1);
  block-size: 1em;
  top: 0.2em;
  border-radius: 0;
  animation: none;
  background: transparent;
  margin-inline-end: 0;
}
#reading-area:is([data-view="clean"], [data-view="marks"]) .sentence-status-dot:focus-visible {
  outline: none;
  border-inline-start: var(--border-size-2) solid var(--brand);
}
/* A queued decode has not started, so its dot is drawn hollow: the ring says which
   sentences the job covers while the filled dot stays for work that is running. Only in
   `dots`, since the other two views leave the dot unpainted. */
#reading-area:not([data-view="marks"], [data-view="clean"]) .sentence[data-status="sync-queued"] > .sentence-status-dot {
  background: transparent;
  outline: var(--border-size-1) solid var(--_status);
}
