/* ══════════════════════════════════════════════════════════════
   Koma Image
   Design intent: the generated image is the only bright object on
   screen. Chrome stays quiet, cool-neutral, and low-contrast so a
   picture placed on it reads as a physical print on a dark mat.
   ══════════════════════════════════════════════════════════════ */

:root {
  color-scheme: dark;

  /* ── Surfaces ───────────────────────────────────────────────
     Slightly blue-shifted greys. Pure neutral greys look muddy next
     to saturated artwork; a cool cast makes warm colours pop. The
     blue shift (B−R) grows with lightness: +2 → +8.

     Steps are set by ΔL* (perceptual lightness), NOT by WCAG contrast
     ratio. CR exists to predict text legibility; its +0.05 term
     dominates at the dark end, so a fixed CR target demands a far
     bigger perceptual jump near black than it does at mid grey. An
     earlier pass here chased "CR ≥ 1.15" from a near-black canvas and
     produced ΔL* = +13 between the viewport and the panels — two to
     four times what any shipping product uses, which read as a rift
     down the middle of the window rather than as structure:

       VS Code  +3.45    Notion  +3.49
       Linear   +5.15    Figma   +6.74

     A later pass tightened it further: +4 still read as too much separation
     for a tool whose entire job is looking at pictures. Image generators sit at
     the quiet end of that band — NovelAI is barely a shade — because every step
     in the chrome competes with the artwork. canvas→chrome is +2.5 now and the
     panels are still unmistakably panels. The original
     problem still had to be fixed: topbar, composer, stage, viewport
     and gallery all painted the same colour, so the five main regions
     had zero separation and the app read as controls scattered on one
     black board. Panels are now distinct — by one step, not a chasm.

     Absolute level stays close to the original near-black, because the
     point of this UI is that the picture is the bright object. White
     on --canvas is still 19.3:1. */
  --canvas: #131315;   /* viewport — the mat a print sits on */
  --sunken: #131316;   /* inputs, wells, thumbnails */
  --chrome: #18181c;   /* topbar, composer, stage head, gallery */
  --raised: #1e1e24;   /* cards, drawers, sheets, tooltips */
  --hover:  #24242c;   /* pointer feedback on raised things */

  /* Focus deepens the well by one notch. This was a hardcoded #0c0c0f that
     survived two retunes of the ramp and had drifted to L*=3.4 — darker than
     the canvas the image sits on. */
  --sunken-focus: #101013;

  /* "Recessed" is a relationship, not a colour. --sunken is tuned to sit one
     notch (ΔL* 2.4) below --chrome, which is right for controls in the composer
     but wrong inside a --raised card: there the same absolute value lands 5.5
     below its container and the control reads as a hole punched in the panel.
     Raised containers re-point these two so the recess stays one notch deep
     wherever a control happens to live. */
  --well: var(--sunken);
  --well-focus: var(--sunken-focus);

  --line: #1f1f25;     /* ΔL* +3.6 over chrome */
  --line-2: #25252e;   /* ΔL* +6.6 — emphasis dividers, focus wells */

  /* Contrast ratio IS the right metric here — it is what WCAG models.
     Anchored against --raised, the lightest surface any of these can
     land on: labels and hints sit inside drawers and cards too, and a
     value tuned for the panel silently fails one level up. Measuring
     rendered pixels caught exactly that — .chip came out at 2.79 while
     the arithmetic said 3.20, because .chip sits on --raised. */
  --fg: #e8e8eb;
  --fg-2: #a8a8ae;   /* 7.01 on raised */
  --fg-3: #86868d;   /* 4.59 on raised — AA */
  --fg-4: #6a6a71;   /* 3.09 on raised — decorative/large only */

  --a: #b3a1ff;
  --a-ink: #17132a;
  --a-hover: #c1b2ff;
  --a-glow: rgba(179, 161, 255, 0.09);
  --a-dim: rgba(179, 161, 255, 0.13);
  --a-line: rgba(179, 161, 255, 0.34);

  --ok: #74cf9b;
  --warn: #e0ab68;
  --err: #e88787;
  --err-dim: rgba(232, 135, 135, 0.08);

  /* elevation — spread grows negative with height so edges stay tight */
  --sh-1: 0 6px 16px -4px rgba(0, 0, 0, 0.6);
  --sh-2: 0 12px 32px -8px rgba(0, 0, 0, 0.7);
  --sh-3: 0 14px 38px -10px rgba(0, 0, 0, 0.84);
  --sh-4: 0 32px 80px -12px rgba(0, 0, 0, 0.7);
  --sh-lip: 0 1px 0 rgba(255, 255, 255, 0.035) inset;
  --ring: 0 0 0 3px var(--a-dim);

  /* stacking contract — keep every layer in this table */
  --z-action: 19;
  --z-sticky: 20;
  --z-tip: 40;
  --z-pop: 80;
  --z-scrim: 90;
  --z-modal: 100;
  --z-toast: 110;

  /* ── Spacing ────────────────────────────────────────────────
     A true 4px grid. The old scale claimed "4px baseline" while 7 of
     its 8 values broke it (7/11/15/18/22/30/38) with ratios lurching
     between 1.75 and 1.20. Individually each looked fine; together
     they had no rhythm, which is the difference between a designed
     system and accumulated nudges. */
  --sp-1: 4px;  --sp-2: 8px;  --sp-3: 12px; --sp-4: 16px;
  --sp-5: 24px; --sp-6: 32px; --sp-7: 48px;

  --r1: 5px; --r2: 7px; --r3: 10px; --r4: 15px;

  /* ── Type ───────────────────────────────────────────────────
     The old scale was 10/11/12/13px: a 1.30× total span with 1.09
     steps. That is below the threshold where a size difference reads
     as hierarchy, so "title" and "body" were the same object to the
     eye and the whole surface flattened into grey noise. Commercial
     UI spans ~3×. This spans 2.55× and keeps steps ≥1.15 from --md
     up, where hierarchy actually has to be legible.

     Small sizes stay tight (11/12/13) because CJK glyphs carry far
     more visual weight than Latin at the same size. */
  --fz-2xs: 11px;  /* meta, counters, mono readouts */
  --fz-xs:  12px;  /* labels, hints */
  --fz-sm:  13px;  /* body, inputs, buttons */
  --fz-md:  15px;  /* prompt, primary action */
  --fz-lg:  18px;  /* section titles */
  --fz-xl:  22px;  /* modal titles */
  --fz-2xl: 28px;  /* key gate brand */

  /* Four named weights keep hierarchy deliberate across both pages. The
     variable font files contain every level, so the browser never has to
     synthesize a missing face. */
  --fw-regular: 400;
  --fw-medium: 500;
  --fw-semibold: 600;
  --fw-bold: 700;

  --ico: 16px;

  --e: cubic-bezier(0.32, 0.72, 0.28, 1);
  --e-out: cubic-bezier(0.16, 1, 0.3, 1);
  --t1: 120ms; --t2: 220ms; --t3: 380ms;

  /* Manrope gives the UI its compact geometric voice; Noto Sans SC covers
     arbitrary Chinese input with the same complete variable-weight range. */
  --sans: "Manrope Variable", "Noto Sans SC Variable", sans-serif;
  --mono: "JetBrains Mono Variable", "Noto Sans SC Variable", monospace;

  /* The header is taller than the inner column heads: it carries the brand and
     the gauges, they carry a label. These were all one hardcoded 48px, so any
     change to the header silently broke the shell height and the sticky offset.
     Two names now, and the dependents reference --bar-h. */
  --bar-h: 60px;    /* topbar */
  --head-h: 48px;   /* gallery head (the stage status is sr-only, not a bar) */
  --mobile-foot-h: 61px;

  /* The print's lift off the mat. */
  --print-shadow:
    0 0 0 1px rgba(255,255,255,0.06),
    0 1px 2px rgba(0,0,0,0.24),
    0 14px 36px -12px rgba(0,0,0,0.6);
  /* The same layers at zero strength, so the frame can fade into it. */
  --print-shadow-off:
    0 0 0 1px rgba(255,255,255,0),
    0 1px 2px rgba(0,0,0,0),
    0 14px 36px -12px rgba(0,0,0,0);

  /* Loading-state ink. Both stay inside the accent hue; only lightness moves.
     These are deliberately far stronger than a surface step (~2.5 dL*): a
     loading state has to read as motion from across the room, and the earlier
     0.10-alpha attempt measured as barely-there. */
  --bar-ink: rgba(214, 205, 255, 0.92);
  /* Peak of the travelling band. Only the strip under the band pays this, so the
     frame keeps its own dark base — unlike a flat fill, where the alpha applies
     to every pixel and any value high enough to notice washes the panel out. */
  --sheen-ink: rgba(214, 205, 255, 0.14);

  --rail: 316px;
  --hist: 132px;
  --cart: 300px;
}

*, *::before, *::after { box-sizing: border-box; }
* { min-width: 0; }

html, body { height: 100%; }

body {
  margin: 0;
  background: var(--canvas);
  color: var(--fg);
  font: var(--fw-regular) var(--fz-sm)/1.5 var(--sans);
  font-synthesis: none;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  letter-spacing: -0.003em;
}

button, input, textarea, select, output { font: inherit; color: inherit; letter-spacing: inherit; }
button { cursor: pointer; border: 0; background: none; padding: 0; }
h1 { margin: 0; font-weight: var(--fw-bold); }
b, strong { font-weight: var(--fw-semibold); }
p { margin: 0; }
hr.rule { height: 1px; margin: var(--sp-2) 0; border: 0; background: var(--line); }

.mono { font-family: var(--mono); font-size: var(--fz-2xs); font-variant-numeric: tabular-nums; letter-spacing: 0; }
.dim { color: var(--fg-3); }

kbd {
  font: var(--fw-medium) var(--fz-2xs)/1 var(--mono);
  padding: 3px 5px;
  color: var(--fg-2);
  background: var(--well);
  border: 1px solid var(--line-2);
  border-radius: 5px;
}

:focus-visible { outline: 2px solid var(--a); outline-offset: 2px; border-radius: var(--r1); }
:focus:not(:focus-visible) { outline: none; }
::selection { background: rgba(179, 161, 255, 0.3); }

/* thin, self-effacing scrollbars */
* { scrollbar-width: thin; scrollbar-color: var(--line-2) transparent; }
*::-webkit-scrollbar { width: 11px; height: 11px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb {
  background: var(--line-2);
  border: 3.5px solid transparent;
  background-clip: content-box;
  border-radius: 99px;
}
*::-webkit-scrollbar-thumb:hover { background: #40404c; background-clip: content-box; }

/* ── Form primitives ─────────────────────────────────────────── */
input[type="text"], input[type="number"], input[type="password"],
input[type="search"], textarea, select {
  width: 100%;
  padding: 7px 10px;
  background: var(--well);
  border: 1px solid var(--line);
  border-radius: var(--r2);
  transition: border-color var(--t1) var(--e), background var(--t1) var(--e),
    box-shadow var(--t1) var(--e);
}
input[type="search"] { appearance: none; }
input[type="search"]::-webkit-search-cancel-button { opacity: 0.45; cursor: pointer; }
input[type="number"] { font-family: var(--mono); font-size: var(--fz-sm); }
input:hover:not(:focus), textarea:hover:not(:focus), select:hover:not(:focus) { border-color: var(--line-2); }
input:focus, textarea:focus, select:focus {
  outline: none;
  border-color: var(--a-line);
  background: var(--well-focus);
  box-shadow: var(--ring);
}
::placeholder { color: var(--fg-4); }
textarea { resize: vertical; line-height: 1.6; }
/* No spinner arrows: they are a 9px target that crowds the figure, and every
   number field here is typed or nudged with the arrow keys anyway. */
input[type="number"] { -moz-appearance: textfield; appearance: textfield; }
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }

select {
  appearance: none;
  padding-right: 30px;
  cursor: pointer;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='11' height='11' viewBox='0 0 12 12'%3E%3Cpath d='M2.5 4.75 6 8.25l3.5-3.5' fill='none' stroke='%23a6a5b0' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
  background-repeat: no-repeat;
  background-position: right 10px center;
}
select option { background: var(--raised); }

/* field */
.fld { display: grid; gap: 6px; }
.fld-group { display: grid; gap: var(--sp-3); }
.fld-top { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2); min-height: 22px; }
.fld-actions { display: flex; align-items: center; gap: var(--sp-2); }
.fld label, .slider-top label {
  font-size: var(--fz-xs); font-weight: var(--fw-medium);
  color: var(--fg-3);
  letter-spacing: 0.01em;
}
.hint { font-size: var(--fz-xs); line-height: 1.45; color: var(--fg-3); transition: color var(--t1) var(--e); }
.hint.warn { color: var(--warn); }
.hint[hidden] { display: none; }
.fld-inline input:disabled { opacity: 0.52; cursor: not-allowed; }

