/*
 * app.css — hand-written structural rules only, layered on top of Pico
 * CSS's classless build (pico.classless.min.css). Phases 0-3 (see
 * /home/noah/.claude/plans/buzzing-conjuring-muffin.md) built the
 * original sage-green/Pico-card visual design page by page. That design
 * read as "randomly thrown together" (no wordmark, generic Pico
 * defaults) — the retro terminal/CRT redesign below
 * (/home/noah/.claude/plans/warm-purring-kettle.md) replaces it, again
 * page by page, on top of the same Pico classless base.
 */

/*
 * JetBrains Mono, vendored (internal/web/doc.go has the full
 * version/source record) — this app is monospace-only from here on, no
 * serif/sans anywhere, so both weights are declared once at the top
 * rather than per-component.
 */
@font-face {
  font-family: "JetBrains Mono";
  src: url("/static/fonts/jetbrains-mono-regular.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

@font-face {
  font-family: "JetBrains Mono";
  src: url("/static/fonts/jetbrains-mono-bold.woff2") format("woff2");
  font-weight: 700;
  font-style: normal;
  font-display: swap;
}

/*
 * Color palette: a retro terminal/CRT concept — a dark "Soft Mint"
 * phosphor screen and a paired, equally deliberate "Light" paper
 * terminal (not a generic light-mode fallback), auto-selected via
 * prefers-color-scheme like the rest of this app already does (no
 * theme-toggle UI). Two more saturated variants (neon-green "Phosphor",
 * warm "Amber") were prototyped and rejected as "too nerdy"/too
 * saturated — Soft Mint is deliberately desaturated. Selectors mirror
 * Pico's own internal light/dark selectors
 * (":root:not([data-theme=dark])", the dark media query, and
 * "[data-theme=dark]") for the same reason the previous palette used
 * them: same cascade specificity as Pico's own tokens, automatic
 * OS-preference following, "[data-theme=dark]" kept only for parity in
 * case a toggle is ever added.
 *
 * --phosphor-dim/--glow/--glow-soft/--scanline-opacity/--crt-red have no
 * Pico equivalent — they're this redesign's own tokens, consumed by the
 * CRT chrome primitives below and by each page's own rules.
 */
:root:not([data-theme=dark]) {
  --pico-font-family: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  --pico-background-color: #eee6d6;
  --pico-card-background-color: #f8f4ea;
  --pico-primary: #2f5644;
  --pico-primary-background: #2f5644;
  --pico-primary-underline: rgba(47, 86, 68, 0.5);
  --pico-primary-hover: #1c3327;
  --pico-primary-hover-background: #1c3327;
  --pico-primary-focus: rgba(47, 86, 68, 0.35);
  --pico-primary-inverse: #f8f4ea;
  --phosphor-dim: #9c9478;
  --phosphor-bright: #1c3327;
  --glow: transparent;
  --glow-soft: transparent;
  --scanline-opacity: 0;
  --crt-red: #a5453a;
}

@media only screen and (prefers-color-scheme: dark) {
  :root:not([data-theme=light]) {
    --pico-background-color: #101613;
    --pico-card-background-color: #151d19;
    --pico-primary: #7fbfa0;
    --pico-primary-background: #2f5644;
    --pico-primary-underline: rgba(127, 191, 160, 0.5);
    --pico-primary-hover: #b9e0c9;
    --pico-primary-hover-background: #3f6b56;
    --pico-primary-focus: rgba(127, 191, 160, 0.375);
    --pico-primary-inverse: #0a0e0c;
    --phosphor-dim: #47695a;
    --phosphor-bright: #b9e0c9;
    --glow: rgba(127, 191, 160, 0.35);
    --glow-soft: rgba(127, 191, 160, 0.1);
    --scanline-opacity: 0.1;
    --crt-red: #c17a68;
  }
}

[data-theme=dark] {
  --pico-background-color: #101613;
  --pico-card-background-color: #151d19;
  --pico-primary: #7fbfa0;
  --pico-primary-background: #2f5644;
  --pico-primary-underline: rgba(127, 191, 160, 0.5);
  --pico-primary-hover: #b9e0c9;
  --pico-primary-hover-background: #3f6b56;
  --pico-primary-focus: rgba(127, 191, 160, 0.375);
  --pico-primary-inverse: #0a0e0c;
  --phosphor-dim: #47695a;
  --phosphor-bright: #b9e0c9;
  --glow: rgba(127, 191, 160, 0.35);
  --glow-soft: rgba(127, 191, 160, 0.1);
  --scanline-opacity: 0.1;
  --crt-red: #c17a68;
}

/*
 * Shared CRT/terminal chrome primitives — reused across every page's own
 * per-component rules (login/chat/versions/settings/media), rather than
 * each page reinventing its own box/border/glow treatment.
 */

/* The base "screen" panel: any card-like container (auth card, chat
 * embed panel, a future settings/media panel) can opt into this instead
 * of Pico's default <article> shadow. Deliberately no glow/scanline in
 * the Light theme (--glow-soft/--scanline-opacity are both zeroed
 * there) — nothing glows in daylight. */
.crt-panel {
  position: relative;
  overflow: hidden;
  background: var(--pico-card-background-color);
  border: var(--pico-border-width) solid var(--phosphor-dim);
  border-radius: 4px;
  box-shadow:
    0 0 40px var(--glow-soft),
    inset 0 0 60px var(--glow-soft);
}

/* The scanline overlay itself, as its own element
 * (.crt-panel > .crt-scanlines) rather than a ::before pseudo-element —
 * kept as a plain sibling div so it never has to compete for a single
 * ::before slot with a component that already uses one. Purely
 * decorative (pointer-events: none) and invisible under the Light theme
 * (--scanline-opacity: 0 there). */
.crt-scanlines {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: repeating-linear-gradient(
    to bottom,
    rgba(0, 0, 0, var(--scanline-opacity)) 0px,
    rgba(0, 0, 0, var(--scanline-opacity)) 1px,
    transparent 1px,
    transparent 3px
  );
}

/* A thin dashed hairline divider — the terminal-concept's replacement
 * for Pico's default <hr>/border treatment wherever a page needs to
 * separate two blocks of terminal-styled content without a full boxed
 * panel. */
.crt-hairline {
  border: none;
  border-top: 1px dashed var(--phosphor-dim);
  margin: var(--pico-spacing) 0;
}

/* A small "// section" label — this redesign's replacement for a plain
 * <h1> on pages whose masthead (app_header) already carries the
 * wordmark, so a second, page-repeating heading right below it would be
 * redundant. The real <h1> stays in the DOM as .sr-only (screen readers
 * still get "Settings"/"Media" announced), this is the visible stand-in. */
.crt-section-label {
  color: var(--phosphor-dim);
  font-size: 0.8rem;
  margin-bottom: var(--pico-spacing);
}

/* Bracket-style "[ LABEL ]" button treatment — applied to real
 * <button>/[role=button] elements via ::before/::after content rather
 * than requiring literal "[ "/" ]" text in every template, so the
 * bracket glyphs stay purely decorational (not read twice by a screen
 * reader, which only announces the button's actual accessible name). */
.crt-bracket-button,
/* Pico classless's own button[type=submit]{width:100%} rule has
   (0,1,1) specificity (element+attribute) — a plain ".crt-bracket-button"
   class alone is only (0,1,0) and loses that fight, so every bracket
   button (Save, Upload, ...) silently stretched to its container's full
   width outside login.html (where .auth-submit's own explicit
   width:100% happened to want that anyway, masking the bug there).
   Repeating the selector as "button.crt-bracket-button" matches Pico's
   own specificity; app.css loads after pico.classless.min.css, so the
   later, equal-specificity rule wins. */
button.crt-bracket-button {
  display: inline-flex;
  width: auto;
  align-items: center;
  gap: 0.35em;
  background: transparent;
  border: var(--pico-border-width) solid var(--pico-primary);
  color: var(--pico-primary);
  border-radius: 2px;
  padding: 0.5em 0.9em;
  font-family: inherit;
  font-size: 0.95rem;
  cursor: pointer;
}

.crt-bracket-button::before { content: "[ "; }
.crt-bracket-button::after { content: " ]"; }

.crt-bracket-button:is(:hover, :focus) {
  background: var(--glow-soft);
  color: var(--phosphor-bright);
  border-color: var(--phosphor-bright);
}

/* The blinking-cursor utility — a solid block that blinks via
 * animation, used wherever a terminal-style "ready" state or an active
 * input wants the classic cursor affordance. prefers-reduced-motion
 * turns the blink off (still visible, just static) rather than removing
 * the element outright. */
.crt-cursor {
  display: inline-block;
  width: 0.55em;
  height: 1em;
  background: var(--phosphor-bright);
  vertical-align: text-bottom;
  animation: crt-cursor-blink 1s steps(1) infinite;
}

@keyframes crt-cursor-blink {
  50% { opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  .crt-cursor { animation: none; }
}

/* login.html's error message (e.g. "Invalid customer ID or password.") —
 * a real warning treatment: tinted background, border, and padding so it
 * reads as a distinct alert, not just red text floating in the page.
 * Built from Pico's own "--pico-del-color" token (the color Pico itself
 * already uses for negative/deleted content) via color-mix() rather than
 * a hardcoded hex, so it automatically stays correct in both light and
 * dark mode instead of needing its own light/dark overrides. */
.error {
  color: var(--pico-del-color);
  background-color: color-mix(in srgb, var(--pico-del-color) 12%, transparent);
  border: var(--pico-border-width) solid color-mix(in srgb, var(--pico-del-color) 45%, transparent);
  border-radius: var(--pico-border-radius);
  padding: var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal);
  margin-bottom: var(--pico-spacing);
}

/* login.html: vertically center the card, not just horizontally (Pico's
 * classless <main> only centers horizontally via margin auto) — makes
 * the page read as a dedicated sign-in screen rather than content
 * dropped at the top of an otherwise-empty page. */
body.auth-page {
  display: flex;
  min-height: 100vh;
  align-items: center;
}

/* Constrains the login card to a comfortable reading width instead of
 * Pico's default <main> width (meant for regular page content, too wide
 * for a short sign-in form) — a narrow, centered card is the standard,
 * recognizable shape for this kind of screen. */
.auth-container {
  width: 100%;
  max-width: 24rem;
}

/* login.html's card: login_test.go's TestHandleLoginSubmit_FailureRendersStyledErrorAndAuthLayout
 * asserts a literal bare "<article>" (no attributes) is present, so the
 * real CRT-panel styling lives one level in, on a wrapper <div> — this
 * strips Pico's own card background/border/shadow/padding from that
 * outer <article> so the page doesn't render two nested boxes. */
.auth-container > article {
  background: none;
  border: none;
  box-shadow: none;
  padding: 0;
}

/* The terminal redesign's .crt-panel/.crt-scanlines primitives (defined
 * above) plus the padding a bare <div> would otherwise lack —
 * auth-crt-panel only adds what .crt-panel doesn't already cover
 * (spacing around the wordmark/tagline block). */
.auth-crt-panel {
  padding: 1.75rem 1.5rem;
}

/* The wordmark: this app's one recurring visual anchor, absent from the
 * UI entirely before this redesign (the old header only ever showed the
 * customer ID — see .app-header-wordmark below for the same treatment
 * reused on every authenticated page). The dot uses --pico-primary
 * rather than the wordmark's own --phosphor-bright so it reads as a
 * small brand accent, not just more heading text. */
.auth-wordmark {
  font-size: 1.5rem;
  font-weight: 700;
  color: var(--phosphor-bright);
  margin-bottom: 0.75rem;
}

.auth-wordmark-dot {
  color: var(--pico-primary);
}

/* The ascii box under the wordmark: plain <pre>, not an image/svg — it's
 * literal monospace box-drawing text, so a <pre> is the correct semantic
 * element and needs no special layout beyond removing the input/form
 * spacing that would otherwise sit above it. */
.auth-tagline {
  color: var(--phosphor-dim);
  font-size: 0.85rem;
  line-height: 1.3;
  margin: 0 0 1.5rem;
}

/* Terminal-style form inputs: transparent background, a single
 * underline rather than Pico's default full bordered box — reads as
 * "typing at a prompt" rather than "filling in a form field", matching
 * every other input across the redesigned pages. */
.crt-input {
  background: transparent;
  border: none;
  border-bottom: var(--pico-border-width) solid var(--phosphor-dim);
  border-radius: 0;
  color: var(--phosphor-bright);
  padding-left: 0;
  padding-right: 0;
}

.crt-input:focus {
  border-bottom-color: var(--pico-primary);
  box-shadow: none;
}

/* input[type=file]'s button part is the one form control whose visual
 * treatment browsers actually expose a hook for (::file-selector-button)
 * — everything else about a file input's native chooser UI is outside
 * CSS's reach. Applied globally rather than scoped to media.html: any
 * future file input in this app should look the same way. Without this,
 * Chromium's own dark-mode default file-picker button (a generic
 * slate-blue) stood out against this redesign's palette the same way
 * the unstyled .button-secondary did before that fix. */
input[type="file"]::file-selector-button {
  background: transparent;
  border: var(--pico-border-width) solid var(--phosphor-dim);
  border-radius: 2px;
  color: var(--phosphor-dim);
  padding: 0.35em 0.7em;
  font-family: inherit;
  font-size: 0.9em;
  cursor: pointer;
}

input[type="file"]::file-selector-button:hover {
  border-color: var(--phosphor-bright);
  color: var(--phosphor-bright);
  background: var(--glow-soft);
}

/* The login submit button: full-width like Pico's own default button
 * (crt-bracket-button doesn't set a width on its own, since most of its
 * other uses are inline-sized), reusing the same bracket-button
 * treatment every other primary action in this redesign uses. */
/* Needs the same "button.X" specificity bump as .crt-bracket-button
   above (0,1,1), otherwise this width:100% — which login.html's
   full-width submit button actually wants — would lose the specificity
   tie against button.crt-bracket-button's own width:auto regardless of
   which rule comes later in the file. */
button.auth-submit {
  width: 100%;
  justify-content: center;
  margin-top: 0.5rem;
}

/* media.html's per-image "Replace" action: de-emphasized relative to the
 * page's primary "Upload" action. Pico's own ".secondary" color tokens
 * are a generic blue-gray untouched by this redesign's palette overrides
 * (only --pico-primary/background/card were repointed to the terminal
 * tokens) — using them verbatim left this button visibly off-palette, a
 * small version of the exact "random/unpolished" problem the whole
 * redesign exists to fix. A dim, transparent "ghost" treatment (the same
 * language .crt-bracket-button's own hover state and the login/chat
 * "discard" buttons already use) instead. */
.button-secondary {
  --pico-background-color: transparent;
  --pico-border-color: var(--phosphor-dim);
  --pico-color: var(--phosphor-dim);
}

.button-secondary:is(:hover, :active, :focus) {
  --pico-background-color: var(--glow-soft);
  --pico-border-color: var(--phosphor-bright);
  --pico-color: var(--phosphor-bright);
}

/* chat.html's rollback/status banner (e.g. "Rolling back to that version
 * failed."). Same alert-with-tint construction as .error above, but built
 * from Pico's "--pico-mark-*" tokens (the color Pico itself uses to
 * highlight text, e.g. <mark>) instead of "--pico-del-color" — a rollback
 * notice isn't a form-validation error, so it deliberately reads as a
 * distinct "pay attention" color rather than reusing .error's red
 * verbatim, while still following the same color-mix()-against-a-Pico-
 * token approach so it automatically stays correct in light and dark mode
 * without its own light/dark overrides. */
.chat-notice {
  color: var(--pico-mark-color);
  background-color: color-mix(in srgb, var(--pico-mark-background-color) 55%, transparent);
  border: var(--pico-border-width) solid color-mix(in srgb, var(--pico-mark-background-color) 90%, transparent);
  border-radius: var(--pico-border-radius);
  padding: var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal);
  margin-bottom: var(--pico-spacing);
}

/* chat.html's message log: the terminal redesign trades chat-bubble
 * treatment for prefixed log lines ("you> ...", "agent> ...") — this
 * app's terminal-log vocabulary applied to the one place it matters
 * most. The prefixes are ::before content (below), not literal text in
 * chat.html/chat.go, so nothing server-side changes and a screen reader
 * doesn't announce "you greater than" before every message. */
.chat-exchange {
  margin-bottom: var(--pico-spacing);
}

.chat-customer-message,
.chat-agent-reply {
  margin: 0 0 0.4rem 0;
}

/* The customer's own message: the brand accent on the prefix only (not
 * a filled bubble background) — reads as "this line is yours" without
 * needing a right-aligned/colored block. */
.chat-customer-message {
  color: var(--phosphor-bright);
}

.chat-customer-message::before {
  content: "you> ";
  color: var(--pico-primary);
  font-weight: 700;
}

/* The agent's reply: a dashed left border (this redesign's recurring
 * "quoted/system output" treatment) instead of a bordered bubble —
 * .chat-agent-reply wraps arbitrary rendered Markdown (headings,
 * paragraphs, lists), so the border reads correctly regardless of how
 * many blocks the reply contains. */
.chat-agent-reply {
  padding-left: 0.9rem;
  border-left: var(--pico-border-width) dashed var(--phosphor-dim);
}

.chat-agent-reply::before {
  content: "agent> ";
  color: var(--phosphor-dim);
  font-weight: 700;
}

/* set_plan's checklist: a third "prefixed log line" family alongside
 * .chat-customer-message ("you>")/.chat-agent-reply ("agent>") above —
 * "plan>" announces steps rather than a message, so it gets the same
 * dashed-border quoted-output treatment .chat-agent-reply already uses
 * rather than a new visual concept. One rule set serves two call sites:
 * the persisted per-exchange rendering (chat_exchange, chat.go/
 * chat_session.go's Plan field) and the live, turn-in-progress indicator
 * (#chat-plan-indicator, chat.html's own readChatStream) — the live one is
 * nothing more than the same steps mid-update, so it deliberately reuses
 * this class rather than growing its own. */
.chat-plan {
  padding-left: 0.9rem;
  margin: 0 0 0.4rem 0;
  border-left: var(--pico-border-width) dashed var(--phosphor-dim);
}

.chat-plan::before {
  content: "plan> ";
  color: var(--phosphor-dim);
  font-weight: 700;
}

.chat-plan-steps {
  margin: 0.3rem 0 0 0;
  padding-left: 0;
}

/* list-style: none must live on the <li> itself, not the <ul> above — Pico
 * classless's own base stylesheet sets "li{list-style:square}" directly on
 * the element, which (an explicit rule always beats an inherited value,
 * regardless of the inherited declaration's own specificity) would
 * otherwise win over .chat-plan-steps' inherited list-style. Mirrors
 * .version-entry's identical fix for the same Pico behavior above. */
.chat-plan-step {
  list-style: none;
  margin-bottom: 0.2rem;
}

/* Each step's "[ ]"/"[~]"/"[x]" status marker lives entirely in this
 * ::before content, keyed off the chat-plan-step-{status} class chat.html's
 * Go template and JS both attach (see the file-level comment above) —
 * neither rendering path has to spell out the marker text itself, only the
 * status word. */
.chat-plan-step::before {
  font-weight: 700;
  margin-right: 0.4rem;
}

.chat-plan-step-pending {
  color: var(--phosphor-dim);
}

.chat-plan-step-pending::before {
  content: "[ ]";
}

.chat-plan-step-in_progress {
  color: var(--pico-primary);
}

.chat-plan-step-in_progress::before {
  content: "[~]";
}

/* A finished step fades back to the same dim tone as a pending one, rather
 * than staying bright — "done" is the settled state; "in_progress" above is
 * the one that should actually draw the eye while a turn is still running. */
.chat-plan-step-done {
  color: var(--phosphor-dim);
  text-decoration: line-through;
}

.chat-plan-step-done::before {
  content: "[x]";
}

/* The "in progress" status line shown while a turn is running (e.g.
 * "Reading data.md"): a fourth "prefixed log line" family alongside
 * .chat-customer-message ("you>")/.chat-agent-reply ("agent>")/.chat-plan
 * ("plan>") above. It used to just borrow .chat-agent-reply wholesale plus
 * three bouncing dots tacked on in front — visually indistinguishable from
 * a real reply starting, and the dots/text read as two unrelated elements
 * rather than one coherent line. "work>" instead makes clear this is a
 * transient status, not the reply that follows it, and the marker reuses
 * .chat-plan-step-in_progress's own "[~]" bracket idiom (animated here,
 * since — unlike a plan step — there's no "[ ]"/"[x]" state to distinguish
 * it from; the pulse itself carries that "still working" meaning). */
.chat-status {
  padding-left: 0.9rem;
  margin: 0 0 0.4rem 0;
  border-left: var(--pico-border-width) dashed var(--phosphor-dim);
}

.chat-status::before {
  content: "work> ";
  color: var(--phosphor-dim);
  font-weight: 700;
}

.chat-status-marker::before {
  content: "[~]";
  display: inline-block;
  color: var(--pico-primary);
  font-weight: 700;
  margin-right: 0.4rem;
  animation: chat-status-pulse 1.2s infinite ease-in-out;
}

@media (prefers-reduced-motion: reduce) {
  .chat-status-marker::before { animation: none; }
}

@keyframes chat-status-pulse {
  0%, 100% { opacity: 0.4; }
  50% { opacity: 1; }
}

.chat-status-text {
  color: var(--pico-primary);
}

/* Elapsed seconds, e.g. "· 3s" — dim like a plan step's settled state
 * (not the active accent color above) since it's secondary information,
 * and the "· " separator lives here rather than in the JS-set text so
 * readChatStream never has to know about this element's own formatting. */
.chat-status-elapsed {
  color: var(--phosphor-dim);
}

.chat-status-elapsed:not(:empty)::before {
  content: "· ";
}

/* versions.html's commit history list: a colophon-style list — each
 * entry a hairline-divided row, not a boxed/shadowed Pico <article>
 * card (this redesign's terminal-log vocabulary applied to version
 * history, matching chat.html's own move away from bubbles/cards). The
 * <article>'s Pico card background/border/shadow/padding is stripped
 * the same way .auth-container > article strips it above, keeping the
 * real <article>/<ul>/<li> markup (version-related tests assert on
 * these classes, and a screen reader still announces "list, N items"
 * either way). */
.versions-panel-list {
  padding-left: 0;
  margin: 0;
}

.version-entry {
  list-style: none;
}

.version-entry article {
  background: none;
  border: none;
  box-shadow: none;
  border-radius: 0;
  padding: 0.9rem 0;
  border-bottom: var(--pico-border-width) dashed var(--phosphor-dim);
}

.version-entry:last-child article {
  border-bottom: none;
}

/* Date vs. message hierarchy: the message is what a customer actually
 * scans the history for ("which one had the broken hours page?"), so
 * it stays fully legible; the date is real but secondary, de-emphasized
 * via --phosphor-dim and a smaller size. (.version-name's own styling
 * lives further down, in the versioning-feature section that introduced
 * it.) */
.version-message {
  margin-bottom: 0.25rem;
}

.version-date {
  margin-bottom: 0;
  color: var(--phosphor-dim);
  font-size: 0.875em;
}

/* The versions panel's empty state (a customer who opens the panel before
 * ever saving a change — a real, reachable state, not just a defensive
 * placeholder). Plain muted text is enough: there's nothing actionable to do
 * here yet besides making a change in chat, so this isn't a warning/notice
 * like .error or .chat-notice above, just a quiet explanation of why the list
 * is blank. */
.version-empty-state {
  color: var(--phosphor-dim);
}

/*
 * Phase 4 ("real chat window, direct-to-chat login, self-service API
 * key") — see /home/noah/.claude/plans/shimmering-cooking-babbage.md.
 * Everything below is new; nothing above this point was restructured.
 */

/* The standard visually-hidden-but-accessible utility class: content stays
 * in the accessibility tree (screen readers announce it) but is removed
 * from visual flow entirely, unlike opacity/visibility tricks that would
 * still reserve layout space. Used by chat.html's message <label> (kept in
 * the DOM for label association, hidden visually since the input's
 * placeholder already communicates its purpose) and by the typing/publish
 * indicators' "Sending…"/"Publishing…" text (the visible treatment is the
 * dot animation below; screen readers still get the same announcement
 * they always did). Standard, widely-used implementation (e.g. Bootstrap's
 * ".visually-hidden") — no reason to invent a variant of it. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

/* Pico classless's own `input:not([type=checkbox],[type=radio]){width:
 * 100%}` rule has (0,1,1) specificity — element + the :not() argument's
 * own attribute selector — which beats plain `.sr-only` alone ((0,1,0)),
 * so media.html's hidden file input silently stretched back out to the
 * full viewport width (invisible, since it has no visible box, but real
 * horizontal overflow on every page/breakpoint). Same Pico-classless
 * specificity clash already fixed once for .crt-bracket-button — same
 * fix shape: bump the selector to match Pico's specificity and let
 * source order (app.css loads after Pico's stylesheet) decide the tie. */
input.sr-only {
  width: 1px;
}

/* app_header (layout.html): the shared chrome for every authenticated page
 * (chat/versions/settings) — a slim single-row bar, not a second block of
 * full-width buttons competing with each page's own primary action. Small,
 * muted nav text plus a de-emphasized "Log out" (.link-button below) is
 * what actually fixes the "Send and Log out look like two competing
 * primary actions" problem this phase was built to address.
 *
 * Horizontal padding is set explicitly here (not left to Pico) because
 * Pico's classless build zeroes out body>header/main's own left/right
 * padding below its ~510px breakpoint (relying on child elements, e.g.
 * <article> cards, to provide their own inset) — versions.html/
 * settings.html/media.html's plain <body> (.app-page below) has no
 * padding of its own, so without this the header's own text sat flush
 * against the screen edge on a real 375px device (caught via a live
 * Playwright screenshot, not just reading the CSS). A flat
 * var(--pico-spacing) on every viewport width, rather than trying to
 * preserve Pico's own wider desktop scale, is simplest since this is the
 * only source of the header's inset on every authenticated page — see
 * .chat-page below, which deliberately keeps its own padding vertical-only
 * so it never stacks with this one (chat.html's <body> used to also carry
 * its own horizontal padding, which stacked with this rule and roughly
 * doubled the header's inset there versus .app-page pages; caught via a
 * live getBoundingClientRect measurement, not just reading the CSS).
 *
 * flex-wrap: wrap — without it, the wordmark + customer ID + the nav
 * (Chat/Versions/Media/Settings/Log out) never break onto a second line,
 * so at a narrow viewport (e.g. 375px) the row simply overflows past the
 * screen edge instead, cutting off "Log out" entirely (caught via
 * responsive-check's scrollWidth/clientWidth measurement, not just a
 * screenshot). gap already supplies the vertical spacing between the two
 * wrapped rows, same as it does horizontally on one row. */
.app-header {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--pico-spacing);
  padding: calc(var(--pico-spacing) * 0.5) var(--pico-spacing);
  margin-bottom: var(--pico-spacing);
  border-bottom: var(--pico-border-width) solid var(--phosphor-dim);
  font-size: 0.9rem;
}

