/*
 *  editor.css
 *  Layout + typography for the transcript editor (SPEC §5 / §12).
 *  Created by Ian Dundas on 08/06/2026.
 */

:root {
    --editor-font-size: 14px;
    /* Editor spacing knobs:
       The app loads the copied CSS in Sources/MagicTranscript/Resources/editor.
       After changing this source file, run `npm run build` from js-src to sync it.
       - editor-inset-* controls the breathing room around the whole editor.
       - content-inset controls the gutter/text/timestamp column gaps.
       - paragraph-gap controls the space between speaker paragraphs. */

    --editor-inset-block: clamp(24px, 6vw, 60px);
    --editor-inset-inline: clamp(20px, 7vw, 80px);
    --editor-leading-inset: clamp(20px, 7vw, 80px);
    --content-inset: 12px;
    --paragraph-gap: 16px;
    /* Fallback only — editor.mjs updateGutterWidth() overrides this with a
       measured value (longest registry name, clamped) on setSpeakers and
       setFontSize, so the column hugs the names without truncating them. */
    --gutter-width: 9em;
    --unknown-color: #8a8a8e;
    --fg: #1d1d1f;
    --bg: #ffffff;
    --scrollbar-thumb: rgba(0, 0, 0, 0.30);
    --scrollbar-thumb-hover: rgba(0, 0, 0, 0.45);
    --favorited-bg: rgba(255, 214, 10, 0.10);
    --search-other: rgba(255, 214, 10, 0.35);
    --search-current: rgba(255, 159, 10, 0.85);
    --playing: rgba(48, 209, 88, 0.30);
    --playing-mix: 30%;
    color-scheme: light dark;
}

@media (prefers-color-scheme: dark) {
    :root {
        --fg: #f5f5f7;
        --bg: #000000;
        --unknown-color: #98989d;
        --scrollbar-thumb: rgba(255, 255, 255, 0.30);
        --scrollbar-thumb-hover: rgba(255, 255, 255, 0.45);
        --favorited-bg: rgba(255, 214, 10, 0.14);
        --search-other: rgba(255, 214, 10, 0.28);
        --search-current: rgba(255, 159, 10, 0.75);
        --playing: rgba(73, 162, 213, 0.4);
        --playing-mix: 38%;
    }
}

* { box-sizing: border-box; }

/* Root-document scrolling, like Safari: the page itself is the scroller, so
   macOS provides the native auto-hiding overlay scrollbar, positioned by the
   compositor's scrolling tree — it tracks the window edge during live resize. */
html, body {
    margin: 0;
    padding: 0;
    min-height: 100%;
    font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", sans-serif;
    background: var(--bg);
    color: var(--fg);
}

body {
    min-height: 100vh;
    overflow-x: hidden;
}

#editor {
    background: var(--bg);
    min-height: 100vh;
}

.ProseMirror {
    background: var(--bg);
    outline: none;
    padding-block: var(--editor-inset-block);
    padding-inline: var(--editor-inset-inline);
    /* Extra inset so the chips sit clear of the sidebar's shadow. */
    padding-inline-start: var(--editor-leading-inset);
}

.ProseMirror:focus { outline: none; }

/* MARK: paragraph row (one speaker-grouped run) */

.paragraph {
    display: flex;
    align-items: flex-start;
    --speaker-color: var(--unknown-color);
}

/* The inter-paragraph gap lives on the (narrow) content column, NOT as a margin
   on the full-width row. Rationale: a native selection spanning two rows makes
   WebKit paint a "gap filler" across the margin between block siblings at the
   block's full width — a band under the gutter/timestamp columns. Putting the
   gap on .paragraph-content keeps the rows flush and the spacing becomes an
   unpainted margin on the text column, so a multi-row selection reads as one
   continuous text highlight. */

.gutter {
    display: flex;
    flex-direction: column;
    align-items: flex-start;
    font-size: var(--editor-font-size);
    flex: 0 0 var(--gutter-width);
    width: var(--gutter-width);
    max-width: var(--gutter-width);
    padding-right: var(--content-inset);
    user-select: none;
    -webkit-user-select: none;
}