.quota-warning {
  display: flex; align-items: flex-start; gap: 7px;
  padding: 7px 9px;
  color: var(--warn); background: rgba(224, 171, 104, 0.08);
  border: 1px solid rgba(224, 171, 104, 0.22); border-radius: var(--r2);
  font-size: var(--fz-2xs); line-height: 1.4;
}
.quota-warning[hidden] { display: none; }
.quota-warning .ico { flex: none; width: 14px; height: 14px; margin-top: 1px; }

.pair { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-2); }
.pair.tight { gap: var(--sp-2); }
.pair[hidden] { display: none; }

/* The prompt is the primary input, so it claims all leftover height. */
.fld-hero {
  position: relative;
  flex: 1 1 auto !important;
  min-height: 190px;
  display: grid;
  /* tab row + input row */
  grid-template-rows: auto minmax(0, 1fr);
}
/* The wrapper takes the flexible row; the textarea fills it. All text metrics
   come from the shared .hl-wrap block so the mirror cannot drift. */
/* The wrapper is a bounded scroller filling the flexible row. The textarea
   inside is unbounded (auto-grown), which is what makes the wrapper scroll. */
.fld-hero .hl-wrap { height: 100%; min-height: 0; }
/* Under the tab strip the well's top edge is the strip's own border, so the
   top corners square off and the top border is dropped to avoid a double line. */
.pane .hl-wrap {
  border-top-left-radius: 0;
  border-top-right-radius: 0;
  border-top-color: transparent;
}
.fld-hero[data-resized="1"] { flex: none !important; }


/* ── Icons ───────────────────────────────────────────────────── */
.sprite { position: absolute; width: 0; height: 0; overflow: hidden; }
.ico {
  width: var(--ico); height: var(--ico);
  flex: none;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.ico.solid { fill: currentColor; stroke: none; }
.ico-lg { width: 26px; height: 26px; }
.ico-xl { width: 34px; height: 34px; stroke-width: 1.3; }
.ico.err { stroke: var(--err); opacity: .8; }

/* Icon-only button. Tooltip comes from data-tip so no visible label is
   needed while the action stays discoverable. */
.ibtn {
  position: relative;
  display: inline-grid; place-items: center;
  width: 32px; height: 32px;
  flex: none;
  color: var(--fg-3);
  text-decoration: none;
  border-radius: var(--r2);
  transition: color var(--t1) var(--e), background var(--t1) var(--e);
}
.ibtn:hover { color: var(--fg); background: var(--raised); }
.ibtn:active { transform: scale(.94); }
.ibtn[hidden] { display: none; }
.ibtn-xs { width: 28px; height: 28px; --ico: 17px; }
/* The prompt block's two actions carry real weight now that the labels are
   gone: bigger glyph, brighter idle colour, and a visible resting surface so
   they read as buttons rather than as ornament. */
/* Recessed icon button for the prompt block: inset controls cut into the
   panel rather than bare glyphs floating on it. */
.fld-actions .ibtn {
  width: 30px; height: 30px;
  --ico: 19px;
  color: var(--fg-2);
  background: var(--well);
  box-shadow: inset 0 0 0 1px var(--line);
}
.fld-actions .ibtn:hover {
  color: var(--fg);
  background: var(--hover);
  box-shadow: inset 0 0 0 1px var(--line-2);
}
/* Header tools stay bare: the balances are the only thing on the bar with
   weight, and a row of boxed icons used to compete with them. */
.topbar-end .ibtn,
.topbar-lock {
  width: 34px; height: 34px;
  --ico: 19px;
}
.fld-actions .ibtn .ico,
.topbar-end .ibtn .ico,
.topbar-lock .ico { stroke-width: 1.9; }
.ibtn-accent { color: var(--a); }
.ibtn-accent:hover { color: var(--a); background: var(--a-dim); }
/* Settled state after a successful clean — reverts on the next edit. */
.ibtn[data-state="done"] { color: var(--ok); }
.ibtn[aria-pressed="false"] { color: var(--fg-4); }
.ibtn[aria-pressed="false"]:hover { color: var(--fg-2); }

[data-tip]::after {
  content: attr(data-tip);
  position: absolute; top: calc(100% + 6px); left: 50%;
  transform: translate(-50%, -3px);
  padding: 4px 7px;
  font-size: var(--fz-2xs); line-height: 1.3;
  white-space: nowrap;
  color: var(--fg-2);
  background: var(--hover);
  border: 1px solid var(--line-2);
  border-radius: var(--r1);
  box-shadow: var(--sh-1);
  opacity: 0; pointer-events: none;
  transition: opacity var(--t1) var(--e), transform var(--t1) var(--e);
  z-index: var(--z-tip);
}
[data-tip]:hover::after { opacity: 1; transform: translate(-50%, 0); transition-delay: 260ms; }
.result-actions [data-tip]::after,
.tray [data-tip]::after,
.fld-actions [data-tip]::after,
.seed [data-tip]::after,
.topbar-end [data-tip]::after,
.topbar-account [data-tip]::after { left: auto; right: 0; transform: translate(0, -3px); }
.fld-actions [data-tip]:hover::after,
.result-actions [data-tip]:hover::after,
.tray [data-tip]:hover::after,
.seed [data-tip]:hover::after,
.topbar-end [data-tip]:hover::after,
.topbar-account [data-tip]:hover::after { transform: translate(0, 0); }

/* ── Buttons ─────────────────────────────────────────────────── */
.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: var(--sp-2);
  padding: 7px 12px;
  font-size: var(--fz-sm); font-weight: var(--fw-medium);
  border-radius: var(--r2);
  white-space: nowrap;
  transition: background var(--t1) var(--e), color var(--t1) var(--e),
    border-color var(--t1) var(--e), transform var(--t1) var(--e), opacity var(--t1) var(--e);
}
.btn:active { transform: scale(0.978); }
.btn[hidden] { display: none; }
.btn-block { width: 100%; }
.btn-sm { padding: 6px 10px; font-size: var(--fz-xs); gap: 6px; --ico: 13px; }
.btn .ico { stroke-width: 1.7; }

.btn-primary { background: var(--a); color: var(--a-ink); font-weight: var(--fw-semibold); }
.btn-primary:hover { background: var(--a-hover); }
.btn-primary:disabled { background: var(--raised); color: var(--fg-4); cursor: not-allowed; transform: none; }

.btn-quiet { color: var(--fg-2); background: var(--raised); border: 1px solid var(--line); }
.btn-quiet:hover { color: var(--fg); background: var(--hover); border-color: var(--line-2); }

.btn-accent { color: var(--a); background: var(--a-dim); border: 1px solid var(--a-line); font-weight: var(--fw-semibold); }
.btn-accent:hover { background: rgba(179, 161, 255, 0.2); }

/* Confirms an irreversible loss; never the default focus. */
.btn-danger { color: var(--err); background: var(--err-dim); border: 1px solid rgba(232, 135, 135, 0.38); font-weight: var(--fw-semibold); }
.btn-danger:hover { color: #f0a5a5; background: rgba(232, 135, 135, 0.16); }

.btn-link {
  font-size: var(--fz-xs); color: var(--fg-3);
  transition: color var(--t1) var(--e);
}
.btn-link:hover { color: var(--a); }
.btn-link.centered { justify-self: center; }

/* ── Generate: the one purchase button on the page ──────────────
   This is the control that spends the user's quota, so it gets the full
   physical treatment: lit from above like every panel (a vertical accent
   gradient and a top lip), a dark hairline that keeps the edge crisp on the
   chrome, and an accent-tinted drop.

   While a job runs, the same button becomes its progress track instead of
   swapping for a different one. Gradients do not interpolate, so the fill
   lives on ::before and fades out; the travelling light lives on ::after and
   fades in. Every state keeps the same four shadows, so those blend as well:
   inset lip, inset ring, outer hairline, glow. */
.btn-go {
  position: relative; isolation: isolate; overflow: hidden;
  height: 46px;
  padding-inline: 44px;
  font-size: var(--fz-md); font-weight: var(--fw-semibold);
  color: var(--a-ink);
  background: var(--a);
  border-radius: var(--r3);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.36),
    inset 0 0 0 1px rgba(255, 255, 255, 0.1),
    0 0 0 1px rgba(10, 8, 20, 0.55),
    0 10px 24px -12px rgba(179, 161, 255, 0.6);
  transition: color var(--t2) var(--e), background-color var(--t2) var(--e),
    box-shadow var(--t2) var(--e), transform var(--t1) var(--e);
}
.btn-go::before, .btn-go::after {
  content: ""; position: absolute; inset: 0; z-index: -1;
  border-radius: inherit; pointer-events: none;
  transition: opacity var(--t2) var(--e), filter var(--t1) var(--e);
}
.btn-go::before { background: linear-gradient(to bottom, #cabdff, var(--a) 55%, #a692fb); }
.btn-go::after {
  /* One soft band, slanted by skew so its edges stay transparent; it rests
     off the left edge, so dropping the animation can't strand it mid-button. */
  right: auto; width: 56%;
  opacity: 0;
  background: linear-gradient(90deg,
    rgba(214, 205, 255, 0) 0%, rgba(214, 205, 255, 0.03) 20%, rgba(214, 205, 255, 0.09) 38%,
    rgba(214, 205, 255, 0.15) 50%,
    rgba(214, 205, 255, 0.09) 62%, rgba(214, 205, 255, 0.03) 80%, rgba(214, 205, 255, 0) 100%);
  transform: translateX(-110%) skewX(-14deg);
}
.btn-go:hover::before { filter: brightness(1.06); }
.btn-go:hover {
  background: var(--a);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.42),
    inset 0 0 0 1px rgba(255, 255, 255, 0.12),
    0 0 0 1px rgba(10, 8, 20, 0.55),
    0 12px 28px -12px rgba(179, 161, 255, 0.78);
}
/* Press = travel down and the glow tucks in, not a generic shrink. */
.btn-go:active {
  transform: translateY(1px);
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.24),
    inset 0 0 0 1px rgba(255, 255, 255, 0.08),
    0 0 0 1px rgba(10, 8, 20, 0.55),
    0 6px 14px -10px rgba(179, 161, 255, 0.45);
}
.btn-go:disabled {
  color: var(--fg-4);
  background: var(--raised);
  transform: none;
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.035),
    inset 0 0 0 1px var(--line-2),
    0 0 0 1px rgba(10, 8, 20, 0),
    0 10px 24px -12px rgba(179, 161, 255, 0);
}
.btn-go:disabled::before { opacity: 0; }
/* The shortcut travels with the label as two small keycaps, so icon, label,
   cost and keys read as one centred group. Glyphs are drawn, not typed: the
   text fonts render ↵ small and low, and ⌘ falls back to whatever the system
   has. Pressed into the fill (dark tint, darker bottom edge) rather than
   stacked on it, so they stay quieter than the label. */
.go-keys { display: inline-flex; gap: 3px; margin-left: 4px; }
.go-keys kbd {
  display: inline-grid; place-items: center;
  min-width: 18px; height: 18px; padding: 0 4px;
  font: var(--fw-semibold) 10px/1 var(--sans); letter-spacing: 0.02em;
  color: rgba(23, 19, 42, 0.5);
  background: rgba(23, 19, 42, 0.05);
  border: 0; border-radius: 5px;
  box-shadow: inset 0 0 0 1px rgba(23, 19, 42, 0.1), inset 0 -1px 0 rgba(23, 19, 42, 0.14);
}
.go-keys .ico { width: 10px; height: 10px; stroke-width: 2.3; }
/* Not on a Mac: the same keys, labelled for a PC keyboard. */
[data-mod="ctrl"] .key-mod .ico { display: none; }
[data-mod="ctrl"] .key-mod::before { content: "Ctrl"; }
/* Can't fire it now (or no keyboard): say nothing rather than grey it out. */
.btn-go:disabled .go-keys { display: none; }
@media (hover: none) { .go-keys { display: none; } }
.btn-go .go-spin { display: none; }