/* The wordmark reused on every authenticated page's masthead — same
 * treatment as .auth-wordmark (login.html) but sized to sit inline in a
 * slim single-row bar rather than as a standalone heading. */
.app-header-wordmark {
  font-weight: 700;
  color: var(--phosphor-bright);
}

.app-header-customer {
  color: var(--phosphor-dim);
}

/* Pushed to the far right via margin-left: auto (rather than
 * justify-content: space-between on .app-header, which would spread all
 * three top-level children apart instead of grouping wordmark+customer
 * on the left) — a pipe-separated masthead line, matching this
 * redesign's terminal-log vocabulary rather than a row of separate
 * nav-bar links. Every item after the first (including the logout
 * form) gets a dim "|" via ::before rather than a literal pipe
 * character in the template.
 *
 * flex-wrap: wrap — .app-header's own wrap (above) only breaks between
 * the masthead and this nav; the nav's five items (four links + the
 * logout form) are wide enough on their own to overflow a narrow
 * viewport's width a second time. Without this, flexbox's default
 * nowrap falls back to shrinking items instead, and singles out the
 * logout form to absorb the shrinkage (it's the only item whose text
 * contains a space to wrap at) — splitting "Log out" into "Log"/"out" on
 * two lines and pushing it past the screen edge, rather than the nav
 * itself wrapping cleanly. */