.paragraph-content {
    flex: 1 1 auto;
    min-width: 0;
    margin-bottom: var(--paragraph-gap);
    font-size: var(--editor-font-size);
    line-height: 1.55;
    /* Plain label colour, matching the other transcript panes; the speaker's
       colour lives on the gutter chip only. */
    color: var(--fg);
    white-space: pre-wrap;
    overflow-wrap: break-word;
}

.word { color: inherit; }

/* The segment boundary marker: a non-selectable structural atom that introduces
   each segment inside a paragraph. The caret steps across it like a hard break,
   never landing "on" it. */
.segment-break {
    user-select: none;
    -webkit-user-select: none;
}

/* Segments flow together with no visible demarcation, but stored line text is
   trimmed, so adjacent segments would otherwise butt together ("itjust"). The
   boundary renders as ONE plain space — presentation only, never part of the
   document, so it can't reach the wire or the caret. `transcriptClipboardText`
   mirrors it for plain-text copy. The paragraph's first break renders nothing
   (no leading space). */
.paragraph-content > .segment-break:not(:first-child)::after {
    content: " ";
    white-space: pre-wrap;
}

/* MARK: speaker chip (one full chip per paragraph) */

.speaker-chip {
    display: inline-flex;
    align-items: center;
    gap: 0.4em;
    max-width: 100%;
    margin: 0;
    padding: 0;
    border: none;
    border-radius: 999px;
    background: transparent;
    color: var(--speaker-color);
    font: inherit;
    font-size: var(--editor-font-size);
    font-weight: 500;
    line-height: 1.55;
    text-align: left;
    cursor: pointer;
    -webkit-user-select: none;
    user-select: none;
}

.speaker-chip:hover { background: transparent; }
.speaker-chip:active { background: transparent; }
.speaker-chip:focus-visible { outline: 2px solid var(--speaker-color); outline-offset: 1px; }

.chip-dot {
    display: none;
}