/* Working ≠ unavailable. Grey answers "can I press this?"; the moment after
   pressing, the question is "is it doing anything?". So while the form is
   busy the button stays in the accent family as a dark track, with a spinner
   in place of the sparkle and a slow light band passing through. */
.generation-form[aria-busy="true"] .btn-go:disabled {
  color: var(--a);
  background: color-mix(in srgb, var(--a) 9%, var(--raised));
  box-shadow:
    inset 0 1px 0 rgba(255, 255, 255, 0.04),
    inset 0 0 0 1px var(--a-line),
    0 0 0 1px rgba(10, 8, 20, 0),
    0 10px 24px -12px rgba(179, 161, 255, 0.22);
}
.generation-form[aria-busy="true"] .btn-go:disabled::after {
  opacity: 1;
  animation: goSweep 2.6s cubic-bezier(0.45, 0.05, 0.35, 1) var(--t2) infinite;
}
.generation-form[aria-busy="true"] .btn-go > .ico:first-child { display: none; }
.generation-form[aria-busy="true"] .btn-go .go-spin { display: block; }
.go-spin {
  flex: none; width: 15px; height: 15px;
  border: 2px solid var(--a-dim); border-top-color: currentColor;
  border-radius: 50%;
  animation: goSpin 0.85s linear infinite;
}
/* Elapsed time on the track's right edge, where the shortcut was. */
.go-time {
  position: absolute; right: 14px; top: 50%; translate: 0 -50%;
  font: var(--fw-medium) var(--fz-xs)/1 var(--mono); font-variant-numeric: tabular-nums;
  color: var(--fg-3);
}
.generation-form:not([aria-busy="true"]) .go-time { display: none; }
/* Same shape as sheenSweep: cross in the first half, then wait off-button.
   The loop restarts while the band is out of sight, so there is no seam; the
   old repeating-gradient version ended each cycle with the band mid-button
   and jumped. Offsets are of the band's own 56% width: -110% clears the left
   edge, 190% the right. */
@keyframes goSweep {
  0%   { transform: translateX(-110%) skewX(-14deg); }
  50%  { transform: translateX(190%) skewX(-14deg); }
  100% { transform: translateX(190%) skewX(-14deg); }
}
@keyframes goSpin { to { transform: rotate(1turn); } }
@media (prefers-reduced-motion: reduce) {
  .generation-form[aria-busy="true"] .btn-go:disabled::after { animation: none; opacity: 0; }
  .go-spin { animation-duration: 2.4s; }
}

/* Price rides inside the action that spends it: the same four-point glyph
   as the header gauge names the currency, mono tabular digits keep the
   figure planted while the count changes. On the accent it is a pale chip
   lifted out of the fill, not a grey hole punched into it. */
.cost-tag {
  display: inline-flex; align-items: center; gap: 3px;
  font: var(--fw-semibold) var(--fz-2xs)/1 var(--mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0;
  padding: 4px 8px 4px 6px;
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.3);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.2);
  color: rgba(23, 19, 42, 0.82);
}
.cost-tag .ico {
  width: 11px; height: 11px;
  fill: currentColor; stroke: none;
}
.cost-tag { transition: opacity var(--t2) var(--e); }
.cost-tag[hidden] { display: none; }
/* Re-quoting: the previous figure stays put, faded, until the new one lands. */
.cost-tag[data-stale="true"] { opacity: 0.5; }
.btn-go:disabled .cost-tag { background: var(--well); box-shadow: none; color: var(--fg-4); }

/* Generate and stop share one row, so starting a job never moves anything
   under the pointer: generate narrows and stop slides in at the far edge,
   well away from where the click landed. Hidden, stop keeps its box (at
   zero width) so it can animate out as well as in. */
.go-row {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 0px;
  column-gap: 0;
  transition: grid-template-columns var(--t2) var(--e-out), column-gap var(--t2) var(--e-out);
}
.go-row:has(> .btn-stop:not([hidden])) { grid-template-columns: minmax(0, 1fr) 92px; column-gap: 8px; }
.go-row > .btn-go { width: auto; min-width: 0; }
.btn-stop {
  min-width: 0; height: 46px; padding: 0; overflow: hidden;
  gap: 6px;
  color: var(--fg-2);
  background: var(--raised);
  border: 1px solid var(--line-2);
  border-radius: var(--r3);
  box-shadow: var(--sh-lip);
  transition: opacity var(--t2) var(--e), transform var(--t2) var(--e-out), visibility 0s,
    color var(--t1) var(--e), background var(--t1) var(--e), border-color var(--t1) var(--e);
}
.btn-stop .ico { width: 18px; height: 18px; fill: currentColor; stroke: none; }
@media (hover: hover) {
  .btn-stop:hover { color: var(--err); background: rgba(232, 135, 135, 0.1); border-color: rgba(232, 135, 135, 0.38); }
}
.go-row > .btn-stop[hidden] {
  display: inline-flex; visibility: hidden; pointer-events: none;
  opacity: 0; transform: translateX(12px);
  transition: opacity var(--t1) var(--e), transform var(--t1) var(--e), visibility 0s linear var(--t1);
}

/* ── Checkbox ────────────────────────────────────────────────── */
.check { display: flex; align-items: center; gap: var(--sp-2); cursor: pointer; color: var(--fg-2); font-size: var(--fz-sm); }
.check .sub { font-style: normal; font-size: var(--fz-2xs); color: var(--fg-3); margin-left: 4px; }
.check:hover { color: var(--fg); }
.check input { position: absolute; opacity: 0; width: 0; height: 0; }
.check > span:first-of-type {
  position: relative; flex: none;
  width: 16px; height: 16px;
  background: var(--well);
  border: 1px solid var(--line-2);
  border-radius: 5px;
  transition: background var(--t1) var(--e), border-color var(--t1) var(--e);
}
.check > span:first-of-type::after {
  content: ""; position: absolute; inset: 0;
  background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M3.8 8.4 6.4 11 12.2 5.2' fill='none' stroke='%2317132a' stroke-width='2.1' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center/11px no-repeat;
  opacity: 0; transform: scale(0.6);
  transition: opacity var(--t1) var(--e), transform var(--t2) var(--e-out);
}
.check input:checked + span { background: var(--a); border-color: var(--a); }
.check input:checked + span::after { opacity: 1; transform: none; }
.check input:focus-visible + span { outline: 2px solid var(--a); outline-offset: 2px; }
.check:has(input:disabled) { opacity: 0.45; cursor: not-allowed; }
.check:has(input:disabled):hover { color: var(--fg-2); }
.switches { display: grid; gap: 9px; }

/* ── Segmented ───────────────────────────────────────────────── */
.seg { display: grid; grid-auto-flow: column; gap: 3px; padding: 3px; background: var(--well); border: 1px solid var(--line); border-radius: var(--r2); }
.seg label { position: relative; }
.seg input { position: absolute; opacity: 0; width: 0; height: 0; }
.seg span {
  display: block; padding: 7px 8px; text-align: center;
  font-size: var(--fz-xs); color: var(--fg-3);
  border-radius: var(--r1); cursor: pointer;
  transition: background var(--t1) var(--e), color var(--t1) var(--e);
}
.seg span:hover { color: var(--fg-2); }
.seg input:checked + span { background: var(--raised); color: var(--fg); box-shadow: 0 1px 2px rgba(0,0,0,.3); }
.seg input:focus-visible + span { outline: 2px solid var(--a); outline-offset: 1px; }



/* ── Sliders ─────────────────────────────────────────────────── */
.slider { display: grid; gap: 5px; }
.slider-top { display: flex; align-items: center; justify-content: space-between; }
.pill {
  min-width: 32px; padding: 2px 6px;
  font: var(--fw-medium) var(--fz-2xs)/1.4 var(--mono); font-variant-numeric: tabular-nums;
  text-align: center; color: var(--fg);
  background: var(--well); border: 1px solid var(--line);
  border-radius: 20px;
  transition: color var(--t1) var(--e), border-color var(--t1) var(--e);
}
.slider:focus-within .pill { color: var(--a); border-color: var(--a-line); }

input[type="range"] {
  -webkit-appearance: none; appearance: none;
  width: 100%; height: 18px;
  background: none; border: 0; padding: 0; cursor: pointer;
}
input[type="range"]::-webkit-slider-runnable-track { height: 3px; border-radius: 2px; background: var(--line-2); }
input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 13px; height: 13px; margin-top: -5px;
  border-radius: 50%; background: var(--fg-2);
  transition: background var(--t1) var(--e), transform var(--t1) var(--e-out),
    box-shadow var(--t1) var(--e);
}
input[type="range"]:hover::-webkit-slider-thumb { background: var(--fg); }
input[type="range"]:active::-webkit-slider-thumb,
input[type="range"]:focus-visible::-webkit-slider-thumb {
  background: var(--a); transform: scale(1.2); box-shadow: 0 0 0 5px var(--a-dim);
}
input[type="range"]::-moz-range-track { height: 3px; border-radius: 2px; background: var(--line-2); }
input[type="range"]::-moz-range-thumb { width: 13px; height: 13px; border: 0; border-radius: 50%; background: var(--fg-2); }

/* Scrub bar: for the one value a panel is about (the editor's strength). The
   fill is the value, and label and figure sit inside it, so it reads as one
   object instead of a hairline with a pill floating above. An invisible range
   input covers the bar, so pointer, touch and arrow keys behave natively. */
.scrub {
  position: relative;
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2);
  height: 38px; padding: 0 var(--sp-3);
  background:
    linear-gradient(to right, transparent calc(25% - 1px), var(--line) 0 25%, transparent 0 calc(50% - 1px),
      var(--line) 0 50%, transparent 0 calc(75% - 1px), var(--line) 0 75%, transparent 0) center / 100% 8px no-repeat,
    var(--well);
  border: 1px solid var(--line);
  border-radius: var(--r2);
  overflow: hidden;
  cursor: ew-resize;
  transition: border-color var(--t1) var(--e), box-shadow var(--t1) var(--e);
}
.scrub::before {
  content: "";
  position: absolute; top: 0; bottom: 0; left: 0;
  width: var(--p, 50%);
  background: linear-gradient(to right, rgba(179, 161, 255, 0.06), rgba(179, 161, 255, 0.2));
  box-shadow: inset -2px 0 var(--a);
  pointer-events: none;
}
.scrub:hover { border-color: var(--line-2); }
.scrub:has(input:focus-visible), .scrub:active { border-color: var(--a-line); box-shadow: var(--ring); }
.scrub > span, .scrub > output { position: relative; pointer-events: none; }
.scrub > span { font-size: var(--fz-xs); font-weight: var(--fw-medium); color: var(--fg-2); }
.scrub > output { font: var(--fw-semibold) var(--fz-sm)/1 var(--mono); font-variant-numeric: tabular-nums; color: var(--fg); }
.scrub input[type="range"] { position: absolute; inset: 0; height: 100%; margin: 0; opacity: 0; cursor: ew-resize; }
/* A hairline thumb, so the value under the pointer is the value set. */
.scrub input[type="range"]::-webkit-slider-thumb { width: 2px; height: 38px; }
.scrub input[type="range"]::-moz-range-thumb { width: 2px; height: 38px; }
.scrub:has(input:disabled) { opacity: 0.5; cursor: not-allowed; }
/* A stack of them (the composer's steps / scale / CFG) runs a notch smaller. */
.scrubs { display: grid; gap: 6px; }
.scrub-sm { height: 34px; }
.scrub-sm input[type="range"]::-webkit-slider-thumb { height: 34px; }
.scrub-sm input[type="range"]::-moz-range-thumb { height: 34px; }

/* ── Gate ────────────────────────────────────────────────────── */
/* A modal <dialog> filling the viewport (key-gate.js); the dialog itself is
   the backdrop, so the top layer's own stays clear. */
.gate {
  position: fixed; inset: 0;
  width: 100%; height: 100%; max-width: none; max-height: none;
  margin: 0; border: 0; padding: var(--sp-5);
  color: var(--fg);
  background: rgba(8, 8, 10, 0.9);
  backdrop-filter: blur(10px) saturate(120%);
  opacity: 0;
  transition: opacity var(--t3) var(--e);
}
.gate[open] { display: grid; place-items: center; }
.gate::backdrop { background: transparent; }
body[data-gate="open"] .gate { opacity: 1; }