.app-header-nav {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 0.5em;
  margin-left: auto;
}

.app-header-nav > :not(:first-child)::before {
  content: "\00b7";
  color: var(--phosphor-dim);
  margin-right: 0.5em;
}

/* Same "quiet until interacted with" treatment as .link-button below, so
 * the four nav links match the logout button — without it they fall back
 * to the browser's default underline, which the "·" separator (generated
 * on the <a> itself via ::before above) then inherits too. */
.app-header-nav > a {
  color: var(--phosphor-dim);
  text-decoration: none;
}

.app-header-nav > a:is(:hover, :focus) {
  color: var(--phosphor-bright);
  text-decoration: underline;
}

/* The logout form is just one more item in the nav's flex row, not its
 * own block-level form with default margins. A <form> is block-level by
 * default, though — left alone, its box didn't line up with the plain
 * <a> nav links' inline text baseline, most visibly in the "·" separator
 * generated right before it (sat noticeably higher than the other
 * separators). inline-flex + align-items:center makes it behave like
 * the anchors for alignment purposes. */
.app-header-logout-form {
  margin: 0;
  display: inline-flex;
  align-items: center;
}

/* Makes a real <button type="submit"> read as an inline text link — this
 * is what keeps "Log out" a genuine CSRF-protected POST while no longer
 * looking like a second full-width primary button under Send/Confirm.
 * Inherits the surrounding nav's font size and uses Pico's own muted-text
 * token rather than a hardcoded color, with the primary accent only on
 * hover/focus (the same "quiet until interacted with" treatment a plain
 * text link gets). */