.chip-label {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* MARK: continuation paragraphs (same speaker as the previous paragraph — a run
   split by a deliberate Enter-break). Repeating the full chip per paragraph
   reads as noise, so a continuation's chip is hidden until the pointer is over
   its paragraph — the run reads as one attribution, but each paragraph stays
   individually reassignable. Opacity, not display, so the gutter keeps its
   layout. The `continuation` class is maintained by refreshParagraphLeads
   (leads.mjs), never on the typing path. */
.paragraph.continuation .speaker-chip {
    /* No transition here: hiding must SNAP. A split reuses the already-painted
       paragraph DOM, so a transition on the hidden state animates the freshly
       demoted chip from visible to hidden — a distracting flash. Only the hover
       reveal below animates. */
    opacity: 0;
    pointer-events: none;
}

.paragraph.continuation:hover .speaker-chip,
.paragraph.continuation .speaker-chip:focus-visible {
    opacity: 1;
    pointer-events: auto;
    transition: opacity 120ms ease;
}

/* MARK: favourite indicator — the same faint yellow the segments view uses for
   favourited rows, but range-accurate: the favourites plugin (favorites.mjs)
   decorates the inline content of each contiguous favourited-segment run, so
   half a favourited paragraph washes only its favourited words (favourite stays
   per-segment data; the selection carve-out splits segments at range edges).
   No border-radius: the class lands on each inline span, and per-span rounding
   would scallop the wash at every word boundary. */
.favorited-range {
    background: var(--favorited-bg);
}

/* The CSS boundary space between two favourited segments is generated on the
   break's ::after, which doesn't inherit the span's background — wash it
   explicitly so a favourited run reads as one continuous highlight. */
.segment-break.favorited-range::after {
    background: var(--favorited-bg);
}

/* "Favorites Only" (host uiflag): collapse non-favourited paragraphs so the
   editor mirrors the favorites filter the segments/transcript views apply.
   Paragraph-granular: a paragraph stays visible when ANY of its segments is
   favourited (the `.favorited` class ParagraphView maintains). */
body.uiflag-favoritesOnly .paragraph:not(.favorited) { display: none; }

/* MARK: right-hand timestamp gutter (paragraph start, on row hover) */

.paragraph-time {
    flex: 0 0 5em;
    font-size: var(--editor-font-size);
    padding-left: var(--content-inset);
    color: var(--unknown-color);
    text-align: right;
    line-height: 1.55;
    white-space: nowrap;
    opacity: 0;
    transition: opacity 120ms ease;
    user-select: none;
    -webkit-user-select: none;
    pointer-events: none;
}

/* "Show End Timestamp": the end stacks below the start as a dimmer second line.
   Empty (feature off) collapses so the row stays single-line. */
.paragraph-time-end { opacity: 0.65; }
.paragraph-time-end:empty { display: none; }

/* Times reveal on row hover by default; "Show Timestamps" makes them persistent
   (both rules resolve to the same visible opacity, so hover never fights it). */
.paragraph:hover .paragraph-time { opacity: 0.5; }
body.uiflag-showTimestamps .paragraph-time { opacity: 0.5; }

/* MARK: host-driven highlights (SPEC §7.2) */

.search-match { background: var(--search-other); border-radius: 3px; }
.search-match.current { background: var(--search-current); }
.playing-word { background: var(--playing); border-radius: 3px; }
/* Whole-segment playing highlight (wordless segments, SPEC §14d): a segment is
   sentence-scale, so the wash is halved to read as a region, not a word. */
.playing-segment { background: color-mix(in srgb, var(--playing) 50%, transparent); border-radius: 3px; }

/* Experiment (host uiflag): the playing word glows in its speaker's colour
   (the row's --speaker-color; unknown rows fall back to the neutral grey). */
body.uiflag-speakerPlayingColor .playing-word {
    background: color-mix(in srgb, var(--speaker-color) var(--playing-mix), transparent);
}
body.uiflag-speakerPlayingColor .playing-segment {
    background: color-mix(in srgb, var(--speaker-color) calc(var(--playing-mix) / 2), transparent);
}

body.read-only .speaker-chip { cursor: default; }
/* Read-only means transcription is still in progress: the timestamp column is
   collapsed outright (not just faded, MW-3215) so the streaming text gets the
   full width; per-paragraph times become meaningful once the completion
   rewrite settles the final segmentation (MW-3272). */
body.read-only .paragraph-time { display: none; }

/* Still transcribing AND no speakers registered yet: every chip would read
   "Unknown", so the speaker column hides until the speaker-identification
   phase (or a manual assignment) produces real speakers (MW-3272). The stream
   itself still reads as prose: the pipeline writes pause-based
   `startsNewParagraph` flags on speakerless lines, so the normal paragraph
   grouping applies during streaming too. */
body.read-only.no-speakers .gutter { display: none; }

/* Load reveal: a document loaded mid-transcription fades in as ONE block
   (editor.mjs adds `load-reveal-hidden` before first paint, then
   `load-reveal-go` at the paint that drops the host's placeholder; the
   transition itself arrives inline, so the duration has one owner). The
   word-cascade below is for streamed APPENDS only. */
#editor.load-reveal-hidden { opacity: 0; }
#editor.load-reveal-hidden.load-reveal-go { opacity: 1; }

/* Streamed-append reveal (reveal.mjs): appended text starts invisible and
   fades in, one unit at a time — the delay and duration arrive inline on each
   decoration. Opacity-only, no movement, so an arbitrarily fast stream can't
   induce motion sickness. */
.reveal-in {
    opacity: 0;
    animation-name: reveal-fade;
    animation-timing-function: ease-out;
    animation-fill-mode: forwards;
}
@keyframes reveal-fade {
    to { opacity: 1; }
}

/* Experimental variant (Editor Experiments → "Unveil with blur"): the text
   additionally resolves from a slight blur as it lands. Positionally static,
   so still motion-safe. */
body.uiflag-revealBlur .reveal-in {
    animation-name: reveal-fade-blur;
}
@keyframes reveal-fade-blur {
    from { opacity: 0; filter: blur(4px); }
    to { opacity: 1; filter: blur(0); }
}

/* Reduce Motion: JS skips the decorations and the load fade entirely; this
   backstop guarantees text can never be stranded invisible even if one slips
   through. */
@media (prefers-reduced-motion: reduce) {
    .reveal-in {
        animation: none;
        opacity: 1;
    }
    #editor.load-reveal-hidden { opacity: 1; }
}