.gate-card {
  width: min(408px, 100%);
  max-height: calc(100vh - 40px);
  overflow-y: auto;
  padding: var(--sp-5);
  display: grid; gap: var(--sp-4);
  background: var(--raised);
  border: 1px solid var(--line);
  border-radius: var(--r4);
  box-shadow: var(--sh-lip), var(--sh-4);
  transform: translateY(10px) scale(0.985);
  transition: transform var(--t3) var(--e-out);
}
body[data-gate="open"] .gate-card { transform: none; }

.gate-brand { display: flex; align-items: center; gap: var(--sp-3); }
.gate-brand .ico { color: var(--a); }
.gate-brand h1 { font-size: var(--fz-2xl); font-weight: var(--fw-bold); letter-spacing: -0.025em; }
.app-gate-brand { gap: 11px; }
.gate-brand-mark {
  width: 40px;
  height: 40px;
  object-fit: contain;
  border-radius: 11px;
  filter: drop-shadow(0 4px 10px rgba(115, 54, 135, 0.28));
}
.app-gate-brand .brand-title { font-size: 25px; }
.gate-transfer { margin-top: 20px; font-size: var(--fz-xs); color: var(--fg-3); }

.lede { margin-top: calc(var(--sp-4) * -1 + 2px); color: var(--fg-2); font-size: var(--fz-sm); line-height: 1.6; }
.gate-view { display: grid; gap: var(--sp-3); }
.gate-view[hidden] { display: none; }
.note { min-height: 14px; font-size: var(--fz-xs); color: var(--err); }
.note.ok { color: var(--ok); }

@media (max-width: 860px) {
  /* full-bleed sheet instead of a floating card */
  .gate { padding: 0; }
  .gate[open] { align-items: stretch; }
  .gate-card {
    width: 100%; max-height: none; min-height: 100dvh;
    border: 0; border-radius: 0;
    padding: var(--sp-5) var(--sp-4) calc(var(--sp-5) + env(safe-area-inset-bottom));
    align-content: start;
  }
}

/* ── Modal ───────────────────────────────────────────────────── */
/* Every <dialog> panel shares one frame: a fixed head, a body that scrolls and
   a foot that never does. Tool panels keep their price and the action that
   spends it in the foot, so neither scrolls out of view however long the form
   or the result list grows. */
.modal {
  /* Controls recess one notch below --raised, as in drawers and cards. */
  --well: var(--chrome);
  --well-focus: var(--sunken);
  width: min(var(--modal-w, 480px), calc(100% - 32px));
  max-height: 88dvh;
  padding: 0;
  color: var(--fg);
  background: var(--raised);
  border: 1px solid var(--line-2);
  border-radius: var(--r4);
  box-shadow: var(--sh-lip), var(--sh-4);
  overflow: hidden;
}
.modal[open] { display: flex; flex-direction: column; }
.modal::backdrop { background: rgba(8, 8, 10, 0.8); backdrop-filter: blur(4px); }
/* Field rules below set display; [hidden] must still win. */
.modal [hidden] { display: none !important; }
/* A form may wrap head, body and foot so its submit button can sit in the foot. */
.modal-form { display: contents; }
.modal-head {
  flex: none;
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4);
  padding: var(--sp-5) var(--sp-5) var(--sp-4);
}
.modal-head h2 { margin: 0; font-size: var(--fz-lg); font-weight: var(--fw-semibold); letter-spacing: -0.01em; }
.modal-body {
  flex: 1 1 auto; min-height: 0;
  overflow-y: auto; overscroll-behavior: contain;
  padding: 0 var(--sp-5) var(--sp-5);
}
.modal-foot {
  flex: none;
  display: grid; gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-5) var(--sp-4);
  /* --line is tuned against --chrome and vanishes on --raised. */
  border-top: 1px solid var(--line-2);
}
.modal-actions { display: flex; align-items: center; justify-content: flex-end; flex-wrap: wrap; gap: var(--sp-2); margin-left: auto; }
/* .btn-quiet rests on --raised, which is this panel's own colour: lift it a step. */
.modal .btn-quiet { background: var(--hover); border-color: var(--line-2); }
.modal .btn-quiet:hover { color: var(--fg); border-color: var(--fg-4); }

.modal-error { margin: 0; font-size: var(--fz-xs); line-height: 1.6; color: var(--err); overflow-wrap: anywhere; }
.modal-error:empty { display: none; }

/* In-page confirmation (confirm-sheet.js). */
.modal-confirm { --modal-w: 420px; }
.modal-confirm .modal-body { display: grid; gap: var(--sp-3); }
.modal-confirm .modal-foot { border-top: 0; padding-top: 0; }
.confirm-message { font-size: var(--fz-sm); line-height: 1.65; color: var(--fg-2); white-space: pre-line; }
.confirm-items { display: grid; gap: 6px; margin: 0; padding: 0; list-style: none; }
.confirm-items li { display: flex; gap: var(--sp-2); font-size: var(--fz-sm); line-height: 1.55; color: var(--fg-2); }
.confirm-items li::before { content: ""; flex: none; width: 4px; height: 4px; margin-top: 0.65em; border-radius: 50%; background: var(--fg-4); }

@media (max-width: 640px) {
  .modal { margin: auto 0 0; width: 100%; max-width: none; max-height: 90dvh; border-radius: var(--r4) var(--r4) 0 0; border-bottom: 0; }
  .modal-head { padding: 20px 16px 12px; }
  .modal-body { padding: 0 16px 16px; }
  .modal-foot { padding: 12px 16px calc(12px + env(safe-area-inset-bottom)); }
  .modal .ibtn { width: 44px; height: 44px; }
  .modal .btn { min-height: 44px; }
}

/* ── Topbar ──────────────────────────────────────────────────── */
.topbar {
  height: var(--bar-h); flex: none;
  /* 16px puts the mark on the same line as the composer's fields. */
  padding: 0 var(--sp-4);
  display: flex; align-items: center; gap: var(--sp-4);
  border-bottom: 1px solid var(--line);
  background: var(--chrome);
  position: sticky; top: 0; z-index: var(--z-sticky);
  /* Tooltips hang below the bar, but a hidden one must not widen the page
     sideways (the wallet tips are wide and wrap). */
  overflow-x: clip;
}
.brand {
  display: flex; align-items: center; gap: 9px;
  flex: none;
  white-space: nowrap;
}
/* The Koma mark: the generation wave's 3×3 tiles on a dark plate. The lit
   tiles draw the surface (assets/icons.svg#mk-image, -chat, -edit, -tags);
   the favicons are the same drawing without the faintest tiles. */
.brand-mark {
  --m: 30px;
  display: inline-grid; place-items: center;
  width: var(--m); height: var(--m);
  flex: none;
  color: var(--a);
  border-radius: calc(var(--m) * 0.27);
  background: radial-gradient(circle at 28% 18%, #2f2752, #18151f 72%);
  box-shadow: inset 0 0 0 1px rgba(179, 161, 255, 0.2), 0 3px 10px -2px rgba(115, 54, 135, 0.35);
}
.brand-mark svg { width: 74%; height: 74%; fill: currentColor; }
.brand-wordmark {
  display: inline-flex;
  align-items: baseline;
  gap: 8px;
  line-height: 1;
}
.brand-title {
  color: #f7f3ff;
  background: linear-gradient(112deg, #ffffff 4%, #eadcff 48%, #bd93f7 100%);
  background-clip: text;
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  font-family: var(--sans);
  font-size: 22px;
  font-weight: var(--fw-bold);
  letter-spacing: -0.055em;
  line-height: 1;
  text-shadow: 0 0 18px rgba(189, 147, 247, 0.09);
}
.brand-subtitle {
  color: rgba(255, 255, 255, 0.88);
  font: var(--fw-bold) 11px/1 var(--sans);
  letter-spacing: 0.11em;
  transform: translateY(-0.5px);
}

/* The brand is the workspace switcher. Its menu is a popover, so light
   dismiss, Esc and focus return come from the browser; brand-switch.js hangs
   it from the brand's edge when it opens (the values here are the fallback). */
.switcher {
  gap: 10px;
  height: 40px;
  margin-left: -6px;
  padding: 0 10px 0 6px;
  border-radius: var(--r2);
  transition: background var(--t1) var(--e);
}
.switcher:hover, .switcher[aria-expanded="true"] { background: var(--raised); }
.switcher:focus-visible { outline-color: var(--a-line); outline-offset: 0; border-radius: var(--r2); }
/* Open, the raised fill and the flipped caret already say where focus is. */
.switcher[aria-expanded="true"]:focus-visible { outline: none; }
.switcher .sw-caret {
  --ico: 14px;
  margin-left: -2px;
  color: var(--fg-4);
  stroke-width: 2;
  transform: rotate(90deg);
  transition: transform var(--t2) var(--e);
}
.switcher[aria-expanded="true"] .sw-caret { transform: rotate(-90deg); }
.switch-menu {
  position: fixed; inset: auto;
  top: calc(var(--bar-h) - 6px); left: 10px;
  width: min(252px, calc(100vw - 12px));
  margin: 0; padding: 4px;
  color: var(--fg);
  background: var(--raised);
  border: 0;
  border-radius: var(--r3);
  box-shadow: inset 0 0 0 1px var(--line-2), var(--sh-2);
}
.switch-menu:popover-open { display: grid; gap: 1px; }
.switch-menu hr { height: 1px; margin: 3px 6px; border: 0; background: var(--line-2); }
/* One line a row: the surface's mark, its name, a muted line on what it does
   and an end mark. The 6px corner sits concentric in the menu's 10px one. */
.switch-item {
  display: grid;
  grid-template-columns: 18px minmax(0, 1fr) 16px;
  align-items: center;
  gap: 9px;
  width: 100%; height: 34px;
  padding: 0 8px;
  color: var(--fg-2);
  text-align: left;
  text-decoration: none;
  border-radius: 6px;
  transition: background var(--t1) var(--e), color var(--t1) var(--e);
}
.switch-item:not(:disabled):hover, .switch-item:focus-visible { color: var(--fg); background: var(--hover); }
.switch-item:focus-visible { outline: 1px solid var(--a-line); outline-offset: -1px; border-radius: 6px; }
.switch-item[aria-current="page"] { color: var(--fg); }
.switch-item:disabled { opacity: 0.5; cursor: default; }
.sw-glyph { justify-self: center; width: 16px; height: 16px; fill: currentColor; color: var(--fg-3); transition: color var(--t1) var(--e); }
.sw-text { display: flex; align-items: baseline; gap: 8px; min-width: 0; }
.sw-text b { flex: none; font-size: var(--fz-sm); font-weight: var(--fw-medium); }
.sw-text small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--fz-2xs); color: var(--fg-4); }
.sw-end { --ico: 16px; color: var(--fg-4); stroke-width: 1.8; transition: color var(--t1) var(--e); }
.switch-item:not(:disabled, [aria-current]):hover :is(.sw-glyph, .sw-end) { color: var(--fg-3); }
.switch-item:not(:disabled, [aria-current]):hover .sw-glyph { color: var(--fg-2); }
.switch-item[aria-current="page"] :is(.sw-glyph, .sw-end) { color: var(--a); }
.switch-item[aria-current="page"] .sw-end { stroke-width: 2; }
@media (pointer: coarse) { .switch-item { height: 44px; } }

.topbar-menu { display: flex; align-items: center; margin-left: auto; flex: none; }
.topbar-menu-toggle,
.topbar-menu-backdrop,
.topbar-action-label { display: none; }
.topbar-end { display: flex; align-items: center; gap: 6px; flex: none; }
/* Balances and the lock close the bar, after a hairline from the tools. The
   margins leave about 20px of air on each side of it, measured to the glyphs. */
.topbar-account { display: flex; align-items: center; gap: 4px; flex: none; }
.topbar-sep { width: 1px; height: 20px; margin: 0 11px 0 -4px; flex: none; background: var(--line-2); }

.chip {
  padding: 3px 8px;
  font-size: var(--fz-2xs); color: var(--fg-3);
  background: var(--raised);
  border: 1px solid var(--line);
  border-radius: 20px;
}