.link-button {
  display: inline;
  width: auto;
  padding: 0;
  margin: 0;
  border: none;
  background: none;
  font-size: inherit;
  font-family: inherit;
  color: var(--phosphor-dim);
  text-decoration: none;
  cursor: pointer;
  /* "Log out" is the only .app-header-nav item whose text contains a
     space, so it's the only one flexbox can wrap internally when its
     container runs short on width — without this, a narrow viewport
     could still split it into "Log"/"out" on two lines instead of
     wrapping as one unit (see .app-header-nav's own flex-wrap comment). */
  white-space: nowrap;
}

.link-button:is(:hover, :focus) {
  color: var(--phosphor-bright);
  text-decoration: underline;
}

/* chat.html's fixed-viewport chat window: <body> becomes a 100vh flex
 * column so the header stays put, #chat-scroll (below) fills the space
 * between it and the input bar, and the input bar itself stays pinned at
 * the bottom — the actual "app-like fixed chat window" this phase was
 * built to deliver, replacing the old ever-growing single page.
 *
 * Padding is vertical-only (see the 0 below) — deliberately, not an
 * oversight. .app-header above is body's first child on every
 * authenticated page, and already carries its own horizontal padding for
 * .app-page pages (versions/settings/media, whose <body> has none of its
 * own). A horizontal padding here would stack with that, roughly doubling
 * the header's inset on chat.html specifically — confirmed via a live
 * getBoundingClientRect measurement (e.g. 230px of left inset on
 * chat.html at a 1280px viewport versus 210px on versions.html, the exact
 * width of one var(--pico-spacing)), not just by reading the CSS.
 * Cancelling that stacking back out with a negative margin on the header
 * (the more obvious fix) turns out *not* to reproduce .app-page's inset
 * exactly at every viewport: Pico's classless build separately gives
 * every body>header its own max-width + margin:auto centering once the
 * viewport is wide enough (e.g. 700px around a 768px viewport) — .app-page
 * headers get that centering "for free" since nothing overrides their
 * margin, but a fixed negative-margin override on chat.html's header
 * would replace it with a flat offset instead, un-centering the header
 * there relative to .app-page at exactly that breakpoint (also only
 * caught live, not from reading the CSS). Keeping this padding vertical
 * only, and instead giving #chat-scroll/.chat-input-bar their own
 * horizontal padding below, means the header never receives any of this
 * padding in the first place — no stacking, no override needed, and
 * .app-header behaves identically on every authenticated page at every
 * viewport width purely because it always sees the same unpadded
 * containing block. */
.chat-page {
  display: flex;
  flex-direction: column;
  height: 100vh;
  /* Bounds the whole chat window to a readable, deliberate width and
     centers it — without this, a wide desktop viewport stretched the
     message log/input bar edge to edge, reading as an unstyled/broken
     page rather than a composed terminal window (the same fix
     .app-page below applies to versions/settings/media). */
  max-width: 900px;
  margin: 0 auto;
  padding: var(--pico-spacing) 0;
  box-sizing: border-box;
}

/* The horizontal inset .chat-page's own padding above deliberately no
 * longer provides (see its comment) — given directly to every child
 * except the header instead, so #version-status/#chat-scroll/
 * .chat-input-bar still get the exact same inset they always have, while
 * .app-header (whose horizontal inset comes entirely from its own padding
 * above) never sees it stack a second time. `:not(.app-header)` rather
 * than enumerating #version-status/#chat-scroll/.chat-input-bar by name
 * so a future addition to chat.html's body doesn't silently lose its
 * inset by being left out of an explicit list. The preview canvas and its
 * handle are excluded: they're fixed-position layers that must span their own
 * full width, and this chat-column inset otherwise pushed the canvas's
 * head/body — and so the head↔body divider — ~20px in from both edges,
 * leaving a visible gap toward the chat and toward the screen edge. */
.chat-page > :not(.app-header):not(.chat-canvas):not(.chat-canvas-handle) {
  padding-left: var(--pico-spacing);
  padding-right: var(--pico-spacing);
}

/* .chat-page's own padding above is still vertical (top/bottom), so the
 * header — its first child — still receives an extra top inset from it
 * that .app-page pages' header never gets (their <body> has no padding at
 * all). Unlike the horizontal case above, there's no Pico auto-margin
 * behavior riding on margin-top to worry about (Pico's own body>header
 * rule only centers horizontally), so cancelling it here with a plain
 * negative margin is safe and exact at every viewport — confirmed via a
 * live measurement, not just reading the CSS. */
.chat-page > .app-header {
  margin-top: calc(var(--pico-spacing) * -1);
}

/* versions.html/settings.html/media.html share this — chat.html gets the
   same treatment directly on .chat-page above, since it's already the
   <body> class there. Centers the whole page (masthead included, since
   app_header is body's direct child) into a bounded "terminal window"
   instead of letting content hug the left edge of a wide viewport with
   a huge dead void beside/beneath it. */
.app-page {
  max-width: 900px;
  margin: 0 auto;
}

/* The one region that actually scrolls: holds #chat-log plus the typing
 * indicator (a sibling, not a child — see chat.html's comment on why).
 * min-height: 0 is load-bearing on a flex child that must be allowed to
 * shrink below its content size in order to scroll internally instead of
 * pushing the input bar off-screen. */
.chat-scroll {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
}

/* The message input bar: a single flex row instead of the old stacked
 * label/input/button block, so it reads as a chat input bar (text field
 * grows to fill the available width, Send never shrinks) rather than a
 * plain form field. Pinned to the bottom by virtue of being a flex sibling
 * after .chat-scroll/#chat-preview inside .chat-page's column. */
.chat-input-bar {
  display: flex;
  align-items: center;
  gap: calc(var(--pico-spacing) * 0.5);
  margin-top: var(--pico-spacing);
}

.chat-input-bar input[type="text"] {
  flex: 1;
  margin-bottom: 0;
}

/* The chat input is the one .crt-input that sits on screen at all times as
 * the primary way to act on the page — unlike the login/settings forms
 * .crt-input's underline treatment was designed for (see that rule's own
 * comment), a bare underline here read as too faint to register as "the
 * field to type in" (customer feedback). A full bordered box plus
 * --glow-soft's tint gives it its own clear affordance without touching
 * .crt-input's shared underline style everywhere else it's used. */
.chat-input-bar .crt-input {
  border: var(--pico-border-width) solid var(--phosphor-dim);
  border-radius: var(--pico-border-radius);
  background: var(--glow-soft);
  padding: var(--pico-form-element-spacing-vertical) var(--pico-form-element-spacing-horizontal);
}

.chat-input-bar .crt-input:focus {
  border-color: var(--pico-primary);
  box-shadow: 0 0 0 3px var(--glow-soft);
}

/* .chat-send-button replaces the old text "Send" button with a compact
 * icon button (an inline SVG, currentColor-stroked, so it inherits the
 * button's own text color for free in both themes — no separate icon
 * asset/font, matching this app's no-new-client-dependency posture). Round
 * rather than Pico's default rectangular button shape, since a lone icon
 * reads more naturally as a circular affordance than a pill with no label
 * text inside it. Selector is scoped under .chat-input-bar (like the
 * plain-button rule above) rather than bare .chat-send-button — needed so
 * its width/height actually beat .chat-input-bar button's own width:auto,
 * which otherwise wins on specificity (class+element beats a lone class)
 * and left this stretched almost full-width instead of a circle. */
