/* ==========================================================================
   BlueTTS — the demo, in three bands: the controls row (language and voice
   pickers, Clear, settings), the text, and one fixed-height row that shows
   the samples, the first download or the clip, with Speak at its end; then
   the foot and the list of earlier clips. Owns everything inside .tts (in
   the #demo section, whose heading is css/site.css) plus the phone-only
   #ttsSticky bar. Nothing in it changes size between states.

   Mobile first: base styles are the 390px layout; min-width queries at 640
   and 900. Light only. Every colour is a token from css/tokens.css
   or a color-mix of two tokens; this file defines none.
   Effects only where they do a job: the flat focus outline, the picked
   sample's tint, --live for whatever is playing, and a faint shadow under
   what floats (the picker lists, the settings panel, the phone bar).
   ========================================================================== */

:where(.tts, .tts-sticky) *, :where(.tts, .tts-sticky) *::before, :where(.tts, .tts-sticky) *::after { box-sizing: border-box; }
:where(.tts, .tts-sticky) :where(button, select, textarea, input) { font: inherit; color: inherit; margin: 0; }
:where(.tts, .tts-sticky) button { cursor: pointer; background: none; border: 0; padding: 0; }
/* Android's default tap wash is an off-palette cyan; focus and :active say enough. */
:where(.tts, .tts-sticky) :where(button, select, input, textarea) { -webkit-tap-highlight-color: transparent; }
:where(.tts, .tts-sticky) button:disabled { cursor: default; }
:where(.tts, .tts-sticky) :focus-visible { outline: 2px solid var(--focus); outline-offset: 2px; }
:where(.tts, .tts-sticky) ::selection { background: var(--selection); color: var(--ink); }

/* tokens.css sets this on :root too; repeated so the demo stays light even
   inside a dark container. */
.tts, .tts-sticky { color-scheme: light; }

.tts {
    font-family: var(--font);
    color: var(--ink);
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
}

/* --------------------------------------------------------------------------
   The controls row: language and voice pickers, then Clear and settings
   -------------------------------------------------------------------------- */
.tts-controls {
    display: flex;
    flex-wrap: wrap; /* never wraps in practice: under 360px the pills tighten (below) */
    align-items: center;
    gap: var(--s-2);
}
/* The icon buttons keep their 44px boxes but their glyphs line up with the
   card's edge (Speak's, the tag's): the box runs 10px into the padding. */
.tts-controls-end { display: flex; align-items: center; gap: 0; margin-inline-start: auto; margin-inline-end: -10px; }
.tts-pick, .tts-settings { position: relative; }

/* A pill that opens a short list: 40px, a 44px hit area, the value in the
   full ink and a chevron. Its width follows its value; nothing sits after
   the two pills that could be pushed. */
.tts-pill {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    height: var(--btn-h-sm);
    padding: 0 10px 0 14px;
    border: 1px solid color-mix(in srgb, var(--rule-strong) 40%, var(--line));
    border-radius: var(--r-btn);
    background: var(--field);
    font-size: var(--fs-control);
    font-weight: 500;
    color: var(--ink);
    white-space: nowrap;
    transition: border-color var(--dur-fade) ease-out, background-color var(--dur-fade) ease-out;
}
.tts-pill::after { content: ""; position: absolute; inset: -2px 0; } /* 44px tall to the touch */
.tts-pill-icon { flex: none; color: var(--ink-3); margin-inline-start: -2px; }
.tts-chevron { flex: none; color: var(--ink-3); transition: transform var(--dur-fade) ease-out; }
.tts-pill[aria-expanded="true"] { border-color: var(--rule-strong); }
.tts-pill[aria-expanded="true"] .tts-chevron { transform: rotate(180deg); }
.tts-pill-value[lang="he"] { font-size: 17px; } /* Hebrew sets a touch small beside Latin */

/* The narrowest phones (under 360px): one row still, never Clear and
   settings alone on a second row. The pills tighten (no microphone, 15px),
   and if a long pair still does not fit they shrink and end in an ellipsis
   rather than push the row onto two lines. */