/* ── Wallet: free allowance and media credits ───────────────── */
/* Two figures on one line, split by a hairline. The free allowance wears its
   provider's mark and a hairline gauge under the figure that empties as the
   pool runs down; credits use a coin, kept neutral so it never reads as the
   amber warning. The labels that sat under the figures made the well busy;
   what each figure is now lives in its tip (and .w-k, for screen readers).
   Both sit in one recessed well, like the rail's view tabs: bare on the bar
   they floated and the eye had nothing to rest them on. The well is pinned to
   --sunken because every bar is --chrome, while --well is re-pointed inside
   modals such as the editor. */
/* Registered so the gauge can ease. Its own name: the scrub bars set --p to a
   percentage, and a <number> registration of --p turned every fill to zero. */
@property --gauge { syntax: "<number>"; inherits: true; initial-value: 0; }
.wallet {
  display: flex; align-items: center; gap: 3px; flex: none;
  padding: 3px;
  background: var(--sunken);
  border: 1px solid var(--line);
  border-radius: var(--r3);
}
/* The same air at both ends and either side of the hairline: 3 + 10. */
.w-sep { flex: none; width: 1px; height: 14px; background: var(--line-2); }
.w-seg {
  --lc: var(--a);
  position: relative;
  display: flex; align-items: center; gap: 7px;
  height: 28px;
  padding: 0 10px;
  color: var(--fg);
  text-align: left;
  border-radius: var(--r2);
  transition: background var(--t1) var(--e);
}
button.w-seg:hover { background: var(--raised); }
.w-ico {
  width: 14px; height: 14px;
  flex: none;
  color: var(--lc);
  fill: currentColor;
  stroke: none;
  transition: color var(--t2) var(--e);
}
.w-credit { --lc: var(--fg-3); }
.w-v {
  font: var(--fw-medium) 13px/1 var(--mono);
  font-variant-numeric: tabular-nums;
  letter-spacing: 0;
  white-space: nowrap;
}
.w-v b { font-weight: inherit; color: var(--fg); transition: color var(--t2) var(--e); }
.w-v i { font-style: normal; font-weight: var(--fw-regular); font-size: 11px; color: var(--fg-4); }
/* The gauge hangs below the figure, so both figures keep one centre line. */
.w-gauge { position: relative; transition: --gauge var(--t3) var(--e-out); }
.w-gauge::after {
  content: "";
  position: absolute; left: 0; right: 0; top: calc(100% + 4px);
  height: 2px;
  border-radius: 1px;
  background: linear-gradient(90deg, var(--lc) calc(var(--gauge) * 100%), var(--line-2) 0);
  transition: opacity var(--t2) var(--e);
}
/* The mark, the gauge and the figure change colour together. */
.w-seg[data-level="low"] { --lc: var(--warn); }
.w-seg[data-level="crit"] { --lc: var(--err); }
.w-seg[data-level="unknown"] { --lc: var(--fg-4); }
.w-seg:is([data-level="low"], [data-level="crit"], [data-level="unknown"]) .w-v b { color: var(--lc); }
/* An unknown pool shows a dash, and an empty track under it read as two. */
.w-seg[data-level="unknown"] .w-gauge::after { opacity: 0; }
/* The tips carry a full sentence (pool, reset time, temp-key budget), so
   they wrap instead of running one long line across the header. */
.w-seg[data-tip]::after {
  width: max-content;
  max-width: min(280px, calc(100vw - 24px));
  white-space: normal;
  line-height: 1.45;
}
/* Quota in flight: pulse the gauge rather than swapping in a spinner. */
.wallet[data-loading="1"] .w-gauge::after { opacity: 1; animation: meterWait 1.1s var(--e) infinite; }
@keyframes meterWait { 50% { opacity: 0.45; } }

@media (max-width: 600px) {
  .topbar .brand-wordmark { display: none; }
  .wallet { gap: 2px; }
  .w-seg { padding: 0 8px; }
  .w-v i { display: none; }
}


/* ── The print ───────────────────────────────────────────────── */
/* A figure sized by its own aspect-ratio so the image never sits in
   dead space, with a real drop shadow so it reads as an object. */
/* Sizing note: the previous version put `aspect-ratio` on .print and
   `max-height:100%` on the img. A percentage height resolving against a
   content-sized parent computes to `none`, so the image escaped its box and
   overflowed. Now JS measures the viewport and publishes --fit-w/--fit-h in
   pixels; the image is bounded by those, and .print simply wraps it. */
.print {
  position: relative;
  margin: 0;
  display: block;
  line-height: 0;
  border-radius: var(--r3);
  overflow: hidden;
  background: var(--sunken);
  box-shadow: var(--print-shadow);
  animation: printIn var(--t3) var(--e-out);
}
@keyframes printIn { from { opacity: 0; transform: translateY(6px) scale(0.988); } }

.print img {
  display: block;
  width: auto; height: auto;
  max-width: var(--fit-w, 100%);
  max-height: var(--fit-h, 70vh);
  cursor: zoom-in;
}

/* Fit-only: the image always scales down to the stage. Clicking opens the
   lightbox for a closer look, which replaces the old 1:1 toggle. */

/* Result information sits below the image instead of obscuring it: the
   model, the file it saves as, then the parameters beside the actions. */
.result-panel {
  display: none;
  gap: var(--sp-2);
  padding: 10px var(--sp-3) var(--sp-3);
  min-width: 0;
  color: var(--fg-2);
  background: color-mix(in srgb, var(--canvas) 78%, var(--chrome));
  border-top: 1px solid var(--line);
}
body[data-state="done"] .result-panel { display: grid; }

.result-copy { display: grid; gap: 2px; min-width: 0; }
.result-title,
.result-file {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.result-title { font: var(--fw-semibold) var(--fz-sm)/1.35 var(--sans); color: var(--fg); }
.result-file { font-size: var(--fz-2xs); line-height: 1.4; color: var(--fg-4); }
/* Keep the actions in place; excess parameters scroll within one row. */
.result-bottom { display: flex; flex-wrap: nowrap; align-items: center; gap: 10px var(--sp-3); min-width: 0; }
.result-params {
  display: flex;
  flex: 1 1 0;
  flex-wrap: nowrap;
  align-items: center;
  gap: 6px;
  max-width: 100%;
  min-width: 0;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  /* Trailing room lets the final tag scroll fully clear of the fade. */
  padding-inline-end: 16px;
  mask-image: linear-gradient(90deg, #000 calc(100% - 16px), transparent);
}
.result-params::-webkit-scrollbar { display: none; }
.result-params:focus-visible, .result-seed:focus-visible { outline-offset: -2px; }
/* Tags, not buttons. They used to wear the buttons' raised fill and outline,
   twelve look-alike boxes in a row with nothing to say which could be
   pressed. Now a soft fill and no outline; the label in quiet sans, the
   figure in mono, words (the sampler, transparency) in sans. */
.result-param {
  flex: none;
  height: 26px;
  padding: 0 9px;
  font: var(--fw-regular) var(--fz-2xs)/26px var(--mono);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--fg-2);
  background: color-mix(in srgb, var(--fg) 3.5%, transparent);
  border: 0;
  border-radius: var(--r1);
}
/* One line box, so the sans label and the mono figure share a baseline. */
.result-param i { font: normal var(--fw-regular) var(--fz-2xs) var(--sans); color: var(--fg-3); }
.result-param[hidden] { display: none; }
#resultSampler, #resultAlpha { font-family: var(--sans); }
/* The seed copies on click: the one figure worth pasting elsewhere. */
.result-seed { position: relative; cursor: pointer; transition: color var(--t1) var(--e), background var(--t1) var(--e); }
.result-seed:not(:disabled):hover { color: var(--fg); background: var(--hover); }
.result-seed:disabled { cursor: default; }

/* Grouped by what they lead to — a new picture, the composer, keeping it —
   with a hairline between groups. Plain icons; only download wears a fill. */
.result-actions { display: flex; flex: none; align-items: center; margin-left: auto; }
.result-group { display: flex; align-items: center; gap: 2px; }
.result-group:not(:has(> :not([hidden]))) { display: none; }
.result-group + .result-group::before {
  content: ""; align-self: center;
  width: 1px; height: 18px; margin: 0 8px;
  background: var(--line-2);
}
.result-actions .ibtn {
  width: 36px;
  height: 36px;
  --ico: 17px;
  color: var(--fg-2);
  background: transparent;
  border: 1px solid transparent;
  box-shadow: none;
}
.result-actions .ibtn:hover { color: var(--fg); background: var(--hover); border-color: var(--line-2); }
/* Download is the one action most results end with: it alone wears the accent. */
.result-actions .ibtn-accent { color: var(--a); background: var(--a-dim); border-color: var(--a-line); }
.result-actions .ibtn-accent:hover { color: var(--fg); background: color-mix(in srgb, var(--a) 26%, transparent); border-color: var(--a); }
.result-actions [data-tip]::after {
  top: auto;
  bottom: calc(100% + 6px);
  transform: translate(0, 3px);
}
.result-actions [data-tip]:hover::after { transform: translate(0, 0); }

/* ══════════════════════════════════════════════════════════════
   Workspace components
   The NAI workspace and the image editor are built from the same
   parts — composer, splitter, stage, history rail — so they live here
   rather than in styles.css, which the chat page does not load.
   ══════════════════════════════════════════════════════════════ */

/* ── Prompt / Negative tabs ──────────────────────────────────── */
/* One block, two panes. The inactive pane keeps its DOM (values, highlight
   mirror, autocomplete bindings) and is only hidden, so switching is free
   and nothing has to be re-attached. */
.promptbox { gap: 0; }
.promptbox-head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--sp-2);
  /* The tab strip sits on this line and the active tab bridges it. */
  border-bottom: 1px solid var(--line);
  padding-bottom: 0;
}
.promptbox .fld-actions { padding-bottom: var(--sp-1); }

.tabs { display: flex; align-items: flex-end; gap: 2px; min-width: 0; }
.tab {
  position: relative;
  display: inline-flex; align-items: center; gap: 6px;
  padding: 6px var(--sp-3) 7px;
  color: var(--fg-3);
  font-size: var(--fz-xs); font-weight: var(--fw-medium);
  letter-spacing: -0.005em;
  border-radius: var(--r2) var(--r2) 0 0;
  white-space: nowrap;
  transition: color var(--t1) var(--e), background var(--t1) var(--e);
}
.tab:hover { color: var(--fg-2); background: var(--sunken); }
/* Underline drawn over the head's border so the active tab reads as attached
   to the pane below it rather than floating above it. */
.tab::after {
  content: "";
  position: absolute; left: 0; right: 0; bottom: -1px;
  height: 2px;
  background: var(--a);
  border-radius: 2px 2px 0 0;
  transform: scaleX(0);
  transition: transform var(--t2) var(--e-out);
}
.tab[aria-selected="true"] { color: var(--fg); font-weight: var(--fw-semibold); }
.tab[aria-selected="true"]::after { transform: scaleX(1); }

/* Reads as a count, not a badge — it must not compete with the label. */
.tab-count {
  font: var(--fw-regular) var(--fz-2xs) var(--mono);
  font-variant-numeric: tabular-nums;
  color: var(--fg-4);
  transition: color var(--t1) var(--e);
}
.tab[aria-selected="true"] .tab-count { color: var(--fg-3); }
/* A non-empty Negative is worth noticing while you are on the Prompt tab:
   it silently shapes every result. */
.tab[aria-selected="false"] .tab-count[data-filled="1"] { color: var(--a); }

.promptbox-body { position: relative; min-height: 0; display: grid; }
.pane { min-height: 0; display: grid; }
.pane[hidden] { display: none; }

/* ── Weight highlighting ─────────────────────────────────────── */
/* A mirror sits behind a transparent textarea. Three things previously broke:
   1) The textarea showed an 11px scrollbar (from the global scrollbar rule)
      while the mirror used overflow:hidden. Different content widths mean
      different line-break points, so every wrapped line drifted. Both now
      reserve the same gutter via `scrollbar-gutter: stable`.
   2) The mirror drew its own border/background and fought the textarea's
      focus ring. The wrapper owns all chrome now; both children are bare.
   3) Sub-pixel font differences: `font: inherit` does not carry every metric,
      so the shared block below sets each one explicitly on both elements. */
/* THE WRAPPER IS THE ONLY SCROLLER.
   The mirror and the textarea live in this one scrolling box, so scrolling
   moves both at once, natively. Earlier versions gave each its own scrollport
   and synced with `mirror.scrollTop = textarea.scrollTop`, which is always a
   frame late (blocks lag behind the caret) and clamps differently at the
   ends (blocks visibly detach at the top). There is no sync code now. */