.chat-input-bar .chat-send-button {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;
  height: 2.75rem;
  padding: 0;
  flex-shrink: 0;
  margin-bottom: 0;
  border-radius: 50%;
  color: var(--pico-primary-inverse);
  background-color: var(--pico-primary-background);
  border-color: var(--pico-primary-background);
}

.chat-input-bar .chat-send-button:is(:hover, :focus) {
  background-color: var(--pico-primary-hover-background);
  border-color: var(--pico-primary-hover-background);
}

/* settings.html's current-key display, styled like a config line
 * ("current_key = ..." — set directly in the template) — reuses the
 * same dim secondary-text token as .version-date/.chat-page-embed-titlebar
 * above rather than introducing a new visual concept for a short
 * informational line. */
.settings-current-key {
  color: var(--phosphor-dim);
}

/* The two credential sections (password, API key) shared by the standalone
 * settings page and the in-chat settings panel — one fragment, so one set of
 * rules (docs/features/FEATURE_SETTINGS.md). The dashed rule between them is
 * the same separator idiom .versions-panel-publishbar uses. */
.settings-body { display: flex; flex-direction: column; gap: 1.4rem; }
.settings-section + .settings-section {
  padding-top: 1.2rem;
  border-top: var(--pico-border-width) dashed var(--phosphor-dim);
}
.settings-heading {
  margin: 0 0 0.3rem;
  font-family: var(--pico-font-family);
  font-size: 0.95rem;
  color: var(--pico-primary);
}
.settings-hint {
  margin: 0 0 0.8rem;
  font-size: 0.78rem;
  color: var(--phosphor-dim);
}
/* Both forms' inputs sit in plain <p> wrappers, so Pico's own bottom margin on
 * the input duplicates the wrapper's — with four stacked credential fields that
 * doubled spacing is what pushes the panel into needless scrolling. Keep the
 * wrapper's spacing, drop the input's. */
.settings-section form p { margin-bottom: 0.7rem; }
.settings-section form p .crt-input { margin-bottom: 0.3rem; }

/* The header's settings control: an icon, not a nav destination — it opens the
 * in-chat panel (chat.html intercepts the click) and only falls back to
 * navigating to the standalone page without JS. Sized and coloured to sit in the
 * same quiet register as the other .app-header-nav items. */
.app-header-iconlink {
  display: inline-flex;
  align-items: center;
  color: var(--phosphor-dim);
  text-decoration: none;
}
.app-header-iconlink:is(:hover, :focus) { color: var(--phosphor-bright); }

/*
 * Phase 5 (show_page agent tool) — see
 * /home/noah/.claude/plans/shimmering-cooking-babbage.md. chat_exchange's
 * embedded-page panel: an <article> already gets Pico's card treatment
 * (tinted background, border, shadow) for free from the classless build —
 * this only tightens the spacing around the iframe it wraps, since the
 * default <article> padding plus the iframe's own block-level default
 * margin would otherwise leave an uneven gap only along the bottom edge.
 * The terminal redesign (/home/noah/.claude/plans/warm-purring-kettle.md)
 * adds a nested mini-terminal title bar inside this panel — see
 * .chat-page-embed-titlebar below.
 */
.chat-page-embed-panel {
  padding: calc(var(--pico-spacing) * 0.5);
}

/* The nested mini-terminal title bar: shows ShownPageVersion · ShownPagePath
 * (chat_session.go) — the tag this preview maps to, plus the plain page
 * path, never ShownPageURL's token-bearing embed URL — as its own small
 * "screen within the screen" header, echoing the outer chrome's own
 * title-bar language (.crt-panel's callers) rather than inventing a new
 * decoration. One shared color for both parts (no separate span/class):
 * they're only ever displayed together, so there's no independent
 * de-emphasis case the way .version-date has against .version-message. */
.chat-page-embed-titlebar {
  padding: 0.3rem 0.6rem;
  margin-bottom: calc(var(--pico-spacing) * 0.5);
  border-bottom: var(--pico-border-width) dashed var(--phosphor-dim);
  color: var(--phosphor-dim);
  font-size: 0.8rem;
}

/* The iframe itself: a fixed height (rather than trying to size to the
 * framed page's own content, which a sandboxed cross-origin-equivalent
 * document can't report back to the parent anyway — see the sandbox
 * attribute's doc comment in chat.html) keeps it from ever collapsing to
 * zero height, while staying short enough that it doesn't dominate the
 * whole chat log. border reuses the same --phosphor-dim token the
 * title bar's own border already uses, so the two read as one
 * continuous panel rather than two separately-styled boxes. */
.chat-page-embed {
  display: block;
  width: 100%;
  height: 450px;
  border: var(--pico-border-width) solid var(--phosphor-dim);
  border-radius: var(--pico-border-radius);
}

/*
 * Versioning feature, slice 6 (docs/features/FEATURE_VERSIONING.md's "Decisions from
 * slice 6 planning") — the chat page's "N unpublished changes since
 * vX.Y.Z" indicator and the versions page's tag-based changelog styling.
 * Everything below is new; nothing above this point was restructured.
 */

/* chat.html's #version-status: a calm, ambient status line below the
 * input bar, not an alert — same small-muted-text treatment as
 * .crt-section-label/.chat-agent-reply's own secondary text (--phosphor-dim
 * + a reduced font-size), not a boxed/bordered callout. "N unpublished
 * changes" is routine, expected standing state most logins will show at
 * least briefly, so it reads as ambient status, not a warning. */
.chat-publish-indicator {
  color: var(--phosphor-dim);
  font-size: 0.8rem;
  margin-top: calc(var(--pico-spacing) * 0.5);
}

/* versions.html's per-entry version name (e.g. "v1.2.0") — the changelog's
 * primary heading, full-contrast against the colophon row's own dim
 * secondary text (.version-date) — the live/draft distinction itself is
 * still carried entirely by the badge beside it, not by this text's own
 * color. */
.version-name {
  font-weight: 700;
  color: var(--phosphor-bright);
}

/* The live/draft status badge beside each version name: a small, pill-
 * shaped, uppercase label — deliberately compact (0.7em, tight padding) so
 * it reads as metadata attached to the version name, not a second
 * same-weight heading competing with it. */
.version-status {
  display: inline-block;
  margin-left: 0.4em;
  padding: 0.05em 0.5em;
  border-radius: var(--pico-border-radius);
  font-size: 0.7em;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  vertical-align: middle;
}

/* "live" reuses the same brand-accent tokens as .chat-publish-indicator
 * above (this app's one recurring "this is the real, current thing" color)
 * — the single tag matching git.LatestPublishedVersion, so there is always
 * at most one of these per changelog. */
.version-status-live {
  color: var(--pico-primary);
  background-color: color-mix(in srgb, var(--pico-primary-background) 15%, transparent);
}

/* "draft" (every alpha, and every superseded published tag) uses the same
 * muted token .version-date already relies on — draft entries are real
 * history, not a warning, so this deliberately stays as quiet as the date
 * text rather than borrowing an alert color. */
.version-status-draft {
  color: var(--phosphor-dim);
  background-color: color-mix(in srgb, var(--phosphor-dim) 15%, transparent);
}

/*
 * Image upload feature (docs/features/FEATURE_IMAGE_UPLOAD.md), slice 4 — media.html's
 * uploaded-image gallery. Originally a text list (mirroring versions.html's
 * article-per-entry convention), then briefly a plain ls-la-style text
 * table during the terminal/CRT redesign — the user tried that and
 * rejected it twice as unintuitive. Images are visual content; a text
 * listing fights that. This is a real thumbnail grid instead (each tile's
 * <img src> is handleMediaThumbnail's route, media.go), keeping the same
 * CRT chrome/color tokens as every other page — the one page that
 * deliberately breaks from the terminal-log/text pattern the other four
 * use for their primary content.
 *
 * Revised: the grid used to show a caption strip (label/filename/date
 * + a Replace disclosure) under every single tile, which the user found
 * busy for what's supposed to be a fast visual scan ("which photos do I
 * have"). All of that detail now lives behind a click, in a per-tile
 * native <dialog> lightbox (media.html) — the grid itself is thumbnails
 * only.
 *
 * Revised again: a uniform grid of same-size cells forced every
 * thumbnail into a fixed 4:3 box via object-fit: cover, silently
 * cropping anything that isn't already roughly 4:3 landscape — a
 * portrait photo (a team photo, a product shot held upright) lost its
 * top and bottom in the thumbnail. Switched to a CSS multi-column
 * masonry layout instead: no fixed cell height, so every thumbnail
 * keeps its own natural aspect ratio and nothing gets cropped. Pure CSS
 * (`column-width`), no JS reflow logic needed.
 */
.media-grid {
  display: block;
  column-width: 220px;
  column-gap: var(--pico-spacing);
}

.media-tile {
  display: inline-block;
  width: 100%;
  margin: 0 0 var(--pico-spacing);
  /* Without this, a column break can land in the middle of a tile,
     splitting one photo's border/image across two columns. */
  break-inside: avoid;
  border: var(--pico-border-width) solid var(--phosphor-dim);
  border-radius: 4px;
  overflow: hidden;
  background: var(--pico-card-background-color);
}

/* The thumbnail is itself the dialog trigger — an unstyled <button>
 * wrapping the <img> (native keyboard/screen-reader affordance for "click
 * to see details", rather than a bare <img onclick>) with all Pico button
 * chrome stripped since visually it should read as "just the photo". Pico
 * gives every [type=button] a bottom margin by default (meant for stacked
 * form buttons); left un-reset here it showed up as a stray gap of card
 * background between the image and the tile's bottom border. */