@media (max-width: 359px) {
    .tts-controls { flex-wrap: nowrap; gap: 4px; }
    .tts-pick { min-width: 0; }
    .tts-pill { max-width: 100%; gap: 2px; padding: 0 6px 0 11px; font-size: 15px; }
    .tts-pill-value { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
    .tts-pill-icon { display: none; }
    .tts-controls-end { flex: none; }
    .tts-controls-end .tts-icon-btn { width: 40px; } /* 40 wide, still 44 tall */
}

/* The list, and the settings panel: white sheets that float under their
   button (the one place in the demo with a shadow: they really float). */
.tts-menu, .tts-advanced {
    position: absolute;
    z-index: 30;
    top: calc(100% + 6px);
    margin: 0;
    background: var(--field);
    border: 1px solid var(--line);
    border-radius: 16px;
    box-shadow: 0 12px 32px rgb(15 23 42 / .12), 0 2px 6px rgb(15 23 42 / .06); /* taste-skill-ignore: a menu really floats */
}
.tts-menu {
    inset-inline-start: 0;
    min-width: 220px;
    max-height: min(360px, 60vh);
    overflow-y: auto;
    padding: 6px;
    list-style: none;
}
.tts-menu[hidden], .tts-advanced[hidden] { display: none; }
.tts-menu:not([hidden]), .tts-advanced:not([hidden]) { animation: tts-pop 140ms var(--ease-out-quint) both; }
@keyframes tts-pop { from { opacity: 0; transform: translateY(-4px); } }
.tts-menu [role="option"] {
    display: flex;
    align-items: center;
    gap: 12px;
    min-height: 44px;
    padding: 0 12px;
    border-radius: var(--r-control);
    font-size: var(--fs-control);
    color: var(--ink);
    cursor: pointer;
    user-select: none;
}
.tts-menu [role="option"].is-active { background: var(--paper-2); }
.tts-menu-name[lang="he"] { font-size: 17px; }
.tts-menu-sub { margin-inline-start: auto; font-size: var(--fs-small); color: var(--ink-3); }
/* The picked one: a check at the end, in the link colour. */
.tts-menu [role="option"]::after {
    content: "";
    flex: none;
    width: 16px;
    height: 16px;
    background: var(--accent);
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3 8.5l3.2 3L13 4.5' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
            mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3 8.5l3.2 3L13 4.5' fill='none' stroke='black' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
    opacity: 0;
}
.tts-menu-sub:empty { display: none; }
.tts-menu [role="option"]:not(:has(.tts-menu-sub:not(:empty)))::after { margin-inline-start: auto; }
.tts-menu [role="option"][aria-selected="true"]::after { opacity: 1; }

.tts-icon-btn {
    display: inline-grid;
    place-items: center;
    flex: none;
    width: 44px; height: 44px;
    border-radius: 50%;
    color: var(--ink-2);
    transition: color var(--dur-fade) ease-out, background-color var(--dur-fade) ease-out;
}
.tts-icon-btn[aria-expanded="true"] { color: var(--ink); background: var(--paper-2); }

/* Quality and speed */
.tts-advanced {
    inset-inline-end: 0;
    width: min(320px, calc(100vw - 48px));
    padding: 12px 18px 8px;
    display: grid;
    gap: 4px;
}
.tts-setting label { display: flex; justify-content: space-between; font-size: var(--fs-ui); color: var(--ink-2); }
.tts-setting label span { color: var(--ink); font-weight: 500; font-variant-numeric: tabular-nums; }
.tts-setting .tts-seek { --c: var(--accent); height: 40px; }
.tts-setting .tts-seek::-webkit-slider-thumb { width: 16px; height: 16px; margin-top: -6px; }
.tts-setting .tts-seek::-moz-range-thumb { width: 16px; height: 16px; }

/* --------------------------------------------------------------------------
   The text: no box of its own inside the card, large type. It grows from 3
   to 6 lines with its text, then scrolls; the spacing sits inside the field
   so text never touches the rows above and below, and its top and bottom
   edges fade while it scrolls. While it has focus the card's edge turns
   blue (the focus ring, below).
   -------------------------------------------------------------------------- */
.tts-field { position: relative; margin-top: var(--s-2); }
.tts-text {
    display: block;
    width: 100%;
    field-sizing: content;
    min-height: calc(3lh + 24px);
    max-height: calc(6lh + 24px);
    padding-block: 12px;
    padding-inline: 2px 36px; /* the count's place at the end of the line: on the left for Hebrew */
    border: 0;
    border-radius: var(--r-control);
    background: transparent;
    resize: none;
    font-size: 20px; /* never under 16px */
    line-height: 1.4;
    letter-spacing: -0.012em;
    color: var(--ink);
    caret-color: var(--accent);
    text-wrap: pretty;
    overflow-y: auto;
    scrollbar-width: thin;
    /* The fade is a mask on the text, not a colour: the top and bottom 12px. */
    -webkit-mask-image: linear-gradient(to bottom, transparent, #000 12px, #000 calc(100% - 12px), transparent); /* taste-skill-ignore: an edge fade on scrolling text */
            mask-image: linear-gradient(to bottom, transparent, #000 12px, #000 calc(100% - 12px), transparent); /* taste-skill-ignore */
}
.tts-text::placeholder { color: var(--ink-3); opacity: 1; }
.tts-text:focus-visible { outline: none; }
.tts:has(.tts-text:focus-visible) { border-color: var(--focus); box-shadow: 0 0 0 1px var(--focus); } /* taste-skill-ignore: the focus ring, drawn as a second pixel of edge */
@media (min-width: 640px) {
    .tts-text { font-size: var(--fs-field); line-height: 1.35; }
}

/* The count: hidden (its place kept) until the last 20% of the limit, then
   a ring that fills; the characters left join it for the last 50, in the
   warning colour, and in the error colour at the limit. */
.tts-count {
    position: absolute;
    right: 4px;
    bottom: 10px;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: var(--fs-small);
    color: var(--ink-3);
    font-variant-numeric: tabular-nums;
    opacity: 0;
    visibility: hidden;
    transition: opacity var(--dur-fade) ease-out, visibility 0s linear var(--dur-fade);
    pointer-events: none;
}
.tts-text:dir(rtl) ~ .tts-count { right: auto; left: 4px; } /* Hebrew: the line ends on the left */
.tts-count:not([data-level="far"]) { opacity: 1; visibility: visible; transition: opacity var(--dur-fade) ease-out; }
.tts-count svg { order: 2; transform: rotate(-90deg); }
.tts-ring-track, .tts-ring-fill { fill: none; stroke-width: 2.5; }
.tts-ring-track { stroke: var(--line); }
.tts-ring-fill { stroke: var(--primary); stroke-linecap: round; stroke-dasharray: 0 100; }
.tts-count:not([data-level="far"]) .tts-ring-fill { transition: stroke-dasharray 200ms ease-out, stroke 200ms ease-out; }
.tts-count[data-level="near"] { color: var(--warn); }
.tts-count[data-level="near"] .tts-ring-fill { stroke: var(--warn); }
.tts-count[data-level="full"] { color: var(--error); }
.tts-count[data-level="full"] .tts-ring-fill { stroke: var(--error); }

/* --------------------------------------------------------------------------
   Two rows under the text. The clip's row (.tts-slot) is one place of fixed
   height that shows a quiet empty player, the first download or the clip
   (they cross-fade in place). The bottom row holds the samples, always, and
   Speak, which keeps its place and its width whatever happens above it. On
   phones Speak runs full width under the samples.
   -------------------------------------------------------------------------- */
.tts-slot {
    position: relative;
    display: grid;
    height: 48px;
    min-width: 0;
    margin-top: var(--s-2);
}
.tts-slot > * {
    grid-area: 1 / 1;
    min-width: 0;
    opacity: 0;
    visibility: hidden;
    transition: opacity var(--dur-fade) ease-out, visibility 0s linear var(--dur-fade);
}
.tts-slot[data-show="idle"] > .tts-idle,
.tts-slot[data-show="loading"] > .tts-load,
.tts-slot[data-show="player"] > .tts-now {
    opacity: 1;
    visibility: visible;
    transition: opacity 200ms ease-out 60ms, visibility 0s;
}
.tts-bottom {
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    gap: var(--s-2);
    margin-top: var(--s-2);
}
.tts-bottom > .tts-presets { height: 48px; }

/* Before any clip: a flat line of the waveform's dots (2px on, 2px off, the
   bars' rhythm), where the clip will be. No label and no play button: Speak
   is the one action, and the one hint (where it runs, the download size) is
   the foot's. */
.tts-idle { display: flex; align-items: center; }
.tts-idle-line {
    flex: 1;
    height: 2px;
    background: repeating-linear-gradient(to right, color-mix(in srgb, var(--rule-strong) 42%, var(--field)) 0 2px, transparent 2px 4px); /* taste-skill-ignore: the waveform's bar rhythm, flat */
}

/* Samples: named chips, in the picked language. What does not fit scrolls
   sideways (phones); the chips never wrap into a second row. */
.tts-presets {
    display: flex;
    align-items: center;
    gap: 6px;
    padding: 0 2px;
    overflow-x: auto;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
}
.tts-presets::-webkit-scrollbar { display: none; }
/* Phones: the row runs to the card's edges (its padding moves inside the
   scroller, so the first chip still starts on the card's left edge), and the
   chips scroll under a fade there (js/home.js, [data-scroll-fade]). */
@media (max-width: 639px) {
    .tts-presets {
        margin-inline: calc(-1 * var(--tts-pad, 0px));
        padding-inline: var(--tts-pad, 2px);
        scroll-padding-inline: var(--tts-pad, 0px);
    }
}
.preset-item {
    position: relative;
    flex: none;
    height: 36px;
    padding: 0 14px;
    border: 1px solid color-mix(in srgb, var(--rule-strong) 28%, var(--line));
    border-radius: var(--r-btn);
    background: var(--field);
    font-size: var(--fs-ui);
    color: var(--ink-2);
    white-space: nowrap;
    transition: color var(--dur-fade) ease-out, background-color var(--dur-fade) ease-out, border-color var(--dur-fade) ease-out;
}
.preset-item::after { content: ""; position: absolute; inset: -4px 0; } /* 44px to the touch */
.preset-item[lang="he"], .tts-presets[lang="he"] .preset-item { font-size: 17px; }
/* The sample in the box: the blue's soft tint. Colour only, so the
   chip keeps its width. */
.preset-item.active { background: var(--accent-soft); border-color: transparent; color: var(--accent-soft-ink); }
.preset-item:active { transform: scale(.97); }
.preset-item:focus-visible { outline-offset: -2px; }

/* The first download: a slim bar and what it is, with the percentage. */
.tts-load { display: flex; align-items: center; gap: var(--s-3); }
.tts-bar {
    flex: none;
    position: relative;
    width: 96px;
    height: 4px;
    border-radius: 2px;
    background: var(--rule);
    overflow: hidden;
}
.tts-bar[hidden] { display: none; }
.tts-bar > span {
    display: block;
    height: 100%;
    width: 100%;
    border-radius: inherit;
    background: var(--accent);
    transform: scaleX(0);
    transform-origin: left center;
    transition: transform 450ms var(--ease-out), background-color 200ms ease-out;
}
.tts-load-text {
    display: flex;
    min-width: 0;
    font-size: var(--fs-small);
    color: var(--ink-2);
    white-space: nowrap;
}
.tts-load-words { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.tts-load-pct { flex: none; }
.tts-load-text .num { font-variant-numeric: tabular-nums; }
/* Done: the fill turns the ready colour as the row moves on. */
.tts-panel:has(.tts-foot[data-done="true"]) .tts-bar > span { background: var(--ok); }
/* Waiting for the connection: the fill holds still in the waiting colour. */
.tts-panel:has(.tts-foot[data-offline="true"]) .tts-bar > span { background: var(--warn); }
.tts-panel:has(.tts-foot[data-offline="true"]) .tts-load-text { color: var(--warn); }

/* Speak matches .btn .btn-primary (css/site.css) in every measure: 48px,
   a pill, 16px medium, a 20px icon 8px from the label. While a request
   runs the label stays and only the icon changes, so it never moves. */
.tts-speak {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--s-2);
    width: 100%;
    height: var(--btn-h);
    padding: 0 var(--s-5);
    border-radius: var(--r-btn);
    background: var(--primary);
    color: var(--accent-ink);
    font-size: var(--fs-control);
    font-weight: 500;
    line-height: 1.2;
    white-space: nowrap;
    transition: transform var(--dur-press) ease-out, background-color var(--dur-fade) ease-out;
}
.tts-speak svg { flex: none; }
.tts-speak:active:not(:disabled):not([aria-disabled="true"]) { transform: scale(.97); filter: brightness(.94); }
.tts-speak:focus-visible { outline-offset: 3px; }
/* Busy: full colour, only aria-disabled, so a keyboard user keeps focus. */
.tts-speak.loading, .tts-speak[aria-disabled="true"] { cursor: progress; }
.tts-speak.loading svg { display: none; }
/* Nothing to say yet. */
.tts-speak:disabled:not(.loading) { background: var(--rule); color: var(--ink-3); }

@media (min-width: 640px) {
    .tts-bottom { grid-template-columns: minmax(0, 1fr) auto; align-items: center; column-gap: var(--s-4); }
    .tts-speak { width: auto; min-width: 128px; }
}
@media (max-width: 639px) {
    .tts-bar { width: 40px; } /* the whole line fits beside it at 390 */
}

/* Play buttons: a 44px circle; --live fills it only while it plays. */
.tts-play {
    position: relative;
    flex: none;
    display: grid;
    place-items: center;
    width: 44px; height: 44px;
    border-radius: 50%;
    border: 1px solid var(--rule-strong);
    background: var(--field);
    color: var(--ink);
    transition: background-color var(--dur-fade) ease-out, border-color var(--dur-fade) ease-out, color var(--dur-fade) ease-out, transform var(--dur-press) ease-out, opacity var(--dur-fade) ease-out;
}
.tts-play:active:not(:disabled) { transform: scale(.96); }
.tts-play:disabled { opacity: .45; }
.tts-play svg { grid-area: 1 / 1; transition: opacity var(--dur-fade) ease-out, transform var(--dur-fade) ease-out; }
.tts-play .i-play { transform: translateX(1px); } /* optical centre of the triangle */
/* Play and pause cross-fade in place: opacity and scale, no blur. */
.tts-play .i-pause { opacity: 0; transform: scale(.6); }
.is-playing .tts-play {
    background: var(--live);
    border-color: var(--live);
    color: var(--live-ink);
}
.is-playing .tts-play .i-play { opacity: 0; transform: scale(.6); }
.is-playing .tts-play .i-pause { opacity: 1; transform: none; }

/* The scrub line (earlier clips, the settings): a 4px rule; the played part
   and the 12px knob are --live while playing and --ink-3 when paused. */
.tts-seek {
    --p: 0%;
    --c: var(--ink-3);
    -webkit-appearance: none;
    appearance: none;
    display: block;
    width: 100%;
    height: 24px;
    margin: 0;
    background: transparent;
    cursor: pointer;
    touch-action: pan-y;
}
.is-playing .tts-seek { --c: var(--live); }
.tts-seek::-webkit-slider-runnable-track {
    height: 4px;
    border-radius: 2px;
    background: linear-gradient(to right, var(--c) var(--p), var(--rule) var(--p)); /* taste-skill-ignore: a hard-stop fill, the played part */
}
.tts-seek::-moz-range-track { height: 4px; border-radius: 2px; background: var(--rule); }
.tts-seek::-moz-range-progress { height: 4px; border-radius: 2px; background: var(--c); }
.tts-seek::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 12px; height: 12px;
    margin-top: -4px;
    border-radius: 50%;
    background: var(--c);
}
.tts-seek::-moz-range-thumb { width: 12px; height: 12px; border: 0; border-radius: 50%; background: var(--c); }
.tts-seek:focus-visible { outline-offset: 0; border-radius: 4px; }
/* While a clip still streams it can't be scrubbed or downloaded yet. */
.tts-seek:disabled { cursor: default; }

/* The clip in the bottom row: play, its waveform, the time, download. */
.tts-now {
    display: grid;
    grid-template-columns: 44px minmax(0, 1fr) auto 44px;
    column-gap: var(--s-3);
    align-items: center;
    scroll-margin: var(--s-4);
    margin-inline-end: -10px; /* the download glyph on the card's edge, like Clear and settings above */
}
/* The waveform is a canvas (js/script.js drawWave) under a range input with
   no track and no knob of its own: the input does click, drag and the keys,
   the canvas shows where. Colours for the canvas come from here. */
.tts-wave {
    --wave-rest: color-mix(in srgb, var(--rule-strong) 42%, var(--field));
    --wave-played: var(--ink-2);
    --x: 0px;
    position: relative;
    height: 40px;
    min-width: 0;
}
.tts-wave-canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.tts-wave .tts-seek { position: absolute; inset: 0; height: 100%; }
.tts-wave .tts-seek::-webkit-slider-runnable-track { height: 100%; background: transparent; }
.tts-wave .tts-seek::-moz-range-track { height: 100%; background: transparent; }
.tts-wave .tts-seek::-moz-range-progress { background: transparent; }
.tts-wave .tts-seek::-webkit-slider-thumb { width: 2px; height: 100%; margin-top: 0; border-radius: 0; background: transparent; }
.tts-wave .tts-seek::-moz-range-thumb { width: 2px; height: 100%; border-radius: 0; background: transparent; }
.tts-wave .tts-seek:focus-visible { outline-offset: 2px; border-radius: 6px; }
/* Hover: a hairline and the time under the pointer. */
.tts-wave::after {
    content: "";
    position: absolute;
    top: 2px;
    bottom: 2px;
    left: 0;
    width: 1px;
    background: var(--ink);
    transform: translateX(var(--x));
    opacity: 0;
    pointer-events: none;
}
.tts-wave.is-hovering::after { opacity: .5; }
.tts-wave-tip {
    position: absolute;
    bottom: calc(100% + 4px);
    left: 0;
    padding: 2px 6px;
    border-radius: 6px;
    background: var(--ink);
    color: var(--field);
    font-size: 12px;
    line-height: 16px;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    transform: translateX(calc(var(--x) - 50%));
    pointer-events: none;
}
.tts-wave-tip[hidden] { display: none; }
.tts-now-end { display: inline-flex; align-items: center; gap: var(--s-2); }

/* The voice: the logo's cloud beside the time, always in its place. Its
   light rests a little small and soft; while the clip plays, script.js
   scales and brightens it with the loudness (transform and opacity on the
   light only). Reduced motion: no scaling, just a brighter light while it plays. */
.tts-voice { flex: none; display: block; width: 30px; height: 23px; overflow: visible; } /* the logo cloud at a size that reads beside 14px figures */
.tts-voice-body { fill: var(--logo-mark); }
.tts-voice-shade { fill: var(--logo-shade); }
.tts-voice-light {
    transform-box: fill-box;
    transform-origin: center;
    transform: scale(.82);
    opacity: .75;
}
@media (prefers-reduced-motion: reduce) {
    .tts-now.is-playing .tts-voice-light { opacity: 1; }
}
.tts-now-time { font-size: var(--fs-small); color: var(--ink-2); font-variant-numeric: tabular-nums; white-space: nowrap; }
.tts-now.is-pending .tts-now-time { color: var(--ink-3); }
@media (max-width: 379px) {
    .tts-now { column-gap: var(--s-2); }
    .tts-now .tts-voice { display: none; }
}

/* The measured line, in the foot under the clip. */
.tts-made {
    flex: 1 1 auto;
    margin: 0;
    min-width: 0;
    font-size: var(--fs-small);
    line-height: 1.45;
    color: var(--ink-3);
    text-wrap: pretty;
}
.tts-made[hidden] { display: none; }
/* While it streams the line is only "first audio in 0.21 s": it starts a
   sentence, so its first letter is a capital. */
.tts-made::first-letter { text-transform: uppercase; }
.tts-made b { color: var(--ink); font-weight: 500; font-variant-numeric: tabular-nums; }
/* " on WebGPU" is in the text (and the list's copies of the line) but not on
   screen: the backend tag at the end of the same line says where. */
.tts-now-on { display: none; }
@media (max-width: 639px) {
    .tts-now-first { display: none; } /* one line beside the backend tag */
}

/* The level meter: four 3px bars in Speak, standing in for the play icon
   while a request runs and moving only while the clip is being made. Only
   scaleY animates; reduced motion keeps them still. */
.tts-eq {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 2px;
    height: 14px;
    color: var(--live);
    opacity: 0;
    transition: opacity var(--dur-fade) ease-out;
}
.tts-eq i {
    display: block;
    width: 3px;
    height: 100%;
    border-radius: 2px;
    background: currentColor;
    transform: scaleY(.3);
}
.tts-speak .tts-eq { display: none; width: 20px; height: 16px; color: inherit; opacity: 1; }
.tts-speak.loading .tts-eq { display: inline-flex; }
@media (prefers-reduced-motion: no-preference) {
    [data-phase="generating"] .tts-speak.loading .tts-eq i {
        animation: eq-level 720ms cubic-bezier(.45, 0, .55, 1) infinite alternate;
    }
    .tts-eq i:nth-child(2) { animation-duration: 560ms; animation-delay: -300ms; }
    .tts-eq i:nth-child(3) { animation-duration: 840ms; animation-delay: -150ms; }
    .tts-eq i:nth-child(4) { animation-duration: 620ms; animation-delay: -420ms; }
}
@keyframes eq-level {
    from { transform: scaleY(.3); }
    to { transform: scaleY(1); }
}

/* --------------------------------------------------------------------------
   Model state line, download line, backend
   -------------------------------------------------------------------------- */
/* The foot holds its height through every state, so the card never moves:
   one line (two on phones, where the note before the first download wraps). */
.tts-foot {
    position: relative;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    align-content: flex-start;
    gap: 8px 12px;
    margin-top: 18px;
    min-height: 24px;
}
@media (max-width: 639px) {
    .tts-foot { min-height: 48px; align-content: center; } /* two lines for the first-visit note; one line sits in their middle */
}
/* While the bar shows (js/script.js "Progress") the bottom row says what is
   happening, with the percentage; the state line keeps saying it to screen
   readers (it is the live region) but steps out of sight, and the foot
   shows the figures: bytes, time left, and that the model is kept. With a
   clip in the player the measured line takes the state line's place. */
.tts-foot[data-loading="true"] .tts-note,
.tts-foot[data-made="true"][data-state="ready"]:not([data-busy]) .tts-note {
    position: absolute;
    width: 1px; height: 1px; /* taste-skill-ignore: visually hidden, not a rule */
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
}
.tts-foot[data-busy="true"] .tts-made { display: none; }
.tts-note {
    flex: 1 1 auto;
    margin: 0;
    min-width: 0;
    font-size: var(--fs-small);
    line-height: 1.45;
    color: var(--ink-3);
    text-wrap: balance; /* the first-visit note breaks into two even lines on a phone */
}
/* Bytes, at the end of the state line (the line grows to push them there;
   on a narrow screen they wrap under it, on the left). */
.tts-meta {
    margin: 0;
    font-size: var(--fs-small);
    line-height: 1.45;
    color: var(--ink-3);
    white-space: nowrap;
}
.tts-meta[hidden] { display: none; }
.tts-foot[data-offline="true"] .tts-meta { display: none; }
/* The second row while a download runs: that it is kept, and the time left. */
/* On the same line as the bytes, so the foot keeps one line's height while
   it downloads (phones leave out "Saved for next time": the row above says
   "once"). */
.tts-hint {
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    gap: 0 12px;
    margin: 0;
    white-space: nowrap;
    overflow: hidden;
    font-size: var(--fs-small);
    line-height: 1.45;
    color: var(--ink-3);
}
.tts-hint[hidden], .tts-hint > [hidden] { display: none; }
/* "Saved for next time" stays in the markup for the record, out of sight:
   the row above already says "(262 MB, once)". */
#downloadSaved { display: none !important; }
/* The foot is always one line tall (more only for a note of its own). */
.tts-foot > .tts-meta { line-height: 24px; }
.tts-foot > .tts-note { line-height: 24px; }
.tts-foot > .tts-made { line-height: 24px; }
.tts-eta { white-space: nowrap; }
.tts-foot[data-offline="true"] .tts-eta { display: none; }
/* While loading, the figures take the right of the line: no tag or shortcut. */
.tts-foot[data-busy="true"] .tts-backend,
.tts-foot[data-busy="true"] .tts-kbd { display: none; }
/* Ready: a small check in the ready colour, then the line steps back. */
.tts-foot[data-state="ready"]:not([data-busy]) .tts-note { color: var(--ok); transition: color 600ms ease-out; }
.tts-foot[data-state="ready"]:not([data-busy]) .tts-note::before {
    content: "";
    display: inline-block;
    width: 14px;
    height: 14px;
    margin-inline-end: 6px;
    vertical-align: -2px;
    background: currentColor;
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3 8.5l3.2 3L13 4.5' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
            mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3 8.5l3.2 3L13 4.5' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
}
.tts-foot[data-state="ready"]:not([data-busy])[data-settled="true"] .tts-note { color: var(--ink-3); }
/* Before the first download: what the visitor gets (it runs here, the
   download happens once), in the body colour with a small device mark. */
.tts-foot[data-state="missing"] .tts-note { color: var(--ink-2); }
.tts-foot[data-state="missing"] .tts-note::before {
    content: "";
    display: inline-block;
    width: 16px;
    height: 16px;
    margin-inline-end: 8px;
    vertical-align: -3px;
    background: var(--accent);
    -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 3.5h9v7h-9zM1.5 12.75h13' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
            mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.5 3.5h9v7h-9zM1.5 12.75h13' fill='none' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat;
}
.tts-foot[data-state="ready"]:not([data-busy]):has(.tts-backend[data-backend="wasm"]) .tts-note { color: var(--warn); }
.tts-foot[data-state="ready"]:not([data-busy]):has(.tts-backend[data-backend="wasm"]) .tts-note::before { display: none; }
.tts-foot[data-state="error"] .tts-note { color: var(--error); }
/* While something loads the line is the main thing in the foot. */
.tts-foot[data-busy="true"] .tts-note { color: var(--ink-2); font-weight: 500; }
/* Offline mid-download: waiting for the connection, not failed. */
.tts-foot[data-offline="true"] .tts-note { color: var(--warn); }
.tts-backend {
    font-size: var(--fs-small);
    line-height: 24px;
    padding: 0 10px;
    border-radius: var(--r-btn);
    background: var(--paper-2);
    color: var(--ink-2);
    white-space: nowrap;
}
/* Before the engine has said what it runs on, the tag would only repeat the
   note beside it ("Runs on your device"): it shows once the backend is known
   (WebGPU, or the CPU). */
.tts-backend[data-backend="pending"] { display: none; }
.tts-kbd { display: none; }
/* Try again is a .btn (site.css); .btn's display would beat [hidden]. */
.tts-retry[hidden] { display: none; }
/* A failed load: the message and Try again only. */
.tts-foot[data-state="error"] .tts-backend,
.tts-foot[data-state="error"] .tts-kbd { display: none; }
/* The model runs from memory (no room to store it): its own line, under the rest. */
.tts-keep {
    flex-basis: 100%;
    margin: 0;
    font-size: var(--fs-small);
    line-height: 1.45;
    color: var(--ink-2);
    text-wrap: pretty;
}
.tts-keep[hidden] { display: none; }

/* Errors and warnings ----------------------------------------------------- */
.tts-message {
    display: none;
    margin: 8px 0 0;
    font-size: var(--fs-ui);
    line-height: 1.45;
    color: var(--ink-2);
    text-wrap: pretty;
}
.tts-message.is-visible { display: block; }
.tts-message.error { color: var(--error); }
.tts-message.warning { color: var(--warn); }

/* --------------------------------------------------------------------------
   Earlier clips (from the second clip on): rows divided by rules
   -------------------------------------------------------------------------- */
.tts-clips:empty { display: none; }
.tts-clips { margin-top: 32px; }
.tts-clips-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
.tts-clips-title { margin: 0; font-size: var(--fs-ui); line-height: var(--lh-small); letter-spacing: normal; font-weight: 500; color: var(--ink-2); }
.tts-clear {
    min-height: 44px;
    padding: 0 4px;
    font-size: var(--fs-ui);
    color: var(--ink-2);
    text-decoration: underline;
    text-decoration-color: var(--rule-strong);
    text-underline-offset: 4px;
    transition: color var(--dur-fade) ease-out, text-decoration-color var(--dur-fade) ease-out;
}
.tts-list {
    list-style: none;
    margin: 4px 0 0;
    padding: 0;
    border-top: 1px solid var(--rule-strong); /* taste-skill-ignore: a track list, rows read like a table */
}
@keyframes tts-arrive { from { opacity: 0; transform: translateY(6px); } }
.tts-clip {
    animation: tts-arrive 240ms var(--ease-out-quint) both;
    display: grid;
    grid-template-columns: 44px minmax(0, 1fr) 44px;
    column-gap: 14px;
    align-items: start;
    padding: 14px 0 12px;
    border-bottom: 1px solid var(--rule); /* taste-skill-ignore: a track list */
}
.tts-clip-body { min-width: 0; }
.tts-clip-head { margin: 0; min-height: 22px; }
.tts-clip-label { font-weight: 500; font-size: var(--fs-control); }
.tts-clip-text {
    margin: 2px 0 0;
    font-size: var(--fs-ui);
    line-height: 1.4;
    color: var(--ink-2);
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
.tts-clip-track { display: flex; align-items: center; gap: 12px; }
.tts-clip-track .tts-seek { flex: 1; min-width: 0; }
.tts-clip-time { flex: none; font-size: var(--fs-small); color: var(--ink-2); font-variant-numeric: tabular-nums; }
.tts-clip-made { margin: 0; font-size: var(--fs-small); color: var(--ink-3); }
.tts-clip-numbers { font-variant-numeric: tabular-nums; }
.tts-clip-on { white-space: nowrap; }
.tts-clip-download {
    display: grid;
    place-items: center;
    width: 44px; height: 44px;
    border-radius: var(--r-toggle);
    color: var(--ink-2);
    transition: color var(--dur-fade) ease-out, background-color var(--dur-fade) ease-out;
}
.tts-clip-download:disabled { color: var(--rule-strong); }

/* --------------------------------------------------------------------------
   Phones: a bar fixed to the bottom once the demo has scrolled away
   -------------------------------------------------------------------------- */
.tts-sticky {
    position: fixed;
    left: 0; right: 0; bottom: 0;
    z-index: 40;
    display: grid;
    grid-template-columns: 44px minmax(0, 1fr) auto;
    align-items: center;
    gap: 12px;
    padding: 6px max(16px, env(safe-area-inset-right)) calc(6px + env(safe-area-inset-bottom)) max(16px, env(safe-area-inset-left));
    background: var(--field);
    border-top: 1px solid var(--rule-strong); /* taste-skill-ignore: the floating phone bar's edge */
    /* It floats over the page; a faint edge says so. */
    box-shadow: /* taste-skill-ignore: a real overlay */ 0 -8px 24px -18px color-mix(in srgb, var(--ink) 40%, transparent);
    font-family: var(--font);
    color: var(--ink);
    transform: translateY(100%);
    visibility: hidden;
    /* It leaves faster than it comes, accelerating away. */
    transition: transform 180ms var(--ease-in), visibility 0s linear 180ms;
}
.tts-sticky[data-visible="true"] {
    transform: none;
    visibility: visible;
    transition: transform 240ms var(--ease-out), visibility 0s;
}
.tts-sticky-meta { min-width: 0; display: flex; flex-direction: column; line-height: 1.25; }
.tts-sticky-label { font-weight: 500; font-size: var(--fs-ui); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.tts-sticky-time { font-size: var(--fs-small); color: var(--ink-2); font-variant-numeric: tabular-nums; }
/* Speak again is the header's small button (.btn .btn-primary .btn-sm in
   script.js): 40px, a 44px hit area. */
.tts-sticky-speak:disabled { background: var(--rule); color: var(--ink-3); }
@media (min-width: 900px), (max-height: 359px) {
    .tts-sticky { display: none; }
}

/* --------------------------------------------------------------------------
   Hover is an enhancement only
   -------------------------------------------------------------------------- */
@media (hover: hover) and (pointer: fine) {
    .preset-item:not(.active):hover { color: var(--ink); border-color: color-mix(in srgb, var(--rule-strong) 55%, var(--line)); }
    .tts-icon-btn:hover { color: var(--ink); background: color-mix(in srgb, var(--paper-2) 70%, transparent); }
    .tts-pill:hover { border-color: var(--rule-strong); }
    .tts-clip-download:not(:disabled):hover { color: var(--ink); background: color-mix(in srgb, var(--accent-soft) 60%, transparent); }
    .tts-icon-btn[aria-expanded="true"]:hover { color: var(--ink); background: var(--paper-2); }
    .tts-speak:hover:not(:disabled):not([aria-disabled="true"]) { background: var(--primary-hover); }
    .tts-play:hover:not(:disabled) { border-color: var(--ink-2); }
    .tts-clear:hover { color: var(--ink); text-decoration-color: currentColor; }
}

/* The shortcut hint (⌘ Enter) stays hidden at every width (.tts-kbd above):
   Speak's tooltip names it. The shortcut works everywhere. */

@media (min-width: 900px) {
    .tts-clip-text { -webkit-line-clamp: 1; }
}

/* --------------------------------------------------------------------------
   Reduced motion: no press scale, no pop, instant swaps
   -------------------------------------------------------------------------- */
@media (prefers-reduced-motion: reduce) {
    .tts-speak, .tts-play, .tts-play svg, .tts-sticky, .tts-sticky[data-visible="true"], .tts-bar, .tts-bar > span, .tts-note,
    .tts-slot > *, .tts-count, .tts-ring-fill, .tts-chevron, .preset-item { transition: none !important; }
    .tts-menu:not([hidden]), .tts-advanced:not([hidden]), .tts-clip { animation: none; }
    .tts-speak:active:not(:disabled), .tts-play:active, .preset-item:active { transform: none; }
}