.hl-wrap {
  position: relative;
  overflow-y: auto;
  overflow-x: hidden;
  overscroll-behavior: contain;
  background: var(--well);
  border: 1px solid var(--line);
  border-radius: var(--r3);
  transition: border-color var(--t1) var(--e), background var(--t1) var(--e),
    box-shadow var(--t1) var(--e);
  /* the handle now resizes the scroller itself */
  resize: vertical;
}
.hl-wrap:hover { border-color: var(--line-2); }
.hl-wrap:focus-within {
  border-color: var(--a-line);
  background: var(--well-focus);
  box-shadow: 0 0 0 3px var(--a-dim);
}

/* Every property here affects text layout and MUST be identical on both.
   Neither element scrolls; both grow to full content height inside the
   wrapper, so their glyphs cannot drift apart at any scroll offset. */
.hl-wrap > textarea,
.hl-wrap > .hl-mirror {
  display: block;
  width: 100%;
  min-width: 0;
  margin: 0;
  border: 0;
  padding: var(--sp-3);
  font-family: var(--sans);
  font-size: var(--fz-md);
  font-weight: var(--fw-regular);
  font-style: normal;
  font-variant: normal;
  line-height: 1.62;
  letter-spacing: -0.006em;
  word-spacing: normal;
  text-indent: 0;
  text-transform: none;
  tab-size: 4;
  white-space: pre-wrap;
  overflow-wrap: break-word;
  word-break: normal;
  overflow: hidden;
  background: none;
  box-sizing: border-box;
}
/* The textarea is in flow and auto-grown by JS to its full content height,
   so it defines the scroll length. It never scrolls internally. */
.hl-wrap > textarea {
  position: relative;
  z-index: 1;
  height: auto;
  color: var(--fg);
  caret-color: var(--a);
  resize: none;
  border-radius: inherit;
}
.hl-wrap > textarea:focus { outline: none; box-shadow: none; background: none; }
.hl-wrap > textarea::placeholder { color: var(--fg-4); }

/* Painted underneath at the same origin. Because it is inside the same
   scrollport, it translates with the content automatically. */
.hl-wrap > .hl-mirror {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 0;
  min-height: 100%;
  color: transparent;
  pointer-events: none;
  user-select: none;
}

.w { border-radius: 2px; }
/* Weight punctuation stays readable but recedes. */
.w-mark { opacity: 0.85; }

/* ── Splitter ────────────────────────────────────────────────── */
/* Zero-width in flow; the grab area is drawn outside its own box so
   it never steals layout space from either pane. */
.splitter {
  position: relative;
  z-index: 15;
  cursor: col-resize;
  touch-action: none;
}
.splitter::before {
  content: "";
  position: absolute;
  top: 0; bottom: 0; left: -4px; right: -4px;
}
.splitter::after {
  content: "";
  position: absolute;
  top: 0; bottom: 0; left: -1px;
  width: 2px;
  background: var(--a);
  opacity: 0;
  transition: opacity var(--t2) var(--e);
}
.splitter:hover::after { opacity: 0.5; }
[data-drag="on"] .splitter::after { opacity: 1; }
[data-drag="on"] { cursor: col-resize; user-select: none; }
[data-drag="on"] .viewport { pointer-events: none; }

.composer {
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  border-right: 1px solid var(--line);
  background: var(--chrome);
  min-height: 0;
}
/* Flex column, not grid: the prompt field needs to absorb leftover height
   so the primary input is the biggest thing in the panel.
   Sections sit 20 apart and a heading 8 above its control: at 16 against the
   12 inside a section, "next section" and "same section" looked alike, and
   at 24 the column fell apart into islands. */
.composer-scroll {
  --section: 20px;
  overflow-y: auto;
  overscroll-behavior: contain;
  /* A soft edge where content scrolls under the generate button. At rest it
     lands on the bottom padding and the empty lower half of the last row. */
  mask-image: linear-gradient(to bottom, #000 calc(100% - var(--sp-4)), transparent);
  padding: var(--sp-4) var(--sp-4) var(--sp-2);
  display: flex;
  flex-direction: column;
  gap: var(--section);
}
.composer-scroll > * { flex: none; }
.composer-scroll > .fld:not(.fld-hero) { gap: var(--sp-2); }
.composer-heading {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  color: var(--fg-2);
  font-size: var(--fz-xs);
  font-weight: var(--fw-semibold);
  letter-spacing: 0.01em;
}
.section-ico {
  width: 16px;
  height: 16px;
  color: var(--fg-3);
  fill: currentColor;
  stroke: none;
  transition: color var(--t1) var(--e), transform var(--t2) var(--e-out);
}
.composer-heading .section-ico { color: var(--a); }
.composer-foot {
  padding: var(--sp-3) var(--sp-4) var(--sp-4);
  display: grid; gap: 6px;
  /* No rule: it never lined up with the result panel's edge across the
     splitter. The scroll area fades out under it instead. */
  background: linear-gradient(to top, var(--chrome) 60%, transparent);
}

/* Size tiers share a segmented row. */
.tiers { grid-template-columns: repeat(3, minmax(0, 1fr)); grid-auto-flow: row; }
.tiers span {
  display: flex; flex-wrap: wrap; align-items: baseline; justify-content: center;
  gap: 0 6px;
  padding: 6px 6px;
}
.tiers b { font-weight: var(--fw-medium); white-space: nowrap; }

/* ratio cards: the glyph is a true scale model of the output */
.ratios { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--sp-2); }
.ratio {
  display: grid; place-items: center; gap: 6px;
  padding: 8px 4px 7px;
  color: var(--fg-4);
  background: var(--raised);
  border: 1px solid var(--line);
  border-radius: var(--r2);
  transition: color var(--t1) var(--e), background var(--t1) var(--e),
    border-color var(--t1) var(--e), transform var(--t1) var(--e);
}
.ratio-box { display: grid; place-items: center; height: 20px; }
.ratio i { display: block; border: 1.5px solid currentColor; border-radius: 2.5px; transition: border-color var(--t1) var(--e); }
.ratio b { font: var(--fw-medium) var(--fz-2xs)/1 var(--mono); letter-spacing: 0; }
/* 自定义 has no fixed shape, so its glyph is an outline still to be drawn. */
.ratio[data-custom] i { border-style: dashed; }
.ratio:hover { color: var(--fg-2); border-color: var(--line-2); transform: translateY(-1px); }
.ratio[aria-checked="true"] { color: var(--a); background: var(--a-dim); border-color: var(--a-line); }
/* Radio-backed cards (the editor's modes) read the same as the role=radio ones. */
.ratio input { position: absolute; opacity: 0; width: 0; height: 0; }
.ratio .ico { width: 18px; height: 18px; }
.ratio:has(input:checked) { color: var(--a); background: var(--a-dim); border-color: var(--a-line); }
.ratio:has(input:focus-visible) { outline: 2px solid var(--a); outline-offset: 2px; }
.ratio:has(input:disabled) { opacity: 0.4; cursor: not-allowed; transform: none; }


/* drawer */
/* Containers that sit on --raised re-point the well pair so controls inside
   them recess by the same single notch they do out on the panel. Without this
   a select in the 参数 drawer sat 5.5 below its own background — the "internals
   look far too dark" report. */
.drawer,
.gate-card,
.picker-card,
.dropzone-card,
.toast {
  --well: #19191c;         /* ΔL* 2.6 below --raised */
  --well-focus: #17171a;
}

.drawer { border: 1px solid var(--line); border-radius: var(--r3); background: var(--raised); overflow: hidden; }
.drawer summary {
  display: flex; align-items: center; gap: var(--sp-2);
  min-height: 40px;
  padding: 8px var(--sp-3);
  font-size: var(--fz-xs); font-weight: var(--fw-medium); color: var(--fg-2);
  cursor: pointer; list-style: none;
  transition: background var(--t1) var(--e), color var(--t1) var(--e), box-shadow var(--t1) var(--e);
}
.drawer summary::-webkit-details-marker { display: none; }
.drawer summary:hover { background: var(--hover); color: var(--fg); }
.drawer summary:hover .section-ico { color: var(--fg-2); transform: scale(1.04); }
.drawer[open] > summary {
  color: var(--fg);
  background: color-mix(in srgb, var(--hover) 56%, transparent);
  box-shadow: inset 0 -1px var(--line);
}
.drawer[open] > summary .section-ico { color: var(--a); }
.caret {
  margin-left: auto; width: 7px; height: 7px; flex: none;
  border-right: 1.4px solid var(--fg-3); border-bottom: 1.4px solid var(--fg-3);
  transform: rotate(45deg) translate(-1px, -1px);
  transition: transform var(--t2) var(--e);
}
.drawer[open] .caret { transform: rotate(225deg) translate(-1px, -1px); }
.badge {
  padding: 2px 7px;
  font: var(--fw-medium) var(--fz-2xs) var(--mono);
  color: var(--a); background: var(--a-dim);
  border-radius: 20px;
}
.badge[hidden] { display: none; }
.drawer-body {
  display: grid; gap: var(--sp-3);
  padding: var(--sp-1) var(--sp-3) var(--sp-3);
  animation: drawerIn var(--t2) var(--e-out);
}
@keyframes drawerIn { from { opacity: 0; transform: translateY(-6px); } }
/* Where the browser can animate to height:auto, the drawer slides open and
   shut instead of snapping, so whatever sits around it (the prompt box above
   absorbs leftover height) eases into place rather than jumping. */
@supports selector(::details-content) {
  .drawer { interpolate-size: allow-keywords; }
  .drawer::details-content {
    block-size: 0;
    overflow-y: clip;
    transition: block-size var(--t2) var(--e), content-visibility var(--t2) allow-discrete;
  }
  .drawer[open]::details-content { block-size: auto; }
}
@media (prefers-reduced-motion: reduce) {
  .drawer::details-content { transition: none; }
}

/* In the composer the drawers are sections like 模型 and 画幅, only foldable:
   the same heading type and accent glyph, no card around them. A hairline
   marks where "what to make" ends and "how" begins. */
.composer .drawer {
  --well: inherit;
  --well-focus: inherit;
  border: 0;
  border-radius: 0;
  background: none;
  overflow: visible;
}
/* Folded, 参数 and 设置 are one block of two rows close under the hairline. */
.composer .drawer + .drawer { margin-top: calc(-1 * var(--section)); }
.composer .drawer[open] + .drawer { margin-top: calc(var(--sp-3) - var(--section)); }
.composer .drawer:first-of-type { padding-top: var(--sp-1); border-top: 1px solid var(--line); }
.composer .drawer > summary {
  gap: 7px;
  min-height: 32px;
  margin: 0 calc(-1 * var(--sp-2));
  /* The right inset puts the caret over the select chevrons above it. */
  padding: 4px 20px 4px var(--sp-2);
  font-weight: var(--fw-semibold);
  letter-spacing: 0.01em;
  border-radius: var(--r2);
}
.composer .drawer > summary .section-ico,
.composer .drawer > summary:hover .section-ico { color: var(--a); transform: none; }
.composer .drawer[open] > summary { background: none; box-shadow: none; }
.composer .drawer[open] > summary:hover { background: var(--hover); }
.composer .drawer-body { padding: var(--sp-2) 0 var(--sp-1); }
/* With the scroll padding, the fade under the last open drawer is empty space. */
.composer .drawer:last-of-type > .drawer-body { padding-bottom: var(--sp-2); }
/* A tap leaves :hover stuck on touch screens; the heading should not stay lit. */
@media (hover: none) { .composer .drawer > summary:hover { background: none; } }

.seed { display: grid; grid-template-columns: 1fr auto; gap: var(--sp-2); }

/* ── Stage ───────────────────────────────────────────────────── */
.stage {
  container: image-stage / inline-size;
  position: relative;
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  min-width: 0;
  min-height: 0;
  background: var(--chrome);
}
@container image-stage (max-width: 480px) {
  .result-bottom { flex-flow: column nowrap; align-items: stretch; }
  .result-params { flex: none; }
}

/* The mat. Slight vignette gives depth without drawing attention. */
.viewport {
  position: relative;
  display: grid;
  place-items: center;
  padding: var(--sp-5);
  min-height: 0;
  overflow: hidden;
  /* The darkest surface in the app. Everything else rose to make the
     panels legible; this went the other way so the print still reads
     as the one bright object on screen. */
  background:
    radial-gradient(120% 90% at 50% 0%, rgba(255,255,255,0.022), transparent 62%),
    var(--canvas);
}