.media-thumb-button {
  display: block;
  width: 100%;
  margin: 0;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
}

/* The actual uploaded image — full width of its column, height auto so
 * it renders at its own natural aspect ratio instead of being cropped
 * or forced into a fixed box (see .media-grid's comment above). */
.media-thumb {
  display: block;
  width: 100%;
  height: auto;
}

.media-thumb-button:is(:hover, :focus-visible) .media-thumb {
  box-shadow: 0 0 0 3px var(--glow-soft);
}

/* The lightbox: a native <dialog>, so showModal()/close() give the
 * backdrop, focus trap, and Esc-to-close for free with no JS beyond
 * opening/closing it (see media.html's own small script). Deliberately
 * follows Pico's own expected "dialog > article" structure rather than
 * styling the <dialog> element directly: Pico's base stylesheet already
 * makes the bare `dialog` a fullscreen, centering flex container with its
 * own dimmed overlay background (`--pico-modal-overlay-background-color`,
 * already theme-aware) — its `min-width:100%`/`min-height:100%` always
 * wins over a `max-width`/`max-height` put on that same element (min
 * beats max regardless of selector specificity), which is what a first
 * attempt at styling `.media-dialog` directly ran into: the dialog
 * rendered as a full-viewport box instead of a small centered card. The
 * actual visible, sized card is the nested <article>. */
.media-dialog-card {
  position: relative;
  /* fit-content, not a fixed width: the card used to always be a
     landscape-shaped box (bounded only by max-width/max-height below),
     so a portrait photo rendered small inside it with a large empty
     margin on either side. Letting the card wrap to whatever size
     .media-dialog-image actually renders at means a portrait photo gets
     a narrow, tall card and a landscape photo keeps a wide one — no
     forced shape either way. */
  width: fit-content;
  max-width: min(90vw, 640px);
  max-height: 90vh;
  padding: 0;
  border: var(--pico-border-width) solid var(--phosphor-dim);
  border-radius: 4px;
  overflow: hidden auto;
  background: var(--pico-card-background-color);
  color: inherit;
}

.media-dialog-close-form {
  position: absolute;
  top: 0.3rem;
  right: 0.3rem;
  margin: 0;
}

.media-dialog-close {
  width: auto;
  margin: 0;
  padding: 0.1rem 0.5rem;
  border: none;
  background: none;
  color: var(--phosphor-dim);
  font-size: 1.4rem;
  line-height: 1;
  cursor: pointer;
}

.media-dialog-close:is(:hover, :focus-visible) {
  color: var(--phosphor-bright);
}

/* width/height: auto (not width: 100%) so the image renders at its own
 * intrinsic aspect ratio, only capped by max-width/max-height — this is
 * what lets .media-dialog-card's own width: fit-content actually shrink
 * to match a portrait photo instead of stretching it to a fixed
 * landscape-shaped box. No background fill needed anymore either: with
 * nothing forcing a mismatched box around it, there's no letterboxed gap
 * left for a fill color to show through. */
.media-dialog-image {
  display: block;
  width: auto;
  height: auto;
  max-width: min(85vw, 640px);
  max-height: 70vh;
  object-fit: contain;
}

.media-caption {
  padding: 0.6rem 0.7rem;
  border-top: var(--pico-border-width) dashed var(--phosphor-dim);
}

/* Label vs. filename/date hierarchy, inside the per-image dialog: the
 * label is what a customer actually cares about ("which one was the
 * Titelbild?"), so it stays full-weight text; filename/date are
 * secondary/technical detail, de-emphasized the same way .version-date
 * de-emphasizes a version's timestamp relative to its message. */
.media-caption-label {
  margin: 0 0 0.2rem;
  font-weight: 700;
  color: var(--phosphor-bright);
}

.media-caption-meta {
  margin: 0;
  color: var(--phosphor-dim);
  font-size: 0.8em;
}

/* The replace control: a native <details>/<summary> disclosure (see
 * media.html) rather than an always-visible inline form — keeps each tile
 * compact by default, no JavaScript needed to reveal the file input on
 * demand. "ersetzen" (rather than the <button> inside it) is what's
 * visible before the customer commits to replacing an image. */
.media-replace-details summary {
  margin-top: 0.5rem;
  color: var(--phosphor-dim);
  font-size: 0.8em;
  cursor: pointer;
}

.media-replace-details summary:is(:hover, :focus) {
  color: var(--phosphor-bright);
}

.media-replace-details[open] summary {
  margin-bottom: 0.4rem;
}

/* The replace mini-form's own fields — stacked (not the old inline flex
 * row), since it now lives inside a narrow grid tile rather than a wide
 * card row. */
.media-replace-form {
  display: flex;
  flex-direction: column;
  gap: 0.4rem;
  margin: 0;
}

.media-replace-form input[type="text"] {
  margin-bottom: 0;
}

.media-replace-form button {
  margin-bottom: 0;
  width: auto;
  align-self: flex-start;
}

/* The "+ Add image" upload tile: a two-part shape (a top area the same
 * size as .media-thumb, a .media-caption strip below it for the chosen
 * filename + submit button) rather than a plain stacked form — the
 * earlier version was just a bare <label>+<input>+<button> column, which
 * left a large dead gap under the button once the tile matched an image
 * tile's height. A dashed border (this redesign's recurring "add new
 * thing" affordance, matching the login/settings/chat forms' own
 * crt-input/crt-bracket-button treatment) instead of a photo. */
.media-tile-add {
  border-style: dashed;
}

/* The dropzone: a real <label for="image">, not decoration — clicking or
 * tapping anywhere in it opens the file picker via the native label/input
 * association, so the input itself can stay visually hidden (.sr-only,
 * media.html) without losing a large, obvious click target or keyboard/
 * screen-reader access to it. Same aspect-ratio and full-bleed width as
 * .media-thumb above so the two tile shapes actually line up in the grid. */
.media-upload-dropzone {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.3rem;
  width: 100%;
  aspect-ratio: 4 / 3;
  color: var(--phosphor-dim);
  cursor: pointer;
}

.media-upload-dropzone:hover,
.media-upload-dropzone:has(+ input:focus-visible) {
  color: var(--phosphor-bright);
  background: var(--glow-soft);
}

.media-upload-plus {
  font-size: 2rem;
  line-height: 1;
}

/* The chosen filename readout replaces the native file input's own
 * "no file chosen" text (necessarily hidden along with the input) —
 * updated live by media.html's own small <script> on the input's
 * "change" event. Reuses .media-caption's existing padding/border-top
 * rhythm, the same strip style now used inside each image's own
 * .media-dialog. */
.media-upload-filename {
  margin: 0 0 0.5rem;
  color: var(--phosphor-dim);
  font-size: 0.85em;
}

/* media.html's empty state (a configured customer with no images uploaded
 * yet) — same quiet, dim explanatory treatment as .version-empty-state
 * above, reused rather than reinvented. */
.media-empty-state {
  color: var(--phosphor-dim);
  margin-top: var(--pico-spacing);
}

/* ============================================================
 * Preview canvas (docs/features/FEATURE_PREVIEW_CANVAS.md) — the on-demand
 * live-preview panel that replaces the old inline per-exchange
 * iframe (.chat-page-embed-panel above, now unused). Closed by
 * default so the chat keeps its full 900px column exactly as
 * before; it opens on a show_page result or the reopen handle,
 * is dismissable, and carries a Desktop/Tablet/Mobil switcher.
 *
 * Implemented as a FIXED layer rather than a flex sibling of the
 * chat column on purpose: the chat column's own layout above is
 * heavily tuned and live-measured, so it must stay structurally
 * untouched. Every rule here is gated on the body's
 * data-preview="open" state, so the closed page is byte-identical
 * to before.
 * ============================================================ */

/* Reopen handle: a slim fixed tab on the right edge, shown whenever the
 * canvas is closed. It's the always-available manual entry point — clicking
 * it opens the canvas to the current draft, building it on demand if the
 * agent hasn't shown a page yet (docs/features/FEATURE_PREVIEW_CANVAS.md v2). */
.chat-canvas-handle {
  display: none;
  position: fixed;
  top: 50%;
  right: 0;
  transform: translateY(-50%);
  z-index: 40;
  width: auto;
  margin: 0;
  writing-mode: vertical-rl;
  padding: 0.9rem 0.35rem;
  border: var(--pico-border-width) solid var(--phosphor-dim);
  border-right: none;
  border-radius: var(--pico-border-radius) 0 0 var(--pico-border-radius);
  background: var(--pico-card-background-color);
  color: var(--phosphor-dim);
  font-family: var(--pico-font-family);
  font-size: 0.72rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  cursor: pointer;
}
.chat-page[data-preview="closed"] .chat-canvas-handle { display: block; }
.chat-canvas-handle:is(:hover, :focus) { color: var(--phosphor-bright); border-color: var(--pico-primary); }

/* The canvas panel — a fixed layer. Hidden until data-preview="open". */
.chat-canvas {
  display: none;
  position: fixed;
  top: 0;
  right: 0;
  height: 100vh;
  width: 54vw;
  z-index: 45;
  box-sizing: border-box;
  flex-direction: column;
  background: var(--pico-card-background-color);
  border-left: var(--pico-border-width) solid var(--phosphor-dim);
  box-shadow: 0 0 40px var(--glow-soft);
}
.chat-page[data-preview="open"] .chat-canvas { display: flex; }

/* On a wide viewport, opening the canvas un-centers the chat and pins it
 * to the left half so it isn't hidden behind the fixed canvas. Below the
 * breakpoint the canvas is a full-screen overlay (further down), so this
 * side-by-side shift only applies where there's actually room for it. */
@media (min-width: 900px) {
  .chat-page[data-preview="open"] {
    max-width: none;
    margin: 0;
    width: 46vw;
    transition: width .28s ease;
  }
  /* Fullscreen option: the fixed canvas grows to cover the whole viewport
     (on top of the chat), toggled from the header (data-canvas-full).
     left:0 + the base right:0 with width:auto spans the exact viewport width
     without the scrollbar-overflow risk a 100vw would carry. */
  .chat-page[data-preview="open"][data-canvas-full] .chat-canvas { left: 0; width: auto; }
}

.chat-canvas-head {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  /* Explicit border-box height matching .app-header's (0.9rem text + block
     padding of calc(spacing * 0.5) each side + its bottom border), so the
     head↔body divider lines up with the chat side's header divider — both
     headers sit at top:0, so equal heights put the two lines at the same Y.
     (Without this the switcher's own taller box pushed this divider lower
     than the chat header's.) */
  height: calc(0.9rem * var(--pico-line-height, 1.5) + var(--pico-spacing) + var(--pico-border-width));
  box-sizing: border-box;
  padding: 0 0.6rem;
  border-bottom: var(--pico-border-width) solid var(--phosphor-dim);
}
.chat-canvas-url {
  flex: 1 1 0;
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  color: var(--phosphor-dim);
  font-size: 0.74rem;
}
/* The device switcher is the centered middle of the head: the url (left) and
 * the actions (right) each take flex:1, so the fixed-size switcher sits
 * centered with clear space to the tag/url on either side. width:auto and the
 * button's flex:0 0 auto still override Pico classless's [role=group]
 * width:100% / >*{flex:1 1 auto} trap (equal specificity, app.css loads last;
 * without it the switcher balloons and shoves the icon buttons off-screen). */
.chat-canvas-devices {
  display: inline-flex;
  flex: 0 0 auto;
  width: auto;
  margin-bottom: 0;
  gap: 0.1rem;
  padding: 0.12rem;
  border: var(--pico-border-width) solid var(--phosphor-dim);
  border-radius: var(--pico-border-radius);
}
.chat-canvas-devices button {
  width: auto;
  flex: 0 0 auto;
  margin: 0;
  padding: 0.16rem 0.5rem;
  border: none;
  background: none;
  color: var(--phosphor-dim);
  font-family: var(--pico-font-family);
  font-size: 0.68rem;
  letter-spacing: 0.03em;
  cursor: pointer;
  border-radius: calc(var(--pico-border-radius) - 1px);
}
.chat-canvas-devices button[aria-pressed="true"] { color: var(--phosphor-bright); background: var(--glow-soft); }
.chat-canvas-actions {
  flex: 1 1 0;
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.1rem;
}
.chat-canvas-iconbtn {
  width: auto;
  flex: 0 0 auto;
  margin: 0;
  padding: 0.15rem 0.35rem;
  border: none;
  background: none;
  color: var(--phosphor-dim);
  font-size: 0.9rem;
  line-height: 1;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.chat-canvas-iconbtn:is(:hover, :focus) { color: var(--phosphor-bright); }
.chat-canvas-iconbtn[aria-pressed="true"] { color: var(--phosphor-bright); }
/* The fullscreen toggle is only meaningful on a wide viewport — below the
 * split breakpoint the canvas is already a full-screen overlay. */
@media (max-width: 899px) { .chat-canvas-fs-btn { display: none; } }

.chat-canvas-body {
  flex: 1;
  min-height: 0;
  overflow: auto;
  padding: 0.7rem;
  display: flex;
  justify-content: center;
  align-items: flex-start;
  /* Scratchpad dot grid behind the device frame — visible in the margins
     around a narrower tablet/mobile view, so the framed page reads as sitting
     on a surface rather than floating in an empty box. background-repeat:
     repeat is required: Pico's universal reset sets background-repeat:no-repeat
     on every element, so without this the gradient tiles only once (a single
     dot hidden under the padding) instead of forming a grid. */
  background-color: var(--pico-card-background-color);
  background-image: radial-gradient(var(--phosphor-dim) 1px, transparent 1.2px);
  background-size: 28px 28px;
  background-repeat: repeat;
}
.chat-canvas-empty {
  margin: auto;
  max-width: 34ch;
  text-align: center;
  color: var(--phosphor-dim);
  font-size: 0.85rem;
  line-height: 1.5;
}
/* The site renders at TRUE device width and is scaled by JS to fit the
 * pane (transform doesn't change layout size, so JS sizes the wrap to the
 * scaled box) — the same trick a device-preview / DevTools responsive mode
 * uses, so a page's own responsive breakpoints fire at the real width. */
.chat-canvas-stage-wrap { position: relative; flex: 0 0 auto; }
.chat-canvas-frame {
  position: absolute;
  top: 0;
  left: 0;
  transform-origin: top left;
  border: var(--pico-border-width) solid var(--phosphor-dim);
  border-radius: var(--pico-border-radius);
  background: #fff;
}

/* Narrow viewport: the canvas is a full-screen overlay, and the chat
 * behind it is hidden — no room for a side-by-side split. */
@media (max-width: 899px) {
  .chat-canvas { width: 100vw; border-left: none; }
  .chat-page[data-preview="open"] .chat-scroll,
  .chat-page[data-preview="open"] .chat-input-bar,
  .chat-page[data-preview="open"] #version-status { visibility: hidden; }
}

/* The preview chip in a chat exchange — replaces the old inline iframe.
 * A quiet affordance that (re)opens the canvas to that page, and (unlike
 * the transient live open) persists in the log so an earlier preview can
 * be reopened after a refresh. */
.chat-preview-chip {
  width: auto;
  margin: calc(var(--pico-spacing) * 0.35) 0 0;
  padding: 0.38rem 0.7rem;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  border: var(--pico-border-width) dashed var(--phosphor-dim);
  border-radius: var(--pico-border-radius);
  background: none;
  color: var(--pico-primary);
  font-family: var(--pico-font-family);
  font-size: 0.8rem;
  cursor: pointer;
}
.chat-preview-chip:is(:hover, :focus) { border-color: var(--pico-primary); background: var(--glow-soft); }
/* A consumed edit chip (disabled after its one save — docs/features/FEATURE_EDITOR.md): dim,
 * inert, no hover affordance. */
.chat-preview-chip:disabled { opacity: 0.5; cursor: default; color: var(--phosphor-dim); }
.chat-preview-chip:disabled:is(:hover, :focus) { border-color: var(--phosphor-dim); background: none; }

@media (prefers-reduced-motion: reduce) {
  .chat-page[data-preview="open"] { transition: none; }
}

/* ============================================================
 * Editor mode (docs/features/FEATURE_EDITOR.md) — the canvas hosts a plain-text editor
 * for one content page's BODY instead of the live preview, toggled by
 * body[data-canvas-mode="editor"]. The agent hands a file over (offer_edit);
 * the customer edits the prose (front matter stays protected) and saves,
 * which builds + previews the result in the same canvas.
 * ============================================================ */
.chat-page[data-canvas-mode="editor"] .chat-canvas-stage-wrap,
.chat-page[data-canvas-mode="editor"] .chat-canvas-empty { display: none; }
/* editor mode has no device/fullscreen/reload controls — the path shows in
 * the url slot, and only close remains */
.chat-page[data-canvas-mode="editor"] .chat-canvas-devices,
.chat-page[data-canvas-mode="editor"] #chat-canvas-fullscreen,
.chat-page[data-canvas-mode="editor"] #chat-canvas-reload { display: none; }
.chat-page[data-canvas-mode="editor"] .chat-canvas-body { padding: 0; align-items: stretch; background: none; }

.chat-editor { display: none; flex: 1; min-height: 0; width: 100%; flex-direction: column; }
.chat-page[data-canvas-mode="editor"] .chat-editor { display: flex; }
.chat-editor-fm {
  flex: 0 0 auto;
  margin: 0;
  padding: 0.5rem 0.75rem;
  /* Solid (not dashed) so the header-region↔body divider reads as one
     continuous line, matching the chat header's own solid divider. */
  border-bottom: var(--pico-border-width) solid var(--phosphor-dim);
  color: var(--phosphor-dim);
  font-size: 0.8rem;
  display: flex;
  align-items: center;
  gap: 0.5rem;
}
.chat-editor-body {
  flex: 1;
  min-height: 0;
  resize: none;
  margin: 0;
  border: none;
  border-radius: 0;
  background: var(--pico-background-color);
  color: var(--phosphor-bright);
  font-family: var(--pico-font-family);
  font-size: 0.9rem;
  line-height: 1.6;
  padding: 0.9rem 1rem;
}
.chat-editor-body:focus { outline: 2px solid var(--pico-primary); outline-offset: -2px; }
.chat-editor-savebar {
  flex: 0 0 auto;
  display: flex;
  align-items: center;
  gap: 0.75rem;
  padding: 0.6rem 0.75rem;
  border-top: var(--pico-border-width) solid var(--phosphor-dim);
}
.chat-editor-save {
  width: auto;
  margin: 0;
  padding: 0.45rem 0.9rem;
  border-radius: var(--pico-border-radius);
  border: none;
  background: var(--pico-primary-background);
  color: var(--pico-primary-inverse);
  font-family: var(--pico-font-family);
  font-size: 0.78rem;
  letter-spacing: 0.03em;
}
.chat-editor-note { font-size: 0.72rem; color: var(--phosphor-dim); }
.chat-editor-status { margin-left: auto; font-family: var(--pico-font-family); font-size: 0.74rem; color: var(--pico-primary); }
/* The edit chip reuses .chat-preview-chip (dashed), just a different glyph. */

/* ============================================================
 * Media manager mode (docs/features/FEATURE_IMAGE_UPLOAD.md) — the canvas hosts the
 * customer's own image manager (upload/replace/delete) instead of the live
 * preview, toggled by body[data-canvas-mode="media"]. The agent opens it with
 * open_media; a successful upload feeds the new image's path back to the agent
 * as a follow-up turn. Reuses the .media-grid/.media-tile/.media-thumb styling
 * the standalone media page already defines above.
 * ============================================================ */
.chat-page[data-canvas-mode="media"] .chat-canvas-stage-wrap,
.chat-page[data-canvas-mode="media"] .chat-canvas-empty { display: none; }
/* media mode has no device/fullscreen/reload controls — the title shows in the
   url slot, and only close remains (same as editor mode). */
.chat-page[data-canvas-mode="media"] .chat-canvas-devices,
.chat-page[data-canvas-mode="media"] #chat-canvas-fullscreen,
.chat-page[data-canvas-mode="media"] #chat-canvas-reload { display: none; }
/* Stretch the single media child to the full body height and drop the preview
   dot-grid background; the grid itself scrolls (below), not the whole body. */
.chat-page[data-canvas-mode="media"] .chat-canvas-body { align-items: stretch; background: none; }

.chat-media { display: none; flex: 1; min-height: 0; width: 100%; flex-direction: column; gap: 0.55rem; }
.chat-page[data-canvas-mode="media"] .chat-media { display: flex; }
.chat-media-status {
  flex: 0 0 auto;
  margin: 0;
  font-family: var(--pico-font-family);
  font-size: 0.78rem;
  color: var(--pico-primary);
}
.chat-media-status:empty { display: none; }
.chat-media-status-error { color: var(--pico-del-color); }
/* The grid is the scroll region, so the status line and the "nothing goes live"
   note stay pinned above/below it as the image list grows. */
.chat-media-grid { flex: 1 1 auto; min-height: 0; overflow: auto; }
.chat-media-note { flex: 0 0 auto; margin: 0; font-size: 0.72rem; color: var(--phosphor-dim); }

/* Per-tile Replace/Delete controls (the panel grid's inline actions, in place
   of the standalone page's per-image <dialog> lightbox). */
.media-tile-actions { display: flex; gap: 0.4rem; margin-top: 0.5rem; }
.media-tile-action {
  width: auto;
  margin: 0;
  padding: 0.24rem 0.6rem;
  border: var(--pico-border-width) solid var(--phosphor-dim);
  border-radius: var(--pico-border-radius);
  background: none;
  color: var(--phosphor-dim);
  font-family: var(--pico-font-family);
  font-size: 0.72rem;
  cursor: pointer;
}
.media-tile-action:is(:hover, :focus) { color: var(--phosphor-bright); border-color: var(--pico-primary); }
.media-tile-action-delete:is(:hover, :focus) { color: var(--pico-del-color); border-color: var(--pico-del-color); }

/* ============================================================
 * Version history mode (docs/features/FEATURE_VERSIONING.md's Slice 7) — the canvas hosts the
 * customer's version history (browse + preview/rollback/reset/publish) instead
 * of the live preview, toggled by body[data-canvas-mode="versions"]. The agent
 * opens it with open_versions, or the customer clicks the #version-status line.
 * Reuses the .version-entry/.version-name/.version-status styling the old
 * standalone versions page defined above. Mirrors the media mode's own layout.
 * ============================================================ */
.chat-page[data-canvas-mode="versions"] .chat-canvas-stage-wrap,
.chat-page[data-canvas-mode="versions"] .chat-canvas-empty { display: none; }
/* Like editor/media mode: no device/fullscreen/reload controls — the title
   shows in the url slot, and only close remains. */
.chat-page[data-canvas-mode="versions"] .chat-canvas-devices,
.chat-page[data-canvas-mode="versions"] #chat-canvas-fullscreen,
.chat-page[data-canvas-mode="versions"] #chat-canvas-reload { display: none; }
.chat-page[data-canvas-mode="versions"] .chat-canvas-body { align-items: stretch; background: none; }

.chat-versions { display: none; flex: 1; min-height: 0; width: 100%; flex-direction: column; gap: 0.55rem; }
.chat-page[data-canvas-mode="versions"] .chat-versions { display: flex; }
.chat-versions-status {
  flex: 0 0 auto;
  margin: 0;
  font-family: var(--pico-font-family);
  font-size: 0.78rem;
  color: var(--pico-primary);
}
.chat-versions-status:empty { display: none; }
.chat-versions-status-error { color: var(--pico-del-color); }
/* The list is the scroll region, so the status line and the "publishing puts it
   live" note stay pinned above/below it as the history grows. */
.chat-versions-list { flex: 1 1 auto; min-height: 0; overflow: auto; }

/* The publish/reset bar pinned above the timeline: the one place a customer
   goes live, so the Publish button is the panel's single emphasized action. */
.versions-panel-publishbar {
  flex: 0 0 auto;
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
  align-items: center;
  padding-bottom: 0.7rem;
  margin-bottom: 0.3rem;
  border-bottom: var(--pico-border-width) dashed var(--phosphor-dim);
}
/* The only filled/primary button in the panel — going live is the deliberate,
   high-consequence action, so it reads as the one call to action here. */
.versions-panel-publish {
  width: auto;
  margin: 0;
  padding: 0.4rem 0.9rem;
  border: var(--pico-border-width) solid var(--pico-primary);
  border-radius: var(--pico-border-radius);
  background: color-mix(in srgb, var(--pico-primary-background) 15%, transparent);
  color: var(--pico-primary);
  font-family: var(--pico-font-family);
  font-size: 0.8rem;
  font-weight: 600;
  cursor: pointer;
}
.versions-panel-publish:is(:hover, :focus) { color: var(--phosphor-bright); border-color: var(--phosphor-bright); }

/* The current-HEAD marker beside a version name, same compact metadata weight
   as the live/draft badge but in the ambient dim token (it's positional info,
   not a status). */
.versions-panel-current {
  margin-left: 0.4em;
  font-size: 0.7em;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--phosphor-dim);
}