/* blank / error copy block */
.blank { display: grid; justify-items: center; gap: var(--sp-3); text-align: center; max-width: 280px; color: var(--fg-4); }
.blank-s { font-size: var(--fz-sm); line-height: 1.6; color: var(--fg-3); }
/* Same drawn keycaps as the generate button, cut for the dark stage, and
   following the platform's modifier. No keyboard, no keys: touch gets its
   own sentence. */
.blank-keys { display: inline-flex; gap: 3px; margin-left: 2px; vertical-align: -1px; }
.blank-keys kbd {
  display: inline-grid; place-items: center;
  min-width: 20px; height: 20px; padding: 0 5px;
  font: var(--fw-semibold) 10.5px/1 var(--sans); letter-spacing: 0.02em;
  color: var(--fg-2);
  background: var(--raised);
  border: 0; border-radius: 5px;
  box-shadow: inset 0 0 0 1px var(--line-2), inset 0 -1px 0 rgba(0, 0, 0, 0.35);
}
.blank-keys .ico { width: 11px; height: 11px; stroke-width: 2.2; }
.blank-touch { display: none; }
.blank-drop { margin-top: -2px; font-size: var(--fz-xs); color: var(--fg-4); }
@media (hover: none) {
  .blank-keyed { display: none; }
  .blank-touch { display: block; }
}
.blank .btn { margin-top: var(--sp-1); }

/* ── Empty stage: brand head and the favourites shelf ─────────── */
/* The shelf keeps one shape whatever the count: two justified rows that
   favourites take in order, the rest left as ghosts (docs/lab/empty-stage.html,
   K). Ghosts are outlines lit from the top that dissolve downward, nearly
   unfilled, each fainter than the one before, so the shelf trails off instead
   of ending in a row of solid boxes. */
.home { display: grid; width: var(--home-w, min(800px, 100%)); }
.home-top {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-5);
  padding-bottom: 20px; border-bottom: 1px solid var(--line);
}
.home-lock { display: inline-flex; align-items: center; gap: 14px; min-width: 0; }
.home-mark { width: 40px; height: 40px; flex: none; overflow: visible; fill: var(--a); }
.home-mark rect { transform-box: fill-box; transform-origin: center; animation: homeBreath 3.6s var(--e) infinite; animation-delay: calc(var(--d) * 160ms); }
@keyframes homeBreath { 0%, 100% { transform: scale(1); } 40% { transform: scale(0.8); } }
.home-word { display: inline-flex; align-items: baseline; gap: 10px; line-height: 1; }
.home-word .brand-title { font-size: 26px; letter-spacing: -0.01em; }
.home-word .brand-subtitle { font-size: 13px; }
.home-hint { display: grid; justify-items: end; gap: 2px; text-align: right; font-size: var(--fz-sm); line-height: 1.6; color: var(--fg-3); }
.home-hint small { font-size: var(--fz-xs); color: var(--fg-4); }

.home-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-4); margin: 20px 0 12px; }
.home-head h2 { display: inline-flex; align-items: center; gap: 6px; margin: 0; font-size: var(--fz-xs); font-weight: var(--fw-regular); color: var(--fg-3); }
.home-head h2 .ico { --ico: 14px; color: var(--fg-4); }
.home-n { font-size: var(--fz-xs); color: var(--fg-4); }
.home-all { display: inline-flex; align-items: center; gap: 2px; font-size: var(--fz-xs); color: var(--fg-4); }
.home-all .ico { --ico: 13px; }
.home-all:hover { color: var(--fg-2); }
.home-n[hidden], .home-all[hidden], .home-empty[hidden] { display: none; }

.home-shelf { position: relative; }
.home-grid { display: grid; gap: var(--home-gap, 12px); }
.home-row { display: flex; gap: var(--home-gap, 12px); }
.home-fav, .home-ghost { flex: none; width: var(--w); aspect-ratio: var(--ar); border-radius: var(--r2); }
.home-fav {
  background: var(--thumb) center / cover no-repeat;
  opacity: 0.9;
  box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.05), 0 10px 24px -14px rgba(0, 0, 0, 0.9);
  transition: opacity var(--t1) var(--e), transform var(--t1) var(--e);
}
.home-fav:hover { opacity: 1; transform: translateY(-2px); }
.home-ghost {
  position: relative;
  opacity: max(0.14, calc(1 - var(--g) * 0.13));
  background: linear-gradient(to bottom, rgba(255, 255, 255, 0.028), rgba(255, 255, 255, 0) 70%);
}
.home-ghost::before {
  content: ""; position: absolute; inset: 0; padding: 1px; border-radius: inherit;
  background: linear-gradient(to bottom, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.025) 55%, rgba(255, 255, 255, 0));
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); mask-composite: exclude;
}
/* With nothing saved the whole shelf is ghosts: fade it out to every edge
   from the message, and clear it behind the words. */