/* ============================================================
 * Settings mode (docs/features/FEATURE_SETTINGS.md) — the canvas hosts the
 * customer's own credentials (password + Anthropic API key) instead of the live
 * preview, toggled by body[data-canvas-mode="settings"]. Opened by the agent
 * (open_settings), by the header's settings control, or automatically when a
 * turn failed because Anthropic rejected the key. Mirrors the media/versions
 * modes' own layout exactly.
 * ============================================================ */
.chat-page[data-canvas-mode="settings"] .chat-canvas-stage-wrap,
.chat-page[data-canvas-mode="settings"] .chat-canvas-empty { display: none; }
/* Like editor/media/versions mode: no device/fullscreen/reload controls — the
   title shows in the url slot, and only close remains. */
.chat-page[data-canvas-mode="settings"] .chat-canvas-devices,
.chat-page[data-canvas-mode="settings"] #chat-canvas-fullscreen,
.chat-page[data-canvas-mode="settings"] #chat-canvas-reload { display: none; }
.chat-page[data-canvas-mode="settings"] .chat-canvas-body { align-items: stretch; background: none; }

.chat-settings { display: none; flex: 1; min-height: 0; width: 100%; flex-direction: column; gap: 0.55rem; }
.chat-page[data-canvas-mode="settings"] .chat-settings { display: flex; }
.chat-settings-status {
  flex: 0 0 auto;
  margin: 0;
  font-family: var(--pico-font-family);
  font-size: 0.78rem;
  color: var(--pico-primary);
}
.chat-settings-status:empty { display: none; }
.chat-settings-status-error { color: var(--pico-del-color); }
/* The forms are the scroll region, so the status line and the "only you can
   change these" note stay pinned above/below them on a short viewport. */
.chat-settings-form { flex: 1 1 auto; min-height: 0; overflow: auto; }

.versions-panel-actions { display: flex; flex-wrap: wrap; gap: 0.4rem; margin-top: 0.5rem; }
/* Per-entry Preview/Restore (and the bar's Reset) — the same quiet outline
   button as the media panel's tile actions, so the two panels read as one
   system. Publish above is the one deliberately louder action. */
.version-action {
  width: auto;
  margin: 0;
  padding: 0.24rem 0.6rem;
  border: var(--pico-border-width) solid var(--phosphor-dim);
  border-radius: var(--pico-border-radius);
  background: none;
  color: var(--phosphor-dim);
  font-family: var(--pico-font-family);
  font-size: 0.72rem;
  cursor: pointer;
}
.version-action:is(:hover, :focus) { color: var(--phosphor-bright); border-color: var(--pico-primary); }

/* The #version-status line doubles as the click-to-open affordance for this
   panel (see chat.html) — signal that with a pointer cursor on the text that's
   actually there (the empty div, when there's no history yet, isn't clickable). */
#version-status .chat-publish-indicator { cursor: pointer; }
#version-status .chat-publish-indicator:hover { color: var(--phosphor-bright); }