.home-grid.is-empty {
  --edge: radial-gradient(closest-side, #000 35%, transparent);
  --hole: radial-gradient(ellipse 52% 30% at 50% 50%, transparent 55%, #000 100%);
  -webkit-mask-image: var(--edge), var(--hole); -webkit-mask-composite: source-in;
  mask-image: var(--edge), var(--hole); mask-composite: intersect;
}
.home-grid.is-empty .home-ghost { opacity: 1; }
.home-empty {
  position: absolute; inset: 0;
  display: grid; place-content: center; justify-items: center; gap: 4px;
  text-align: center; pointer-events: none;
}
.home-empty .ico { --ico: 22px; margin-bottom: 8px; color: var(--a); }
.home-empty b { font-size: var(--fz-sm); font-weight: var(--fw-medium); color: var(--fg-2); }
.home-empty span { font-size: var(--fz-xs); color: var(--fg-4); }

/* A narrow stage (a phone, or a desktop squeezed by both side panels). */
@container image-stage (max-width: 600px) {
  .home-top { gap: var(--sp-3); padding-bottom: 12px; }
  .home-lock { gap: 10px; }
  .home-mark { width: 26px; height: 26px; }
  .home-word { gap: 7px; }
  .home-word .brand-title { font-size: 19px; }
  .home-word .brand-subtitle { font-size: 9.5px; }
  .home-hint { font-size: var(--fz-xs); color: var(--fg-4); }
  .home-hint small { display: none; }
  .home-head { margin: 12px 0 8px; }
  .home-empty { gap: 2px; }
  .home-empty .ico { --ico: 16px; margin-bottom: 2px; }
  .home-empty b { font-size: var(--fz-xs); }
  .home-empty span { font-size: var(--fz-2xs); }
}
@media (prefers-reduced-motion: reduce) { .home-mark rect { animation: none; } }

/* ── History rail (right) ────────────────────────────────────── */
.gallery {
  display: flex;
  flex-direction: column;
  border-left: 1px solid var(--line);
  background: var(--chrome);
  overflow: hidden;
  min-width: 0;
}
/* The rail keeps a compact heading for its count and clear action. */
.gallery-head {
  display: flex; align-items: center; justify-content: space-between; gap: var(--sp-2);
  height: var(--head-h); flex: none;
  padding: 0 var(--sp-2) 0 var(--sp-3);
  border-bottom: 1px solid var(--line);
  font-size: var(--fz-2xs); color: var(--fg-4);
  overflow: hidden;
}
.gallery-head > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gallery-title { font-weight: var(--fw-medium); color: var(--fg-3); }
.gallery-title b { margin-left: 4px; font-weight: var(--fw-medium); color: var(--fg-4); }

.gallery-head .btn-link { flex: none; }
.gallery-head .btn-link[hidden] { display: none; }

/* vertical stack */
.tray {
  flex: 1 1 auto;
  display: flex; flex-direction: column; gap: 6px;
  overflow-y: auto; overscroll-behavior: contain;
  padding: var(--sp-2);
  min-height: 0;
}
.tray:empty { flex: none; padding: 0; }
.tray-empty {
  flex: none;
  display: grid; gap: var(--sp-3);
  padding: var(--sp-2);
  font-size: var(--fz-2xs); line-height: 1.55; color: var(--fg-3); text-align: center;
}
.tray-empty[hidden] { display: none; }
.tray-empty span { display: block; color: var(--fg-4); }
/* Ghost tiles in the rail's own grid, fading out down the column. */
.tray-ghosts {
  display: grid; gap: 6px;
  mask-image: linear-gradient(to bottom, #000, transparent);
}
.tray-ghosts i {
  aspect-ratio: 1;
  border-radius: var(--r1);
  background: rgba(255, 255, 255, 0.018);
  box-shadow: inset 0 0 0 1px var(--line);
}
.tray-ghosts i:nth-child(4) { display: none; }

.tile {
  position: relative; flex: none;
  width: 100%;
  /* A uniform square grid. Letting each tile take the aspect of its image made
     a portrait thumbnail nearly three times the height of a landscape one, so
     the rail scanned as randomly sized blocks and the eye had no column to
     follow. The shape of a result is already legible from the crop. */
  aspect-ratio: 1 / 1;
  border-radius: var(--r1);
  overflow: hidden;
  /* Letterbox bars: the same mat the stage uses, so a thumbnail reads as a
     small print rather than as a control with a picture in it. */
  background: var(--canvas);
  box-shadow: 0 0 0 1px var(--line);
  transition: box-shadow var(--t1) var(--e), transform var(--t2) var(--e-out);
  /* skip paint work for tiles scrolled out of the rail */
  content-visibility: auto;
  contain-intrinsic-size: auto 140px;
}
/* The whole picture, letterboxed. `cover` filled the square but cropped the
   composition — on a portrait result that meant losing the top and bottom of
   the very thing being judged. `contain` keeps the grid uniform AND the frame
   intact; the unused area is the tile's own dark backing. */
.tile img { position: relative; display: block; width: 100%; height: 100%; object-fit: contain; }
/* The letterbox bars carry a dim, blurred copy of the same picture, so a
   portrait in a square tile sits in its own colour instead of two black slabs. */
.tile:not(.tile-pending)::before {
  content: "";
  position: absolute; inset: -12px;
  background: var(--thumb, none) center / cover no-repeat;
  filter: blur(10px) brightness(0.45) saturate(1.15);
}
.tile:hover { transform: translateY(-1px); box-shadow: 0 0 0 1px var(--line-2), 0 6px 14px -4px rgba(0,0,0,.6); }
.tile[aria-current="true"] { box-shadow: 0 0 0 1.5px var(--a); }

/* Placeholder for the image still being generated. Carries the same rings as
   the stage frame so the rail and the viewport read as one pending operation. */
.tile-pending {
  background:
    linear-gradient(170deg, rgba(255, 255, 255, 0.026), transparent 62%),
    var(--canvas);
  box-shadow: 0 0 0 1px var(--a-line);
  content-visibility: visible;   /* never skip paint: it is the active element */
}
/* Currently on screen: matches how a selected thumbnail reads. */
.tile-pending[aria-current="true"] { box-shadow: 0 0 0 1.5px var(--a); }
/* Browsing elsewhere — invite the click back. */
.tile-pending[aria-current="false"]:hover {
  transform: translateY(-1px);
  box-shadow: 0 0 0 1.5px var(--a), 0 6px 14px -4px rgba(0, 0, 0, 0.6);
}
.tile-x {
  position: absolute; top: 3px; right: 3px;
  width: 17px; height: 17px;
  display: grid; place-items: center;
  font-size: var(--fz-2xs); line-height: 1; color: #fff;
  background: rgba(0,0,0,0.6);
  backdrop-filter: blur(3px);
  border-radius: 5px;
  opacity: 0;
  transition: opacity var(--t1) var(--e), background var(--t1) var(--e);
}
.tile:hover .tile-x, .tile-x:focus-visible { opacity: 1; }
.tile-x:hover { background: var(--err); color: var(--a-ink); }

/* ── Loading ──────────────────────────────────────────────────
   A frame waiting for a generated picture carries the tile wave (tile-wave.js):
   rounded tiles swelling along the diagonal on the frame's dark base, which
   then open onto the result as a crest. It is indeterminate on purpose — the
   upstream is one POST and one blob with no step callback, so anything that
   filled toward an end would be fiction. Real progress stays in the text and
   the batch counter.

   Two smaller pieces remain for places that are not a picture being made:

     .bar     small and bright — a sweeping-light progress bar on the top edge
              (the editor's overlay on the picture it is changing)
     .sheen   large and dark — a wide band of light travelling across the frame
              (attachment and gallery placeholders, the editor's overlay)

   Strength is inverse to area, and neither layer raises the base colour. Two
   earlier attempts got this wrong in opposite directions: five bright rings from
   three origins (nine independent motion parameters, none subordinate, read as
   noise), then a flat fill over the whole frame (visible only by lifting every
   pixel 10.8 dL* above the panel, read as a pale slab). A travelling highlight
   over an untouched dark base is what leaves the frame looking deep. */
/* No panel of its own: the wave sits straight on the mat and fades out before
   the border. The print's edge and shadow come in with the crest, so by the
   time the picture is whole it has the same frame the print will. */
.skeleton,
[data-wave] {
  background: transparent;
  box-shadow: var(--print-shadow-off);
  transition: box-shadow 1.2s var(--e);
}
[data-wave="crest"] { box-shadow: var(--print-shadow); }
.skeleton {
  display: grid; place-items: center;
  width: var(--sk-w, 320px);
  height: var(--sk-h, 460px);
  animation: none;
}
.tile-wave {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  pointer-events: none;
}

/* Sweeping-light progress bar, pinned to the top edge of the frame. */
.bar {
  position: absolute; top: 0; left: 0; right: 0;
  height: 3px;
  overflow: hidden;
  background: rgba(255, 255, 255, 0.05);
  border-top-left-radius: inherit;
  border-top-right-radius: inherit;
  contain: paint;
  pointer-events: none;
  z-index: 2;
}
/* Two full periods of the pattern, translated by exactly one — the loop is
   seamless, so there is no dead frame at the wrap and no stutter at the seam.
   Only transform animates, so it stays on the compositor. */
.bar i {
  position: absolute; top: 0; bottom: 0; left: 0;
  width: 200%;
  background: repeating-linear-gradient(
    90deg,
    transparent 0%,
    rgba(179, 161, 255, 0.30) 12%,
    var(--bar-ink) 25%,
    rgba(179, 161, 255, 0.30) 38%,
    transparent 50%
  );
  will-change: transform;
  animation: barSweep 1.5s linear infinite;
}
@keyframes barSweep {
  from { transform: translate3d(0, 0, 0); }
  to   { transform: translate3d(-50%, 0, 0); }
}

/* A wide, soft band of light travelling across the frame at an angle.

   This replaces a flat fill that covered the whole frame. That fill had to raise
   the base colour to be visible at all — every one of the frame's ~429k pixels
   sat 10.8 dL* above the panel, with chroma pushed from 4 to 18 — so it read as
   a pale lilac slab. Depth needs the base to stay dark and only a highlight to
   move: here the band peaks around L* 21 where it passes, while everywhere else
   keeps the panel's own L* 8.41.

   Seamless by construction: the pattern repeats every 100% of the frame width
   and travels exactly one period, so a new band enters as the previous one
   leaves. The first sweep this project shipped parked its band off-panel for 38%
   of the cycle (1.10s of dead air); there is no dead phase here.

   Feathering comes from wide gradient ramps rather than a blur filter — this
   element is four frames wide, and blur cost scales with area. */
.sheen {
  position: absolute; inset: 0;
  overflow: hidden;
  border-radius: inherit;
  contain: paint;
  pointer-events: none;
}
.sheen::before {
  content: "";
  position: absolute;
  /* Oversized vertically so the tilt never exposes a corner. */
  top: -60%; bottom: -60%;
  /* 56%, not 40%: only the middle of the gradient is bright enough to register,
     so a 40% band lit the frame for just 19% of the cycle and the pass read as a
     blink rather than a sweep. */
  left: 0; width: 56%;
  /* ONE band. This was a repeating gradient four periods wide, which meant four
     highlights were on the frame at once, marching in file — a queue, not a
     sweep. A sheen is a single pass. */
  background: linear-gradient(
    90deg,
    rgba(214, 205, 255, 0) 0%,
    rgba(214, 205, 255, 0.02) 18%,
    rgba(214, 205, 255, 0.07) 38%,
    var(--sheen-ink) 50%,
    rgba(214, 205, 255, 0.07) 62%,
    rgba(214, 205, 255, 0.02) 82%,
    rgba(214, 205, 255, 0) 100%
  );
  will-change: transform;
  animation: sheenSweep 2.8s cubic-bezier(0.45, 0.05, 0.35, 1) infinite;
}
/* Crosses in the first 42% (~1.2s) and then waits out the rest off-frame, so
   each pass is a distinct event with quiet between. The earlier note here said
   an eased loop "stutters at the seam" — true only for a continuous repeat,
   where the curve restarts mid-frame. With the band parked off-frame during the
   gap there is no seam on screen, which is what lets it be eased at all: it
   accelerates in, is quickest across the middle, and decelerates out, the way a
   highlight tracks across a surface being tilted.

   Offsets are percentages of the band's own 40% width: -120% clears the left
   edge, 300% clears the right. */
@keyframes sheenSweep {
  0%   { transform: translate3d(-110%, 0, 0) rotate(14deg); }
  52%  { transform: translate3d(190%, 0, 0) rotate(14deg); }
  100% { transform: translate3d(190%, 0, 0) rotate(14deg); }
}

/* .print zeroes line-height to kill the <img> baseline gap, and the skeleton
   reuses .print — so both lines here inherited line-height:0, collapsed to zero
   height, and their glyphs overlapped by 9px while `gap` did nothing (it was
   spacing zero-height boxes). Restore a line box locally; .print keeps its own
   rule for the image case. */
/* The clock and the parameter line are announced, not shown (.sr-only on the
   wrapper), so they need no visual treatment. Their former centring, sizing and
   contrast rules are gone with them — including the line-height override that
   used to be needed here, since .print zeroes line-height for the <img> case
   and a clipped 1px box has no line box to collapse. */


/* ── Lightbox ────────────────────────────────────────────────── */
.lightbox {
  position: fixed; inset: 0; z-index: var(--z-scrim);
  --lb-pad: var(--sp-6);
  display: grid; place-items: center;
  padding: var(--lb-pad);
  background: rgba(6, 6, 8, 0.95);
  backdrop-filter: blur(4px);
  opacity: 0;
  transition: opacity var(--t2) var(--e);
  cursor: zoom-out;
}
.lightbox[hidden] { display: none; }
body[data-zoom="on"] .lightbox { opacity: 1; }
/* Bounded by the viewport itself: the grid's auto row is content-sized, so a
   percentage max-height resolved to none and tall images ran off-screen. */
.lightbox img {
  width: auto; height: auto;
  max-width: calc(100vw - 2 * var(--lb-pad));
  max-height: calc(100dvh - 2 * var(--lb-pad));
  object-fit: contain;
  border-radius: var(--r2);
  box-shadow: 0 30px 90px -20px rgba(0,0,0,.9);
  transform: scale(0.97);
  transition: transform var(--t3) var(--e-out);
}
body[data-zoom="on"] .lightbox img { transform: none; }
.lightbox-x { position: fixed; top: var(--sp-4); right: var(--sp-4); cursor: pointer; }


/* ── Toasts ────────────────────────────────────────────────────
   One column of equal-width notices, newest on top, in the window's
   top-right corner: the first sits inside the bar, centred on its height,
   and the rest hang below it. The editor's own stack sits in the same place
   over its dialog. A 2px line along the bottom drains with the time
   left; toast.js dismisses the notice when that animation ends, so pausing
   it on hover or focus pauses dismissal. */
.toasts, .editor-toasts {
  position: fixed; z-index: var(--z-toast);
  top: max(4px, calc((var(--bar-h) - 48px) / 2)); right: 16px;
  display: grid;
  width: min(380px, calc(100vw - 32px));
  pointer-events: none;
}
/* Each notice sits in a row that opens and closes its own height, so the
   rest of the stack glides rather than jumps. Clipped only while moving,
   or the shadow would be cut. */
.toast-slot { display: grid; grid-template-rows: 1fr; }
.toast-slot.in { overflow: clip; animation: toastRow 280ms var(--e-out); }
.toast-slot.out { overflow: clip; animation: toastRow 240ms var(--e) reverse forwards; }
@keyframes toastRow { from { grid-template-rows: 0fr; } }
.toast {
  --tc: var(--a);
  position: relative; overflow: hidden;
  display: flex; align-items: center; gap: 12px;
  min-height: 48px;
  margin-bottom: 8px; padding: 0 16px;
  font-size: 14px; line-height: 1.5;
  color: var(--fg);
  background: var(--raised);
  border-radius: var(--r2);
  box-shadow: inset 0 0 0 1px var(--line-2), var(--sh-2);
  pointer-events: auto;
}
.toast[data-tone="ok"] { --tc: var(--ok); }
.toast[data-tone="warn"] { --tc: var(--warn); }
.toast[data-tone="err"] { --tc: var(--err); }
.toast:has(.toast-action, .toast-x) { padding-right: 8px; }
.toast-ico { --ico: 18px; color: var(--tc); stroke-width: 1.7; }
.toast-text {
  flex: 1; min-width: 0; padding: 12px 0;
  overflow-wrap: anywhere;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden;
}
.toast-n { margin-left: 6px; font: var(--fw-medium) 12px/1 var(--mono); color: var(--fg-3); }
.toast-action {
  flex: none; height: 32px; padding: 0 10px;
  font-weight: var(--fw-medium); color: var(--a);
  border-radius: var(--r1);
  transition: background var(--t1) var(--e);
}
.toast-action:hover { background: var(--a-dim); }
.toast-action:disabled { opacity: 0.5; cursor: default; }
.toast-x {
  flex: none; display: grid; place-items: center;
  width: 32px; height: 32px; margin-left: -4px;
  color: var(--fg-4); border-radius: var(--r1);
  transition: color var(--t1) var(--e), background var(--t1) var(--e);
}
.toast-action + .toast-x { margin-left: -6px; }
.toast-x:hover { color: var(--fg); background: var(--hover); }
.toast-x-ico { --ico: 16px; }
.toast-timer {
  position: absolute; left: 0; right: 0; bottom: 0; height: 2px;
  background: var(--tc); opacity: 0.5;
  transform-origin: left;
  animation: toastDrain var(--dwell) linear forwards;
}
@keyframes toastDrain { to { transform: scaleX(0); } }
:is(.toasts, .editor-toasts):is(:hover, :focus-within) .toast-timer { animation-play-state: paused; }
.toast-slot.in > .toast { animation: toastIn 280ms var(--e-out); }
.toast-slot.out > .toast { animation: toastOut 200ms var(--e) forwards; }
.toast.bump { animation: toastBump 260ms var(--e-out); }
@keyframes toastIn { from { opacity: 0; transform: translateX(20px); } }
@keyframes toastOut { to { opacity: 0; transform: translateX(20px); } }
@keyframes toastBump { 40% { transform: scale(1.02); } }
/* Phones: full width under the bar, so a long sentence still fits. */
@media (max-width: 860px) {
  .toasts, .editor-toasts { left: 12px; right: 12px; width: auto; }
}
/* The timer is also the dismissal clock: it keeps its real length when
   motion is reduced (the rule below would cut it to nothing), but is not
   drawn. */
@media (prefers-reduced-motion: reduce) {
  .toast-timer { animation-duration: var(--dwell) !important; opacity: 0; }
}


@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
  /* The rule above freezes every animation on its first frame, so that frame has
     to stand on its own. A travelling band fails that by default: frame one has
     the band off-panel, which is the dead rectangle this project already shipped
     once. Replace the sweep with a single static band across the frame — the
     highlight still reads, it just does not move. */
  .bar i { animation: none !important; transform: none !important; width: 100% !important; }
  .sheen::before {
    animation: none !important;
    top: 0 !important; bottom: 0 !important;
    left: 0 !important; width: 100% !important;
    transform: none !important;
    background: linear-gradient(
      100deg,
      rgba(214, 205, 255, 0) 34%,
      rgba(214, 205, 255, 0.05) 44%,
      var(--sheen-ink) 50%,
      rgba(214, 205, 255, 0.05) 56%,
      rgba(214, 205, 255, 0) 66%
    ) !important;
  }
}
.sr-only { position:absolute; width:1px; height:1px; padding:0; margin:-1px; overflow:hidden; clip-path:inset(50%); white-space:nowrap; }
