/* ─────────────────────────────────────────────────────────────
   base.css — the foundation. This file is NOT going away.

   Tailwind v4 keeps tokens, base rules and keyframes in CSS; only
   component styling becomes utilities. So the migration's target was
   "legacy.css reaches zero lines", not "no CSS at all" — legacy.css is
   there now, and this file is the CSS that was never going anywhere.
   What lives here stays here:

     · the token substrate (:root + html[data-theme="light"]) that
       styles/tailwind.css aliases into Tailwind's namespace via
       @theme inline — delete these and every utility resolves to
       nothing;
     · the hand-rolled preflight. Tailwind's own preflight is
       deliberately NOT imported: this stylesheet is written against
       browser defaults, so the UA button/form neutralisation below is
       the app's reset;
     · the scroll model, overscroll containment, and the
       chrome-isn't-selectable / content-is policy;
     · platform chrome — Electron drag regions, Window Controls Overlay
       and env(titlebar-area-*), PWA standalone;
     · global policies: reduced motion, reduced transparency, and the
       @supports progressive enhancement.

   Concatenated ahead of legacy.css by src/server/frontend-build.ts.
   ───────────────────────────────────────────────────────────── */

/* ─────────────────────────────────────────────────────────────
   Open Session
   Devin-inspired dark theme
   ───────────────────────────────────────────────────────────── */

:root {
	/* Conductor-style dark: lifted neutral charcoal rather than near-black — the
	   deepest surface sits ~#1b, so code blocks/terminals (which stay near-black)
	   read as inset wells instead of blending in. */
	/* Elevation ramp. Four tiers, each one step further from the page than the
	   last, and — this is the part that has to hold — stepping the SAME
	   direction in both themes: lighter than --bg in dark, darker than --bg in
	   light. A surface's tier is what makes it read as lifted, so a token may
	   not swap tiers between themes or the same stack reads raised in one and
	   inset in the other.

	     L0  --bg                                   the page
	     L1  --bg-raised / --sidebar-bg / --topbar-bg   chrome behind the content
	     L2  --bg-panel                             cards, bubbles, tool rows
	     L3  --control-surface                      controls/popups on top

	   --bg-hover/--bg-active continue past L3 (see below), so the whole chain
	   is monotonic: bg < raised < panel < control < hover < active in dark, and
	   the same order descending in light. */
	--bg: #1c1c1c;
	--bg-raised: #222222;
	/* The sidebar and the chrome band around it. It has its own token because
	   light puts it a hair off the page rather than a full L1 step down, which
	   is what the Codex sidebar does and what the app copies. In dark it is
	   L1 like every other piece of chrome, so the two stay the same surface. */
	--sidebar-bg: var(--bg-raised);
	/* Desktop sidebar material: a translucent charcoal wash over the native
	   window/backdrop — the sidebar surface at partial opacity. */
	--sidebar-material: rgba(34, 34, 34, 0.66);
	--sidebar-material-sheen: rgba(255, 255, 255, 0.035);
	/* The cast on the content column's left edge, which gives the chrome beside
	   it a little depth. Dark has none, for the reason --auth-card-edge drops
	   its cast below: black ink on a near-black column lands as a smudge rather
	   than a lift. It measured one grey step over 7px, which is not depth, and
	   the seam is already saying where the sidebar ends. Light keeps it: see
	   the light block. */
	--content-edge-shadow: none;
	/* Level with the sidebar in both themes: a top bar that sits between the
	   page and the sidebar reads as a third surface across one row of chrome. */
	--topbar-bg: var(--sidebar-bg);
	/* Clean control chips (header Share, PR chip), the composer, popups: the top
	   of the ramp, so they read as lifted off whatever they sit on, with a
	   whisper of shadow. Codex-style. */
	--control-surface: #292929;
	/* The plate under a control you *press*. In dark it is the control surface
	   above — lighter than the page, so the fill is what lifts it. Light is the
	   composer's case at chip scale, so it takes the composer's answer; see the
	   light block. */
	--button-surface: var(--control-surface);
	/* Phone navigation controls float over live content, the way an iOS toolbar
	   item does: a translucent plate that takes its colour from whatever passes
	   underneath it, rather than a paper sticker laid on top. The fill used to be
	   `--bg`, the same colour as the page it floats over, so nothing but the cast
	   shadow said the control was there at all, and the bar read as stickers on a
	   page instead of glass over one.

	   Dark mode keeps a defined edge, because even thinned the fill lands close
	   to the page; light mode takes the native toolbar treatment below, where an
	   even cast around the control replaces that edge. The inset line leading
	   each shadow is the specular top edge every glass control on iOS carries. It
	   is what gives the plate a thickness, and it is the one part of the look a
	   blur alone cannot produce. */
	--mobile-header-control-border: var(--border);
	--mobile-header-control-surface: color-mix(in srgb, var(--bg) 68%, transparent);
	--mobile-header-control-blur: blur(20px) saturate(1.7);
	/* The docked tab strip follows iOS: the selected tab is the bright plate and
	   the rest sit a step under it. Both are OPAQUE on purpose. iOS paints its
	   pills over a thick material, so the transcript passing behind them is a
	   change in tone, never legible text; a thinned fill here put running code
	   through the tab labels. The blur stays for the pill's edge, not to see
	   through it. */
	--mobile-tab-surface-selected: var(--bg-hover);
	--mobile-tab-surface: var(--bg-raised);
	--mobile-header-control-shadow:
		inset 0 1px 0 rgba(255, 255, 255, 0.07),
		0 1px 3px -1px rgba(0, 0, 0, 0.06),
		0 4px 10px -4px rgba(0, 0, 0, 0.09);
	/* The segmented control's knob: the one plate that sits in a WELL rather
	   than on the page, so it gets its own pair of tokens.

	   It is deliberately ABOVE the ramp's last step (--bg-active #353535). A
	   cast shadow on a near-black track does nothing, so in dark the fill is
	   the only thing that can say "raised" — and the knob's own track is
	   already --bg-hover, so anything at or below that tier reads as a hole
	   cut in the group rather than a plate lifted out of it. That is what
	   --control-surface (#292929, BELOW the track) looked like. The value is
	   iOS's own: its selected segment sits around #636366 on a #2C2C2E track,
	   which is a bigger step than an app this flat would arrive at on its own,
	   and it is the step that actually reads as pressed.

	   The edge is a hair LIGHTER than the fill, not darker — a dark line
	   around a light plate is a drawn outline, while a light one reads as the
	   top of the plate catching the light. Light mode wants neither; see the
	   light block. */
	--segmented-knob-surface: #5a5a5c;
	--segmented-knob-edge: #6a6a6c;
	/* Floating popups — hover cards, menus, context menus. Same L3 tier as the
	   controls above, and in dark that is all it needs: lighter than the page,
	   so the fill alone reads as lifted and the hairline is just an edge. Light
	   is the case that needs its own pair, for the reason the composer states
	   below — see the light block. */
	--popup-surface: var(--control-surface);
	--popup-ring: var(--smooth-ring-color);
	/* A modal's edge, which is not a menu's. A menu floats over the page it
	   came from, so the fill's step above --bg is what says it is in front. A
	   dialog floats over a scrim that has already darkened everything around
	   it: the fill's step shrinks to nothing, the cast shadow falls on ink it
	   cannot darken further, and the shared hairline — 14% ink, painted at
	   half strength by the ring utility — landed FAINTER than the dividers
	   inside the dialog's own rows. A box whose interior lines are more
	   definite than its outline reads as unfinished. 26% is the value that
	   puts the outline level with those dividers (--border) once the utility
	   has halved it. Light needs the firmer line for the opposite reason, the
	   one --popup-surface gives below: a white box on white. */
	--dialog-ring: color-mix(in srgb, var(--text) 26%, var(--bg));
	/* Glass. The same fill, thinned so the page shows through, over a blur
	   heavy enough that what shows through is colour and shape rather than
	   somebody else's sentence — a popup you can read a row title through is
	   not translucent, it is broken. Written once here rather than per theme:
	   --popup-surface re-resolves under html[data-theme], so the mix follows.
	   Both are overridden back to opaque at the bottom of this block's file
	   for browsers without backdrop-filter and for reduced transparency. */
	--popup-glass: color-mix(in srgb, var(--popup-surface) 72%, transparent);
	--popup-blur: blur(20px) saturate(1.8);
	/* The command palette's own glass. Denser than a menu's for two reasons: it
	   is a big empty box rather than a strip of rows — the same reason
	   --composer-surface and --popup-surface go white in light instead of
	   following the ramp down — and it is the one popup with a dimming backdrop
	   underneath, which composites THROUGH the fill and drags a 72% mix down to
	   a grey slab. At 88% the page's colour and shape still read through it and
	   the surface stays paper. */
	--palette-glass: color-mix(in srgb, var(--popup-surface) 88%, transparent);
	/* The composer's own fill + edge. In dark it IS --control-surface: lighter
	   than the page, so the box already reads as lifted. Light needs its own
	   pair — see the light block. */
	--composer-surface: var(--control-surface);
	--composer-border: var(--border);
	--composer-shadow: 0 2px 6px rgba(0, 0, 0, 0.16), 0 14px 36px rgba(0, 0, 0, 0.2);
	/* The composer box's corner. It lives here, not on .composer, because the
	   expand/collapse morph is driven from Composer.tsx — Motion writes the
	   radius inline, so it has to be able to READ the resting value rather than
	   carry a second copy of it. Same corner as the surfaces physically
	   attached to the composer (the queue flap, the agents flap), which read
	   this token rather than a `rounded-*` step of their own so the seam stays
	   flush. The composer is deliberately the roundest surface on a session
	   page — it is the one thing you touch — so it sits a step above the
	   panels around it instead of matching them. */
	--composer-radius: calc(28px * var(--rf));
	/* The sign-in card's edge. On the dark marketing artwork, a cast has
	   nothing to fall on: black ink on a near-black picture becomes a grey
	   smudge rather than a lift. A hairline holds the glass shape instead, and
	   nothing darkens the backdrop. Light keeps the cast: see the light block. */
	--auth-card-edge: 0 0 0 1px var(--smooth-ring-color);
	--bg-panel: #262626;
	--bg-hover: #2e2e2e;
	--bg-active: #353535;
	/* What a mermaid diagram is drawn on. A token rather than a value in the
	   `.md-mermaid` rule because the media lightbox paints the same surface
	   under a diagram it has opened: the chart's ink is themed to the page, so
	   on the viewer's near-black scrim a light-theme one would be unreadable. */
	--diagram-canvas: #0c0c10;
	/* The session's right-hand panel: the fill of the column itself, and the
	   plate its sections sit on. The column takes a hair off the page rather
	   than a whole tier, the argument --sidebar-bg already makes at column
	   scale: what separates it is the seam, so the fill only has to be a shade
	   off the page. The plate takes a smaller step than --bg-panel because five
	   or six of them stack down one narrow column, and at the page's full plate
	   strength the panel reads as a pile of blocks rather than a list of
	   sections. PANEL_SHELL paints the first and re-points --bg-panel at the
	   second, so every plate, chip and pill inside the column steps together. */
	--panel-surface: #1f1f1f;
	--panel-plate: #242424;
	/* The plate a settings group sits on. In dark it is L1, like any other card:
	   six units up from the page is already a whisper, and taking any less off
	   would leave a group with no edge at all. Light is the case this token
	   exists for. See the light block. */
	--settings-plate: var(--bg-raised);
	/* Interaction washes. --bg-hover/--bg-active above are absolute *surfaces*:
	   used as a hover they land as a ~9% step on --bg but barely 2% on
	   --bg-panel, so the same token gives a heavy wash on the composer and an
	   invisible one inside panels and popups. These are the *effect* — a
	   translucent ink that composites to the same strength on whatever surface
	   it sits on. Use --hover for hover/focus, --hover-strong for pressed and
	   selected. The surface tokens stay for real surfaces (segmented-control
	   tracks, chips) and for the scroll-fade box-shadow masks, which need an
	   opaque colour to hide what slides under them. */
	--hover: rgba(255, 255, 255, 0.07);
	--hover-strong: rgba(255, 255, 255, 0.11);
	/* A SELECTED row: the session being viewed, the active tool, the open PR.
	   Quieter than the pressed wash on purpose. A press is momentary and has to
	   be felt under the pointer, while a selection persists and only has to be
	   findable, and it never carries the row alone: a selected row also takes
	   full-strength ink in a title its neighbours write dim. At pressed
	   strength a column of open rows read as grey plates dropped on the list.
	   The sidebar's hover is a layer over this, so pointing at the selected row
	   still lifts it. */
	--selected: rgba(255, 255, 255, 0.08);
	/* Opaque equivalents of the sidebar row's translucent washes. Avatar rings
	   cannot inherit the pixels behind them, so these reproduce the exact sRGB
	   composites for resting selection, hover, and both layers together. */
	--sidebar-row-hover: color-mix(in srgb, #ffffff 7%, var(--sidebar-bg));
	--sidebar-row-selected: color-mix(in srgb, #ffffff 8%, var(--sidebar-bg));
	--sidebar-row-selected-hover: color-mix(
		in srgb,
		#ffffff 14.44%,
		var(--sidebar-bg)
	);
	/* The one interaction fill that is NOT a wash: an action chip floating over
	   a row (the pin/archive cluster). A chip is a LID, not a tint — what is
	   behind it is the row it covers, so a translucent one shows that row's own
	   wash, and whatever the row still has under that spot, through the glyph.
	   So it is opaque, and the background bleeds into its COLOUR instead of
	   through its alpha: the hover wash pre-composited onto the sidebar's own
	   surface, which is the same value the wash resolves to there. One surface
	   it cannot follow is the vibrancy sidebar — see the material block for
	   what was measured there and why it still takes this. */
	--row-chip: color-mix(in srgb, #ffffff 11%, var(--sidebar-bg));
	--row-chip-hover: color-mix(in srgb, #ffffff 7%, var(--row-chip));
	--border: #333333;
	--border-strong: #464646;
	/* The seams of the app chrome: the hairline under a top bar, the sidebar
	   edge, the right panel edge, a pane header's underline. These divide two
	   surfaces that are already different, so the line only has to mark where
	   one ends. At full --border it draws a frame around the whole top of the
	   window instead, and three of them meeting in one corner is the loudest
	   thing on a quiet screen. A wash of the same ink says the same thing more
	   quietly. Keep --border itself for the edge of a control, where the line
	   IS the shape. */
	--divider: color-mix(in srgb, var(--border) 55%, transparent);
	/* A block's own outline, where the fill under it already says where the
	   block is and the line only has to close its shape: the settings plate.
	   Lighter than --divider, which still has to read as a separation between
	   two surfaces. Anything that has to be seen as a rule takes --divider or
	   --border; this one is meant to be felt and not looked at. */
	--divider-soft: color-mix(in srgb, var(--border) 34%, transparent);
	/* The scrollbar thumb, on the platforms that draw a permanent one (see the
	   scrollbar block further down). Translucent rather than a picked grey: the
	   scrollers sit on four different fills, and an alpha keeps the thumb in the
	   same relationship to each of them. */
	--scrollbar-thumb: rgba(255, 255, 255, 0.13);
	--text: #e9e9e9;
	--text-dim: #a2a2a2;
	--text-faint: #767676;
	/* The no-JS fallback brand mark: ink on the page — near-white on dark,
	   near-black on light. The selected accent overrides it below. It is a FILL
	   colour (the
	   send disc, an active tab underline, a checked switch) and a full-strength
	   EMPHASIS colour (an active icon against a dim one). It is deliberately NOT
	   a link colour: at body contrast, words wearing it are indistinguishable
	   from the prose around them, so inline affordances take --link. Red goes
	   back to meaning only "this failed".

	   var(--text) rather than a literal, so it tracks the ink ramp. This is not
	   a new idea in this file — the `ink` Button variant, the light-theme Create
	   button and .pr-bar-btn-solid already paint ink-on-paper, each with its own
	   comment explaining that the red accent shouted. */
	--accent: var(--text);
	/* What sits ON an --accent fill: its inverse. Was a hardcoded #fff at every
	   call site, which is white-on-white the moment the accent becomes ink. */
	--on-accent: var(--bg);
	/* Hover for a filled --accent control. While the accent is ink, the only
	   direction with anywhere to go is toward the page: brightness-110 on a
	   near-white or near-black plate is invisible. */
	--accent-hover: color-mix(in srgb, var(--accent) 86%, var(--bg));
	/* Achromatic wash for a surface that is TINTED by the accent: a badge, an
	   "on" toggle, a focus ring. Pitched above --hover-strong (0.11) so it reads
	   as a tint of its own and not as a heavier hover. Once the accent went ink
	   this and the interaction washes became the same ink, so they were told
	   apart only by opacity; the split is by meaning instead. The three
	   achromatic ones each say a different thing: --hover is the pointer,
	   --hover-strong the press, --selected what is open. "Needs you" is
	   deliberately in none of them — it takes no fill at all, so it can sit on a
	   selected row without either state losing. Don't spell a hover with this
	   token — that is what the collision was. */
	--accent-soft: rgba(255, 255, 255, 0.16);
	/* Inline affordances: a link in an agent answer, a PR title that opens. The
	   accent cannot do this job once it is ink, and red never should have. */
	--link: var(--blue);
	--tooltip-bg: #303030;
	--tooltip-fg: #eeeeee;
	--tooltip-ring: rgba(255, 255, 255, 0.1);
	/* Status green and red. Both are held at the lightness they had and taken
	   out to the chroma their hue can carry, which is what separates them from
	   the greyed GitHub-era pair they started as. Lightness is the channel that
	   is spoken for: green is the fill under CheckStatusIcon's white knockout
	   and red is the fill under the sidebar's white attention count, so moving
	   it moves a contrast ratio. Chroma costs nothing there. */
	--green: #00bc52;
	--green-soft: rgba(0, 188, 82, 0.14);
	--yellow: #d29922;
	/* The wash behind a --yellow label: the "Running" pill. In-progress work is
	   yellow throughout — the pulsing run indicators and the "In progress"
	   lane — because green here means a finished, reviewable result. */
	--yellow-soft: rgba(210, 153, 34, 0.14);
	/* Yellow as a SURFACE rather than as ink. `--yellow` is picked to read as
	   text, so in light mode it is a dark ochre (#9a6700) — mixed down to the
	   5-15% of a tint it loses its hue and lands as warm grey, which is what
	   made the note surfaces look washed out. This one is picked to survive
	   that dilution. Use it for note fills and hatches; keep `--yellow` for
	   the label sitting on them. On dark the ink is already chromatic enough
	   to do both jobs. */
	--yellow-tint: #d29922;
	--blue: #58a6ff;
	--blue-soft: rgba(88, 166, 255, 0.13);
	--red: #fc4a47;
	--red-soft: rgba(252, 74, 71, 0.12);
	--purple: #a371f7;
	/* Tool-family hues (transcript timeline icons + detail accents) */
	--tool-run: #3fb950;
	--tool-file: #58a6ff;
	--tool-edit: #e3b341;
	--tool-find: #f0883e;
	--tool-web: #39c5cf;
	--tool-agent: #f778ba;
	--tool-mcp: #a371f7;
	--tool-skill: #2dd4bf;
	/* The well a tool call's input and output sit in. It is a code colour
	   scheme (GitHub's), not a step of the chrome ramp, which is why it is its
	   own group rather than another --bg-*: the surface has to read as sunk
	   under whatever card the tool block lands on, in both themes.
	   NB the light value is the one `.markdown pre` moved away from — see the
	   note above that rule — so a snippet here reads a touch raised in light
	   where a pasted one reads sunk. Kept as-is by the migration; changing it
	   is a visual call, not a mechanical one. */
	--code-well: #0d0f13;
	--code-well-line: rgba(255, 255, 255, 0.06);
	--code-well-ink: #b6bcc8;
	--code-well-gutter: #565d6b;
	/* Source chips — one tinted pill per session origin (slack / linear / ask).
	   Tokens rather than a rule per source because the theme is the only thing
	   about them that moves: these are light-on-dark, and light needs the same
	   hue re-toned dark-on-light to stay legible. A token re-resolves under
	   html[data-theme] on its own, where the pair of rules it replaced had to
	   state every chip twice. The neutral origins (cli, automation) use
	   --bg-active/--text-dim and need no token of their own. */
	--chip-slack-bg: rgba(74, 21, 75, 0.55);
	--chip-slack-fg: #d9a8db;
	--chip-linear-bg: rgba(94, 106, 210, 0.25);
	--chip-linear-fg: #9da6ee;
	--chip-ask-bg: rgba(210, 153, 34, 0.18);
	--chip-ask-fg: #e3b341;
	--mono: "SF Mono", ui-monospace, "JetBrains Mono", Menlo, Consolas, monospace;
	--sans: -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", sans-serif;
	/* The web keeps desktop's compact metrics, while a phone promotes every role
	   one rung. These are real custom properties, rather than values written
	   directly into Tailwind's @theme block, so the same utility can follow the
	   viewport without a `phone:` override at every call site. Raw markdown and
	   inherited UI text read these too, which keeps the whole scale together. */
	--type-root: 14px;
	--type-meta: 11px;
	--type-label: 13px;
	--type-item-title: 14px;
	--type-body: 15px;
	--type-dialog-title: 17px;
	--type-section-title: 19px;
	--type-page-title: 24px;
	--type-stat: 28px;
	--type-stat-line: 32px;
	--type-input-phone: 16px;
	/* The weight a title takes: a page's own name, and the headings under it.
	   It is a token rather than a fixed utility because the right weight is not
	   the same in both themes. Light text on a dark page blooms, putting more
	   ink on screen than the same stem does as dark text on white, which is the
	   argument the light ramp below already makes about its greys, pointed the
	   other way. So dark carries the lighter cut and light steps back up to
	   semibold, and a heading is written `font-title` once instead of pairing a
	   weight with a theme override at every call site. */
	--title-weight: 500;
	--radius: 14px;
	/* Squircle corner treatment (Chrome 139+ `corner-shape`). A superellipse
	   hugs the corner tighter than a circular arc at the same radius, so a
	   browser that can draw one carries a larger radius multiplier than the
	   circular fallback. --rf scales every px radius in the app and --cs picks
	   the corner shape, so the pair flipped in the `@supports` block below is
	   the whole switch.

	   `corner-shape` is Chromium-only (WebKit has not shipped it), so Safari,
	   and the installed iOS PWA with it, draws circular corners and has to
	   reach the same shape with radius alone. It cannot land on it exactly,
	   but the closest radius is the one that cuts the same amount off the
	   corner: a circular arc removes 0.215r² of its corner square against a
	   squircle's 0.073r², about three times as much, so it has to be 1.75x
	   smaller to read at the same weight. 1.35 / 1.75 is the 0.771 here, and
	   1.75 is the ratio commonly used for squircle classes
	   (8px round against 14px squircle).

	   This was 1, the radius each corner is authored with, and that is what
	   made Safari read visibly rounder than Chrome. It was 0.85 before that,
	   and why THAT was wrong is worth keeping: it reached only controls while
	   large overlays kept the full 1.35, so a Safari palette was a very round
	   shell full of squarer buttons, the hierarchy inverted rather than
	   degraded. Every radius in the app now derives from this one token, so a
	   shell and the controls inside it move together whatever it is set to. */
	--rf: 0.771;
	--cs: round;
	/* The same correction for a radius written as a PERCENTAGE of its box,
	   which --rf cannot scale because the percentage is already relative: the
	   32% avatar tile, and nothing else. Percentages are authored at the value
	   Chrome squircles, so this is 1 there and carries the whole 1/1.75 shrink
	   in Safari. Derived from --rf rather than declared twice, so the
	   `@supports` flip below stays the only place either one moves. */
	--rp: calc(var(--rf) / 1.35);
	/* Session column width — messages, work blocks, composer, ask cards all share
	   it so the conversation reads as one column. */
	--session-col: 780px;
	/* Shared height for the desktop sidebar, session header and PR strip. */
	--desktop-header-h: 48px;
	/* Header height grows by the iOS status-bar inset when installed as a PWA
     (black-translucent draws the webview under the clock/notch). The bar spends
     8px on its own top pad, leaving 40px for MOBILE_BAR_SEGMENT and the settings
     mark in lib/app-header-classes.ts. Move the two together. */
	--header-h: calc(48px + env(safe-area-inset-top, 0px));
}

/* Above the phone breakpoint the top bar isn't rendered at all, so nothing is
   offset by it and the token collapses to zero. It stays a token rather than a
   rule on an element because everything that clears the bar reads it from
   somewhere else in the tree: the workspace panel anchors its ≤920px
   overlay to it, the sidebar masks its scroll by it, and the
   detail pane spends it as --pane-header-h. */
@media (min-width: 721px) {
	:root {
		--header-h: 0px;
	}
}

/* ── Light theme ─────────────────────────────────────────────
   Conductor-inspired neutral gray ramp. Applied when the user picks Light, or picks
   System and the OS is light — see src/frontend/lib/theme.ts + the pre-paint
   script in index.html, which set html[data-theme]. The whole UI is
   variable-driven, so redefining these ~15 core tokens re-themes it; the handful
   of hardcoded light-on-translucent spots get explicit overrides just below. */
html[data-theme="light"] {
	/* Same four tiers as :root, stepping down from white instead of up from
	   charcoal (L0 #ffffff → L1 -9 → L2 -15 → L3 -20). The steps are wider than
	   dark's 6/10/13 because ink on white reads lighter than the same step of
	   white on charcoal. */
	--bg: #ffffff;
	--bg-raised: #f6f6f6;
	/* The one L1 surface that does not take the full step: half of it, five
	   units off white. The sidebar is a whole column of chrome, not a card, and
	   at column scale the L1 grey reads as a panel bolted to the side of the
	   page. What separates it is the seam and the shadow on the workspace's left
	   edge, so the fill only has to be a shade cooler than the page, not a tier
	   below it. Codex sets its sidebar two units off white, which was the first
	   answer here and came out too faint to hold the column at all; this is the
	   step in between. --topbar-bg follows it, and --bg-raised keeps the full
	   step for the cards and panels that need a fill of their own. */
	--sidebar-bg: #fafafa;
	--sidebar-material: rgba(250, 250, 250, 0.72);
	--sidebar-material-sheen: rgba(255, 255, 255, 0.42);
	/* See :root. On paper the cast reads, and it is half of what holds the
	   chrome column apart from the content beside it. */
	--content-edge-shadow: -1px 0 8px rgba(0, 0, 0, 0.05);
	/* Ink on white holds less of the page than white on charcoal does, so a
	   title needs the heavier cut here to read as firmly as the lighter one
	   does in dark. Same reasoning as the grey steps above, applied to stems
	   instead of surfaces. */
	--title-weight: 600;
	/* L3 + a barely-there shadow — the clean lifted chip from the Codex
	   reference. It used to be #ffffff, byte-identical to --bg, so a chip, the
	   composer and the mention popup had no surface of their own in light; and
	   it sat at the BOTTOM of the ramp here while it sits at the top in dark.
	   #ebebeb lands between --bg-panel (#f0f0f0) and --bg-hover (#e9e9e9), which
	   is where dark's #292929 sits between #262626 and #2e2e2e. */
	--control-surface: #ebebeb;
	/* A button is the composer's exception at chip scale. The ramp above is
	   right for a *filled* chip — a status pill, an option row, an input well —
	   which has nothing but its fill to read against the page. A button also
	   carries a hairline and a cast shadow, and those already say "raised"; add
	   the grey fill on top and the control reads as tinted rather than lifted,
	   which is the disabled signal again. So it goes paper and lets the shadow
	   do the lifting, exactly as the composer and the popups below do. */
	--button-surface: #ffffff;
	/* Native light toolbar controls do not draw a dark outline around their
	   glass. A zero-offset cast with a little spread lifts the whole silhouette
	   evenly, including its top edge, without turning that edge into a border. */
	--mobile-header-control-border: transparent;
	/* Thinner than dark's, because what shows through here is a bright page and
	   the plate has to stay lighter than it to keep reading as glass rather than
	   as a grey wash over the words underneath. */
	--mobile-header-control-surface: color-mix(in srgb, var(--bg) 62%, transparent);
	/* Light mode reaches the same two weights from the other side: the selected
	   tab is pure white, the rest the grey a step under it. */
	--mobile-tab-surface-selected: var(--bg);
	--mobile-tab-surface: var(--bg-hover);
	--mobile-header-control-shadow:
		inset 0 1px 0 rgba(255, 255, 255, 0.6),
		0 0 14px 1px rgba(0, 0, 0, 0.1);
	/* The segmented knob goes paper here, like every other raised control, and
	   wears no edge at all. A --border hairline is darker than the --bg-hover
	   track it lies on, so it reads as an outline drawn around the option
	   rather than a plate raised out of the group, and the top of that outline
	   has no shadow to justify it. White on a light grey track plus the cast
	   shadow is enough; dark cannot do that and takes a fill and an edge of
	   its own in the block above. */
	--segmented-knob-surface: var(--button-surface);
	--segmented-knob-edge: transparent;
	/* The composer is the exception to the ramp above, and it has to be. The
	   ramp steps AWAY from the page — darker in light — which is right for a
	   small chip read against its neighbours, but the composer is a big empty
	   box, and a big empty box that is darker than the page reads as a well
	   you cannot type in: greyed out, the same signal a disabled field gives.
	   So it goes white and does its lifting with an edge and a shadow instead
	   of a fill, which is how a raised surface behaves in light anyway.
	   #dcdcdc rather than --border (#e2e2e2): white-on-white needs a firmer
	   line to hold its shape, while --border-strong (#d0d0d0) draws a box
	   around the composer instead of an edge under it.
	   This also fixes the stack behind it — the queue/agents flaps tuck under
	   the composer at ~#f3f3f3, which is lighter than the old #ebebeb box in
	   front of them, so the thing on top looked like the thing behind. */
	--composer-surface: #ffffff;
	--composer-border: #dcdcdc;
	/* A hover card is the composer's case again: a big floating box, and the
	   ramp's "step away from the page" (darker in light) turns it into a grey
	   slab dropped on white — the same greyed-out signal, and it drags every
	   thumbnail and chip inside it down with it. Paper instead: white, lifted
	   by its shadow and an edge rather than a fill. That needs the composer's
	   firmer line — the smooth-shadow ring resolves to ~#f3f3f3 here, which
	   holds an edge under a grey fill but disappears under a white one. */
	--popup-surface: #ffffff;
	--popup-ring: var(--composer-border);
	/* See :root. Here a dialog is the white-box-on-white case in that same
	   paragraph, so it takes the composer's line rather than a mix of its
	   own — the palette already did, and the two shells are one surface. */
	--dialog-ring: var(--popup-ring);
	--composer-shadow: 0 2px 6px rgba(0, 0, 0, 0.04), 0 14px 36px rgba(0, 0, 0, 0.08);
	/* See :root. The light artwork has enough value range for a quiet cast to
	   lift the glass card from it. This is the scale's `lg` step
	   (smooth-shadow.css), written out because only one theme takes it. */
	--auth-card-edge:
		0 4px 12px -4px color-mix(in srgb, black 5%, transparent),
		0 18px 48px -14px color-mix(in srgb, black 11%, transparent);
	--bg-panel: #f0f0f0;
	--bg-hover: #e9e9e9;
	--bg-active: #e0e0e0;
	/* See :root. */
	--diagram-canvas: #fbfbfc;
	/* See :root. Four units off white for the column, eleven for its plates:
	   the page's own #f0f0f0 plate is right for one card on white and too heavy
	   repeated six times down a 480px column. */
	--panel-surface: #fbfbfb;
	--panel-plate: #f4f4f4;
	/* See :root. Between the L1 plate and the sidebar's half-step, and it has to
	   be: a settings page is a COLUMN of these blocks, not one card on the page,
	   and at the full L1 grey (#f6f6f6) five of them stacked down 720px read as
	   a page made of grey slabs rather than paper with a few quiet groups on it.
	   The sidebar's own #fafafa is a step too far the other way: that value is
	   sized for a full-height column, and a block that only has to hold a group
	   of rows loses its edge at it. Seven units, so a group still reads as a
	   surface while the page around it stays white. */
	--settings-plate: #f8f8f8;
	/* Neutral black ink; a touch lighter than dark's wash because ink on white
	   reads heavier than the same alpha of white on charcoal. */
	--hover: rgba(0, 0, 0, 0.055);
	--hover-strong: rgba(0, 0, 0, 0.095);
	--selected: rgba(0, 0, 0, 0.065);
	--sidebar-row-hover: color-mix(in srgb, #000000 5.5%, var(--sidebar-bg));
	--sidebar-row-selected: color-mix(in srgb, #000000 6.5%, var(--sidebar-bg));
	--sidebar-row-selected-hover: color-mix(
		in srgb,
		#000000 11.6425%,
		var(--sidebar-bg)
	);
	--row-chip: color-mix(in srgb, #000000 9.5%, var(--sidebar-bg));
	--row-chip-hover: color-mix(in srgb, #000000 5.5%, var(--row-chip));
	--border: #e2e2e2;
	--border-strong: #d0d0d0;
	/* See the note on the dark token. A touch more ink for the same reason the
	   greys above carry it: black on white reads lighter than the same alpha of
	   white on charcoal. */
	--scrollbar-thumb: rgba(0, 0, 0, 0.15);
	--text: #1a1a1a;
	--text-dim: #646464;
	--text-faint: #949494;
	--accent: var(--text);
	--on-accent: var(--bg);
	--accent-soft: rgba(0, 0, 0, 0.12);
	--link: var(--blue);
	--tooltip-bg: #222222;
	--tooltip-fg: #ffffff;
	--tooltip-ring: rgba(255, 255, 255, 0.12);
	/* See the note on the dark pair. Light mode is where the old values read as
	   dated, because sRGB holds almost no chroma at the lightness they sat at:
	   the green was 92% of the little a forest green can carry. These lift the
	   lightness instead, which is the only way to a vivid green here, and they
	   spend the contrast headroom the two fills were carrying rather than the
	   floor. White on green goes 5.08:1 to 3.82:1, above the 3:1 a knocked-out
	   glyph needs, and white on red 5.36:1 to 4.54:1, still past 4.5:1 for the
	   10px count that sits on it. */
	--green: #009740;
	--green-soft: rgba(0, 151, 64, 0.12);
	--yellow: #9a6700;
	--yellow-soft: rgba(154, 103, 0, 0.12);
	/* See the note on --yellow-tint above: light mode's ink is too dark and too
	   desaturated to tint with, so the surface yellow is its own colour. */
	--yellow-tint: #f0b400;
	--blue: #0969da;
	--blue-soft: rgba(9, 105, 218, 0.1);
	--red: #de3334;
	--red-soft: rgba(222, 51, 52, 0.1);
	--purple: #8250df;
	--tool-run: #1a7f37;
	--tool-file: #0969da;
	--tool-edit: #9a6700;
	--tool-find: #bc4c00;
	--tool-web: #1b7c83;
	--tool-agent: #bf3989;
	--tool-mcp: #8250df;
	--tool-skill: #0f766e;
	--code-well: #f6f8fa;
	--code-well-line: #d8dee4;
	--code-well-ink: #57606a;
	--code-well-gutter: #8c959f;
	/* Same hues as :root, re-toned dark-on-light — see the note there. */
	--chip-slack-bg: rgba(122, 43, 124, 0.12);
	--chip-slack-fg: #8a2b8c;
	--chip-linear-bg: rgba(94, 106, 210, 0.14);
	--chip-linear-fg: #4650c0;
	--chip-ask-bg: rgba(210, 153, 34, 0.16);
	--chip-ask-fg: #9a6700;
}

/* Glass is an enhancement, never the thing holding the text up: without a
   working backdrop-filter a thinned fill is just a see-through card, and
   somebody who has asked the OS for less transparency has asked for exactly
   this. Both fall back to the opaque surface, which is the same colour the
   glass is mixed from, so nothing else about the popup changes. Declared
   after both theme blocks so they win over the :root definition. */
@supports not (backdrop-filter: blur(1px)) {
	:root {
		--popup-glass: var(--popup-surface);
		--palette-glass: var(--popup-surface);
		/* The page colour, which is what these controls were painted in before
		   they became glass: without a blur behind it a thinned fill is just a
		   window onto whatever is scrolling past. */
		--mobile-header-control-surface: var(--bg);
	}
}

@media (prefers-reduced-transparency: reduce) {
	:root {
		--popup-glass: var(--popup-surface);
		--palette-glass: var(--popup-surface);
		--mobile-header-control-surface: var(--bg);
	}
}

/* ── Accent themes ────────────────────────────────────────────
   The same ten sRGB pairs as native AccentTheme.swift. data-accent is written
   before paint by index.html and maintained by lib/accent-theme.ts. Keeping the
   values as theme-level tokens means every existing accent consumer switches
   together. --accent is the saturated fill/ring, --accent-ink is its readable
   foreground form on a page, --on-accent sits on a filled control, and
   --accent-soft becomes a hue-tinted wash/ring/shadow. */
html[data-accent] {
	--accent: var(--accent-dark);
	--accent-ink: var(--accent-ink-dark, var(--accent));
	--on-accent: var(--on-accent-dark, #ffffff);
	--accent-soft: color-mix(in srgb, var(--accent) 16%, transparent);
	/* A hue has somewhere to go that ink does not, so it deepens instead of
	   fading toward the page: the :root formula mixes the fill 14% into --bg,
	   which on a teal plate lands on near-black rather than on a darker teal.
	   Mixing into --text keeps the hue and reads as pressure in both themes —
	   darker under light, lighter under dark, because that is where the ink
	   ramp points. */
	--accent-hover: color-mix(in srgb, var(--accent) 82%, var(--text));
}

html[data-theme="light"][data-accent] {
	--accent: var(--accent-light);
	--accent-ink: var(--accent-ink-light, var(--accent));
	--on-accent: var(--on-accent-light, #ffffff);
	--accent-soft: color-mix(in srgb, var(--accent) 12%, transparent);
}

html[data-accent="sky"] {
	--accent-light: #1d82bc;
	--accent-dark: #2495d6;
}

html[data-accent="indigo"] {
	--accent-light: #366ef5;
	--accent-dark: #5386f6;
}

html[data-accent="coral"] {
	--accent-light: #dd233a;
	--accent-dark: #f73648;
}

html[data-accent="orange"] {
	--accent-light: #d3571c;
	--accent-dark: #eb6221;
}

/* Honey keeps one fill in both appearances: yellow only exists at high
   lightness, so a value deep enough to separate from a white page still reads
   as gold on a dark one, and one colour spares the picker a swatch that
   changes identity with the theme.

   Its white glyph is a deliberate exception to the contrast rule the other
   accents keep, at 1.62:1. It is the pairing this palette was drawn from and
   it is only ever a glyph on a plate, never body text. The ink form is what
   carries a label, and that deepens until it clears text contrast instead.

   A switch or a checkbox has to read as on/off rather than as a colour, and
   yellow does neither: it is 1.3:1 against a white page, and its white glyph is
   the 1.62:1 that only works at the size of an arrow in a disc. Controls borrow
   Sky in both appearances. */
html[data-accent="lime"] {
	--accent-light: #eec75c;
	--accent-dark: #eec75c;
	--accent-ink-light: #8d7110;
	--accent-ink-dark: #eec75c;
	--accent-control: #2495d6;
	--on-accent-control: #ffffff;
}

/* Black is the accent with no hue: it inverts with the page, so its fill is
   white in dark mode. That is unreadable as a control's "on" state, and the ink
   ramp has nowhere left to deepen into, so it borrows Sky there. In light mode
   its own black plate reads perfectly well and is left alone. */
html[data-accent="mono"] {
	--accent-light: #000000;
	--accent-dark: #ffffff;
	--on-accent-light: #ffffff;
	--on-accent-dark: #000000;
	/* The one accent with no hue to deepen: it already sits past the end of the
	   ink ramp, so mixing it into --text moves nothing. Back to the page. */
	--accent-hover: color-mix(in srgb, var(--accent) 86%, var(--bg));
}

html[data-theme="dark"][data-accent="mono"] {
	--accent-control: #2495d6;
	--on-accent-control: #ffffff;
}

html[data-accent="green"] {
	--accent-light: #1e8e45;
	--accent-dark: #24a351;
}

/* ── Motion ──────────────────────────────────────────────────
   One curve, three durations. The native OS1 app runs effectively a single
   spring everywhere — `.snappy(duration: 0.18–0.28, extraBounce: 0)` — and the
   web UI now matches it: one strong ease-out, no bounce, and the duration
   picked by how far the thing travels rather than by which component it is.

   --ease is the Ionic/iOS drawer curve. It decelerates hard into the end
   position, so movement is visible the instant it starts (the moment the user
   is watching most closely) and settles without overshoot. It is the primary
   token: use it for enters, exits, movement, hovers and colour swaps alike.
   The only deliberate exceptions are `linear` for constant motion (spinners,
   progress fills, marquees), where any easing would read as stuttering, and
   `ease-in-out` on the infinite breathing pulses, where the loop has no start
   or end to accelerate away from. Never `ease-in` on UI.

   Durations are chosen by travel distance, not by component:
     --dur-micro  in-place state changes — hover washes, colour, opacity,
                  a caret flip. Nothing moves more than a couple of px.
     --dur        the default. Small anchored movement: popups, chips,
                  carets, a control sliding a short distance.
     --dur-lg     spatial changes that cross the screen — sheets, drawers,
                  the settings pager.

   Two things deliberately sit outside the scale. `linear` stays on the voice
   waveform, which is driven by live audio amplitude rather than by a UI state
   change — easing a signal would misreport it. And the 1ms `animation`
   declarations on scroll-timeline elements are inert sentinels, not durations:
   the scroll position drives their progress, so they never elapse.

   Nothing below --dur-micro: under ~120ms a transition stops reading as
   motion and reads as a jump with a smear on it, which is worse than no
   transition at all. If something wants to be faster than --dur-micro, it
   wants no transition — delete it instead. */
:root {
	--ease: cubic-bezier(0.32, 0.72, 0, 1);
	--dur-micro: 0.15s;
	--dur: 0.2s;
	--dur-lg: 0.28s;
}

/* Reduced motion. One blanket rule rather than a guard per animation, so the
   default is fail-safe: anything added later is covered without anyone having
   to remember. It also reaches the Tailwind `transition-*` utilities in the
   components, which no per-animation guard ever could.

   The exceptions below are the motion that IS the information. A spinner or a
   pulsing status dot is the only thing on screen saying "this is happening
   right now"; freezing it doesn't calm the interface, it makes a live session
   look hung. Both are also safe under this preference for the reason the
   preference exists — they're small, fixed in place, and loop, so there is no
   travel across the viewport to trigger vestibular discomfort. What the
   preference removes is things flying, sliding, popping and washing about,
   which is exactly what the blanket above takes out.

   Deliberately NOT excepted, though they loop. Named by what the markup
   actually spells today — most of these are now Tailwind arbitrary
   animations, and an entry that names a class nobody emits any more is a
   decision nobody can find:
     animate-[staging-shimmer_…]  (StagingLink) the skeleton block already
                        says "loading"; the travelling highlight on top of it
                        is decoration, and it is the one looping thing here
                        that actually translates.
     animate-[composer-agents-pulse_…]  (ComposerAgents) keeping the existing
                        explicit choice to stop it.
     animate-[text-shimmer_…]  (composerQueueSendingShimmer) the crest that
                        crosses "Queueing"/"Sending". The word itself says
                        what is happening, so a still label loses nothing.
     [animation-name:pixel-snake-trail]  (PixelSpinner) the decorative pixel
                        loader — a wave that travels across the block. However
                        fun, it is the single most motion-heavy thing in the
                        app and the clearest thing this preference is asking
                        us to drop; the grid still reads as a graphic at rest.

   PREFER PUTTING A NEW EXCEPTION ON THE ELEMENT, not in this list:
   `motion-reduce:[animation-duration:0.8s]! motion-reduce:[animation-iteration-count:infinite]!`
   wins here (equal specificity, and the utility sheet is linked last) and
   travels with the component. Five indicators silently froze during the
   Tailwind migration because their class name was removed from the markup
   while its exception stayed behind here — invisible on screen, since the
   freeze only happens under the preference. It IS checkable, though: grep
   each name below against the shipped bundle (every .js file of the current
   build, not just the entry — the build code-splits), and a name with no hit
   is an indicator that no longer turns. All eleven were live on 2026-08-08.

   The exceptions restate their durations literally rather than trying to undo
   the blanket with `revert` — `revert` rolls back to the *origin* below the
   author one, i.e. the UA's `0s`, which would freeze exactly the indicators we
   are trying to keep alive. */
@media (prefers-reduced-motion: reduce) {
	*,
	*::before,
	*::after {
		transition-duration: 0.01ms !important;
		transition-delay: 0ms !important;
		animation-duration: 0.01ms !important;
		animation-delay: 0ms !important;
		animation-iteration-count: 1 !important;
		scroll-behavior: auto !important;
	}

	/* Progress spinners — constant rotation, the sole "working" signal, so
	   they keep turning. Matched on the animation utility itself rather than
	   on a hook class per spinner: the old name list silently stopped
	   matching whenever a migration renamed an element, and a frozen spinner
	   next to live output reads as a hung app. Anything spelt `animate-spin`
	   or `animate-[spin_…]` is a progress indicator by construction. */
	[class*="animate-spin"],
	[class*="animate-[spin"] {
		animation-duration: 0.8s !important;
		animation-iteration-count: infinite !important;
	}

	/* A spinner that turns at a rate other than the 0.8s above keeps its own
	   exception, placed after that rule so its duration wins the tie. The
	   0.8s spinners need no entry at all — the rule above already names them
	   by their animation utility. */

	/* Status pulses — opacity breathing that encodes live session state
	   (running vs waiting vs idle). The rule for this preference is "gentler,
	   not zero: keep opacity, drop movement", and these are pure opacity. */
	.pr-check-mark-pending,
	.prc-mark-pending,
	.rv-check-dot-pending {
		animation-duration: 1.4s !important;
		animation-iteration-count: infinite !important;
	}
	/* The caret below also has its animation in this file (see "Transcript
	   liveness" further down), so this duration restates the rule's value. */
	.pr-bar-checking {
		animation-duration: 1.6s !important;
		animation-iteration-count: infinite !important;
	}
	.msg-streaming .msg-body-assistant::after {
		animation-duration: 1s !important;
		animation-iteration-count: infinite !important;
	}
}

/* Phones: the composer is the bottom-most thing on screen, so its shadow has
   only the gap under it to fall into — and in mobile Safari (browser toolbar
   below, so no safe-area inset) that gap is a few px, far less than the ~32px
   a 14px-offset/36px-blur shadow needs. The tail was clipped into a hard line
   along the bottom edge. Pull the offset up and shorten the blur so the whole
   falloff lands inside the gap; the ambient spread still reads as lift. */
@media (max-width: 720px) {
	:root {
		--composer-shadow: 0 2px 6px rgba(0, 0, 0, 0.16),
			0 4px 20px rgba(0, 0, 0, 0.2);
	}
	html[data-theme="light"] {
		--composer-shadow: 0 2px 6px rgba(0, 0, 0, 0.04),
			0 4px 20px rgba(0, 0, 0, 0.08);
	}
}

/* The native iPhone UI reads transcript prose at 17pt. At phone width the PWA
   promotes every semantic role to the next rung of its existing scale, so its
   15px body does not look like a shrunken desktop UI beside the native app.
   Inputs join the body at 17px, still above Safari's 16px zoom threshold. */
@media (max-width: 720px) {
	:root {
		--type-root: 15px;
		--type-meta: 13px;
		--type-label: 14px;
		--type-item-title: 15px;
		--type-body: 17px;
		--type-dialog-title: 19px;
		--type-section-title: 24px;
		--type-page-title: 28px;
		--type-stat: 34px;
		--type-stat-line: 38px;
		--type-input-phone: 17px;
	}
}

html[data-theme="light"] .markdown code {
	background: rgba(149, 59, 57, 0.09);
	color: #953b39;
}
/* A code block is a well: it has to read as sunk BELOW whatever it sits on.
   In dark that's free — #0c0c10 is below every surface in the ramp. Light has
   no absolute that works on both containers a `.markdown` block gets: an
   assistant turn paints on --bg (#fff), but a user or teammate turn paints on
   the bubble (--bg-panel #f0f0f0 / the teal wash), and the old #f6f8fa was
   LIGHTER than the bubble — so a pasted snippet read as raised in light and
   sunk in dark. Translucent ink instead, the same trick as --hover: one value
   that lands a fixed step below any surface it's dropped on. */
html[data-theme="light"] .markdown pre {
	background: rgba(0, 0, 0, 0.055);
	border-color: rgba(0, 0, 0, 0.1);
	color: #1f2328;
}
/* `.markdown pre code { background: none }` is out-specified in light by the
   inline-code rule above (html[data-theme] + .markdown + code beats
   .markdown + pre + code), so a block's own lines picked up the inline chip
   wash as a band behind each line. Dark never had it — there the two rules
   are the same weight and pre code wins on order. */
html[data-theme="light"] .markdown pre code {
	background: none;
	color: inherit;
}

* {
	box-sizing: border-box;
}

html {
	/* Drives light-dark() in embedded components (@pierre/trees) + native UI.
	   theme.ts also sets this inline per resolved theme; this is the fallback. */
	color-scheme: dark;
}
html[data-theme="light"] {
	color-scheme: light;
}

/* ── Scrollbars ──────────────────────────────────────────────
   A Mac on a trackpad draws an OVERLAY scrollbar: it fades in while you
   scroll, sits over the content, and reserves no width at all. Connect a
   mouse, or set Show scroll bars to Always, and the same window gets a
   permanent 15px bar with a filled track down the side of every panel.
   Measured against the transcript: a #272727 band on a #1a1a1a page carrying
   a #606060 thumb, full height, right against the info panel's edge, where it
   reads as a third column. Windows and Linux draw the same bar.

   Thin with the track dropped keeps what the bar is for, which is where you
   are in a long session and something to drag when a wheel is too slow, at a
   fraction of the ink. 15px becomes 11px, and the band becomes the page.

   Deliberately the standard properties, and NOT ::-webkit-scrollbar. That
   pseudo-element replaces the platform scrollbar with a custom one, and a
   custom scrollbar is never an overlay: giving it a width carves a permanent
   gutter out of every Mac that draws overlays (measured in Chrome on macOS:
   0px becomes 8px). The standard pair is inert wherever the platform draws
   overlays, so it only reaches the bar that is really there. The app's own
   `[&::-webkit-scrollbar]:hidden` utilities are safe for the same reason
   nothing here sets a width — they hide, and a hidden bar reserves nothing.

   scrollbar-width does not inherit, so it takes the universal selector. Zero
   specificity, so the scrollers that hide their bar outright still win. */
* {
	scrollbar-width: thin;
	scrollbar-color: var(--scrollbar-thumb) transparent;
}

html,
body {
	margin: 0;
	padding: 0;
	height: 100%;
	background: var(--bg);
	color: var(--text);
	font-family: var(--sans);
	font-size: var(--type-root);
	/* Medium is the app's regular. Metadata and supporting copy are the
	   exceptions: they render at 400 so their secondary text stays quiet and
	   their smaller strokes do not close up. Bold accents stay 600/700. */
	font-weight: 500;
	/* No -webkit-font-smoothing here on purpose: macOS rasterizes text for us.
	   `antialiased` opts out of that and thins every stem, so the same string
	   reads lighter in the web UI than in the native app beside it. The default
	   keeps the contrast boost AppKit applies, which is what the rest of the
	   desktop looks like. */
	/* Every size in this sheet is a hand-picked px value, but WebKit's default
	   text autosizing (-webkit-text-size-adjust: auto) re-scales them per block
	   from that block's width — so on iPhone the sidebar's rows came out at
	   inconsistent, larger-than-authored sizes (and grew again in landscape).
	   Preflight normally pins this to 100%; we deliberately don't ship it (see
	   styles/tailwind.css), so it has to be set here. */
	-webkit-text-size-adjust: 100%;
	text-size-adjust: 100%;
}

/* Form controls don't inherit font-weight — the UA stylesheet pins them to
   regular, and we deliberately don't ship Tailwind preflight to undo that. A
   huge share of the UI is <button>-based (sidebar rows, step rows, tabs), so
   without this the body's medium never reaches it. */
button,
input,
textarea,
select {
	font-weight: inherit;
}

/* Lock the outer document to the viewport so only inner containers scroll.
   position: fixed stops iOS rubber-band/elastic overscroll of the whole page
   (header included); overscroll-behavior stops scroll-chaining to the body. */
/* Must live on html, not body: overscroll-behavior only disables the
   browser's swipe-back/forward navigation gesture (Chrome/Android) when set
   on the root element — it does not propagate up from body. */
html {
	overscroll-behavior-x: none;
}

body {
	position: fixed;
	inset: 0;
	overflow: hidden;
	overscroll-behavior: none;
	touch-action: pan-x pan-y;
}

#root {
	/* Fill the viewport-locked body instead of measuring another viewport unit.
	   In standalone iPhone mode index.html corrects WebKit's short viewport by
	   setting the document-flow roots to screen.height before the app mounts. */
	height: 100%;
	overflow: hidden;
}

/* The standalone correction is for the resting window only. When a text field
   focuses, release its inline screen.height so WebKit can pan the original
   viewport above the keyboard. The keyboard covers the letterboxed strip while
   this class is present, and removing the class restores full device height. */
html:has(body.kb-open),
body.kb-open {
	height: 100% !important;
}

/* A palette is a surface you type into, so the page behind it must hold still.
   Base UI's `trap-focus` mode deliberately neither inerts nor scroll-locks the
   page (the "@"-mention popup portals to <body> and inerting would kill it), so
   on a phone the transcript or list underneath still panned under the sheet and
   took the composer with it. `touch-action` is the smallest lever that stops
   it: it blocks the pan without touching overflow, so no scroller loses its
   position and nothing reflows. Effective touch-action is the intersection down
   the ancestor chain, so one declaration on the app root covers every scroller
   inside it, and the palette itself is portaled outside .app and keeps
   scrolling its own prompt. `:not([hidden])` is load-bearing: a keepMounted
   palette (the Desk) is in the DOM from boot and would otherwise lock the page
   forever. */
body:has(.palette-backdrop:not([hidden])) .app {
	touch-action: none;
}

/* ── Native feel: UI chrome isn't text-selectable ────────────────
   Disable text selection everywhere by default so labels, buttons,
   nav, headers, tabs and status text behave like a native app — no
   accidental blue highlight when you drag across the UI. Genuine
   content (session messages + the names in them, code, diffs, inputs,
   and anything tagged .selectable) opts back in just below. */
body {
	-webkit-user-select: none;
	-moz-user-select: none;
	user-select: none;
	-webkit-touch-callout: none;
}

/* Match copied-text highlights to the selected accent without replacing the
   content's own foreground colour (important for code and diff syntax). */
::selection {
	background: color-mix(in srgb, var(--accent) 32%, transparent);
}

/* The transcript passage staged as composer context ("Selected text"). Adding
   a passage takes the native selection away and hands the mark to this
   highlight, so exactly one band paints under the words and it survives the
   click that moves the caret into the composer. Registered by QuoteSelection,
   which has no element of its own to carry a utility. */
::highlight(quote) {
	background: color-mix(in srgb, var(--accent) 32%, transparent);
}


/* Text the user may legitimately want to select and copy: supporting copy,
   message rows and their prose, names, system text, code, diffs, inputs. The
   row wrapper is included on purpose — a drag has to be able to start in a
   bubble's padding or the gutter beside it (see the transcript rule below) —
   and the highlight still hugs the words because .msg and .msg-body are flex
   columns, which is what stopped WebKit painting full-width bands across the
   gaps. */
.text-supporting,
input,
textarea,
select,
[contenteditable],
[contenteditable] *,
.msg,
.msg *,
.msg-label,
.msg-body,
.msg-body *,
.msg-system-text,
.markdown,
.markdown *,
code,
pre,
.selectable,
.selectable * {
	-webkit-user-select: text;
	-moz-user-select: text;
	user-select: text;
	-webkit-touch-callout: default;
}

/* The transcript is a document, not chrome: the whole reading area selects —
   row gutters, bubble padding and the space between messages included.
   Opting in only the text leaves (above) wasn't enough, because a browser
   refuses to START a selection inside a `user-select: none` region and won't
   extend out of one: a drag that began a few pixels off a glyph — in the
   gutter beside a right-aligned user bubble, in the bubble's own 10/14px
   padding, or between two messages — selected nothing at all, which reads as
   "messages can't be selected" (reported on the desktop shell). The
   full-width "gap band" painting that made us narrow this in the first place
   is handled structurally now: .viewer-messages, .msg and .msg-body are flex
   columns, which WebKit doesn't band-paint. */
.viewer-messages,
.viewer-messages * {
	-webkit-user-select: text;
	-moz-user-select: text;
	user-select: text;
	-webkit-touch-callout: default;
}

/* Interactive controls stay unselectable even inside selectable content
   (e.g. the hover fork button on a message). :is() lifts these to the same
   specificity as the .viewer-messages opt-in above, so — coming later — they
   still win inside the transcript. */
:is(button, [role="button"]),
:is(button, [role="button"]) * {
	-webkit-user-select: none;
	-moz-user-select: none;
	user-select: none;
}

/* No preflight ships, so neutralize the UA button chrome here. The moment an
   author background/border lands on a <button> (e.g. Tailwind's
   bg-transparent), browsers drop native appearance and paint the "devolved"
   2px-outset bevel + buttonface padding instead — iOS renders that as a
   raised 3D card. Every button in the app styles itself via classes; none
   want the native chrome. Scoped to button only — appearance:none would
   break native selects/checkboxes. */
button {
	font-family: inherit;
	cursor: pointer;
	appearance: none;
	-webkit-appearance: none;
	border: none;
	background: none;
	padding: 0;
}

@supports (corner-shape: squircle) {
	/* Turn the squircle treatment on everywhere: every corner radius in this
	   file is authored as `calc(<value> * var(--rf))` + `corner-shape: var(--cs)`,
	   so flipping these two tokens rounds up ALL radii at once. 1.35 matches the
	   bump the Tailwind utilities already ship in tailwind.css, so legacy CSS
	   and the new UI system land on the same corner. --rp follows on its own:
	   it is derived from --rf, so a percentage radius lands back at the value
	   it is authored with here and takes the circular shrink everywhere else. */
	:root {
		--rf: 1.35;
		--cs: squircle;
	}

	:where(
			[class*="rounded-"]:not([class*="rounded-full"]),
			[class~="rounded"]
		) {
		corner-shape: squircle;
	}

	/* Legacy classes that set a radius without any `rounded-*` utility on the
	   markup, so the rule above cannot reach them. An entry dies with its
	   class: once a component migrates, its radius comes from a utility and
	   the name here matches nothing. Read this list as the set of elements
	   legacy deliberately squircles — `rounded-full` is the one utility
	   spelling that opts OUT, so migrating one of these to it flattens the
	   corner at the same radius. Use `rounded-[999px]` for a squircled pill. */
	.session-link,
	.sidebar-reopen {
		corner-shape: squircle;
	}
}

input,
textarea,
select {
	font-family: inherit;
}

a {
	color: var(--link);
}

/* ── App chrome ──────────────────────────────────────────── */

.app {
	display: flex;
	flex-direction: column;
	height: 100%;
}

/* The installed phone composer is a native-feeling capsule rather than the
   desktop squircle. Safari already falls back to circular corners because it
   does not support corner-shape; this keeps Chromium PWAs consistent. Its edge,
   attached flaps, and Slack share card use the desktop ring's 35% strength. */
@media (display-mode: standalone) and (max-width: 720px) {
	.app .composer {
		corner-shape: round;
	}
	.app .composer,
	.app .pwa-composer-edge {
		border-color: color-mix(in srgb, var(--composer-border) 35%, transparent);
	}

	/* Keep dictation in the browser view, where its recording APIs are reliable.
	   The installed phone composer still shows model selection after it expands. */
	.app .pwa-composer-dictation {
		display: none;
	}

	/* Installed navigation uses the foreground colour instead of the custom
	   accent. Slightly larger glyphs make the two edge controls easier to read
	   without changing their 44px touch targets. */
	.app .pwa-header-back,
	.app .pwa-header-actions button {
		color: var(--text);
	}
	.app .pwa-header-back svg {
		width: 34px;
		height: 34px;
	}
	.app .pwa-header-actions button svg {
		width: 25px;
		height: 25px;
	}
}

/* A drag in progress owns the cursor for the whole document, not just the
   handle under the pointer — the pointer routinely leaves the 4px handle
   mid-drag, and without this the cursor flickers back to a caret over the
   text it passes. Set on <body> by the resize handlers, which is why it can't
   be a utility: React never renders that element. */
body:is(.resizing-sidebar, .resizing-panel, .resizing-tab-split) {
	cursor: col-resize;
	user-select: none;
}

/* The reclaimed PWA back/forward cluster (see TitleBar.tsx) only exists in
   Window Controls Overlay mode — hidden everywhere else. */
.wco-nav {
	display: none;
}

/* Window Controls Overlay (desktop PWA + the Open Session desktop app): the OS titlebar
   is replaced by just the window-control buttons overlaid on our own chrome —
   no OS bar, no centered title text, and NO dedicated titlebar band of our
   own. The window's whole first content row IS the titlebar: on the sidebar
   it's a pure chrome row — traffic lights (centered on the row by the shell's
   trafficLightPosition), then collapse + the avatar account trigger, with
   back/forward/search at the right edge — and it continues through the detail pane's
   header and the right panel's PR strip. Every segment is a drag region
   so grabbing anywhere along the top moves the window.

   Keyed off `html.wco` (set by the detection script in index.html), NOT the
   `(display-mode: window-controls-overlay)` media query: Electron (the Open Session
   desktop shell, packages/clients/mac) exposes the WCO geometry and env() vars but
   keeps display-mode at "browser", so the media query never matches there. */
/* Updated macOS shell builds supply one native visual-effect surface beneath
   Chromium. Reveal it only through the structural chrome: the sidebar, the one
   region left beside the opaque workspace. The shell capability flag keeps
   PWAs and older opaque shell builds on the normal solid surfaces. Native
   vibrancy provides the blur, so this deliberately adds no backdrop-filter. */
@media (min-width: 721px) {
	html.material-backdrop,
	html.material-backdrop body,
	html.material-backdrop #root,
	html.material-backdrop .app {
		background: transparent;
	}

	html.material-backdrop .app-body {
		background:
			linear-gradient(
				180deg,
				var(--sidebar-material-sheen),
				transparent 180px
			),
			var(--sidebar-material);
	}

	/* The app-body owns the material behind both regions. Painting it again on
	   the child sidebar would double the wash and make it darker than the gutter. */
	html.material-backdrop .sidebar-container {
		background: transparent;
	}

	/* Until the app has something to draw, keep the window opaque. An app with
	   no data yet paints almost nothing (measured: three elements on the whole
	   page), and through a transparent window that does not read as an app
	   waiting for its data, it reads as no app at all: the desktop shows
	   through where the window should be. `app-ready` is set alongside the
	   launch splash coming down (App.tsx), so this covers the load and any
	   later state where the app has nothing to show. Same declaration as the
	   reduced-transparency block below, for the same reason. */
	html.material-backdrop:not(.app-ready),
	html.material-backdrop:not(.app-ready) body,
	html.material-backdrop:not(.app-ready) #root,
	html.material-backdrop:not(.app-ready) .app,
	html.material-backdrop:not(.app-ready) .app-body,
	html.material-backdrop:not(.app-ready) .sidebar-container {
		background: var(--sidebar-bg);
	}

	/* --row-chip is deliberately NOT re-derived here, and the vibrancy sidebar
	   is the surface where the chip cannot have everything. Measured, in light,
	   against a mid-green wallpaper: sidebar L222, hovered row L209. A chip
	   that keeps the material's alpha and steps its colour lands at L214 — it
	   re-paints a slab of the light material over the row's ink and comes out
	   LIGHTER than the row it is pressed into. An opaque chip cannot track an
	   unknown wallpaper either: the same #dedede reads as a lid on a light
	   backdrop and as a bright tile on a dark one. Only a translucent ink wash
	   steps reliably, and a wash is what a chip must not be — it shows the
	   row's own wash and its title through the glyph. So the chip stays the
	   opaque lid, and on this one surface it reads as a neutral tile rather
	   than picking the wallpaper up. */
}

@media (min-width: 721px) and (prefers-reduced-transparency: reduce) {
	html.material-backdrop,
	html.material-backdrop body,
	html.material-backdrop #root,
	html.material-backdrop .app,
	html.material-backdrop .app-body,
	html.material-backdrop .sidebar-container {
		background: var(--sidebar-bg);
	}
}

/* Pinned headings need an opaque backing so rows cannot show through them.
   Keep this material opaque rather than animating row opacity: `view()`
   timelines can resolve against nested overflow containers and hide rows. */
@media (min-width: 721px) {
	html.material-backdrop {
		--sidebar-material: var(--sidebar-bg);
		--sidebar-material-sheen: transparent;
	}
}
/* Sidebar top row clears the traffic lights. env(titlebar-area-x) is the
   controls' right edge on macOS; it drops to 0 in fullscreen (lights hidden)
   and on Windows (controls on the right), where the default padding wins. */
html.wco .sidebar-brand {
	padding-left: max(env(titlebar-area-x, 78px), calc(var(--sidebar-icon-left) - 8px));
}
/* The shell centers its traffic lights on this row, and everything in it wants
   to sit 2px below where a 48px row would put it so its visual mass lands on
   the lights' line. Give the row those 2px at both ends — one taller row —
   rather than 4px of top padding: padding moved the content without balancing
   it, so every row in the first band read top-heavy, and on `.pr-bar` (sized
   by min-height, not fixed) it grew the row past the header beside it and
   carried its hairline down with it. Nothing inside moves; the row is 4px
   deeper and its divider sits 4px lower. */
html.wco {
	--desktop-header-h: 52px;
}
/* The whole first row doubles as the titlebar — everything interactive in it
   opts back out so it stays clickable.

   `wco-chrome` is the opt-in: any row that spans the top of a pane at
   `--desktop-header-h` carries it, and gets the drag region plus every
   carve-out below. It is one name rather than a list of them because a row
   left out of the list is a dead stretch of the window's top edge: the report
   pages had two such rows, and the window could only be dragged by the
   sidebar. Rows: the sidebar brand row, the session header, the plain pane
   title, the PR bar, the analytics bar, and both report headers. */
/* `desktop-shell` is the durable Electron capability. `wco` still drives the
   geometry and spacing, but Chromium can transiently report its overlay as
   hidden while the BrowserWindow still has no native titlebar. Dragging must
   survive that report or the window has no way to move. */
html:is(.wco, .desktop-shell) .wco-chrome {
	-webkit-app-region: drag;
	app-region: drag;
}
html:is(.wco, .desktop-shell) .wco-chrome
	:is(button, a, input, select, textarea, [role="button"], [tabindex]) {
	-webkit-app-region: no-drag;
	app-region: no-drag;
}
/* Chromium withholds pointer events from native drag regions. While a visible
   menu is mounted, temporarily return the chrome to the page so its outside
   press can dismiss the menu. A hidden keep-mounted popup must not disable
   window dragging indefinitely. */
html:is(.wco, .desktop-shell):has(.app-menu-popup:not([hidden])) .wco-chrome {
	-webkit-app-region: no-drag;
	app-region: no-drag;
}
/* The media lightbox floats over the draggable first row. Electron does not
   reliably let an overlapping sibling carve clickable links out of that row,
   so suspend the underlying drag region while the lightbox is mounted. */
html:is(.wco, .desktop-shell):has([data-media-lightbox]) .wco-chrome {
	-webkit-app-region: no-drag;
	app-region: no-drag;
}
/* The chrome row itself (collapse + avatar left, back/forward + search at the
   right edge) is shared by web and the desktop shell — only the traffic-light
   inset and drag regions above are shell-specific. */
.sidebar-brand .wco-nav {
	display: flex;
	align-items: center;
	gap: 2px;
	margin-left: auto;
}
/* Collapsed sidebar takes the chrome row (and its nav) with it — show the
   floating fallback cluster in the detail pane instead, next to the sidebar
   re-open control, and inset the header text past both. In the shell the
   detail pane starts at x=0 with the traffic lights overlaying its left edge,
   so everything additionally shifts right of env(titlebar-area-x) (0 in
   fullscreen, where the lights hide and the row packs left again). */
.wco-nav-pane {
	position: absolute;
	top: calc((var(--desktop-header-h) - 30px) / 2);
	left: 50px;
	z-index: 20;
}
/* These controls are siblings of the draggable header rather than descendants
   of it, so the generic header no-drag rule above does not reach them. Mark the
   floating cluster itself as interactive or the desktop shell consumes clicks
   as window drags while the sidebar is collapsed. */
html:is(.wco, .desktop-shell) .sidebar-reopen,
html:is(.wco, .desktop-shell) .wco-nav-pane {
	-webkit-app-region: no-drag;
	app-region: no-drag;
}
html:is(.wco, .desktop-shell) .app-body.sidebar-collapsed .detail-pane .wco-nav-pane {
	display: flex;
	align-items: center;
	gap: 2px;
}
html.wco .sidebar-reopen {
	/* Match .sidebar-brand's left padding exactly so collapsing does not move
	   the panel icon horizontally. */
	left: max(env(titlebar-area-x, 78px), 8px);
}
html.wco .wco-nav-pane {
	left: calc(env(titlebar-area-x, 78px) + 42px);
}
html.wco .app-body.sidebar-collapsed .viewer-header,
html.wco .app-body.sidebar-collapsed .detail-topbar-title {
	padding-left: calc(env(titlebar-area-x, 78px) + 148px);
}
/* Electron does not reliably subtract a no-drag region when it belongs to an
   overlapping sibling of the draggable header. The collapsed controls float
   above that header, so make the underlying row non-draggable in this state;
   otherwise it intercepts sidebar/back/forward clicks before they reach DOM. */
html:is(.wco, .desktop-shell) .app-body.sidebar-collapsed .wco-chrome {
	-webkit-app-region: no-drag;
	app-region: no-drag;
}
/* The collapsed pane's floating controls occupy the traffic-light inset and
   the next 104px. Its header stays no-drag because Electron does not reliably
   subtract those overlapping siblings. This empty slot directly after the
   controls restores a safe native grip without covering titlebar actions. */
.wco-collapsed-drag-handle {
	display: none;
}
html:is(.wco, .desktop-shell)
	.app-body.sidebar-collapsed
	.wco-collapsed-drag-handle {
	position: absolute;
	top: calc((var(--desktop-header-h) - 34px) / 2);
	left: calc(env(titlebar-area-x, 78px) + 112px);
	z-index: 20;
	display: block;
	width: 36px;
	height: 34px;
	-webkit-app-region: drag;
	app-region: drag;
}


/* ─────────────────────────────────────────────────────────────
   Markdown — the typography of agent output.

   This is content styling, not component styling, which is why it
   lives here rather than in legacy.css: the HTML is produced by the
   markdown renderer from model output, so there is no JSX to hang
   utility classes on. Element selectors under `.markdown` are the only
   way to reach it, and that stays true however far the migration goes.
   ───────────────────────────────────────────────────────────── */
/* Full automated-review preview: a readable long-form scale, bounded by the
   popover rather than the viewport, with the same theme tokens as markdown. */
.review-preview-markdown.markdown {
	font-size: var(--type-body);
	line-height: 1.5;
	color: var(--text-dim);
}
.review-preview-markdown.markdown > h3:first-child {
	margin: 0 0 12px;
	font-size: var(--type-section-title);
	font-weight: 600;
	line-height: 1.25;
	letter-spacing: -0.015em;
	color: var(--text);
	text-wrap: balance;
}
.review-preview-markdown.markdown pre {
	max-width: 100%;
}

/* markdown content */
/* Model and GitHub output carries tokens no line break can fall inside — a
   module path, a query string, a bare URL. Left to default `normal` wrapping
   those set the block's min-content width, so on a phone they don't just
   overflow their own line: any flex/grid ancestor sized off min-content grows
   with them and the whole surface scrolls sideways. `anywhere` both wraps them
   and takes them out of the intrinsic width. `pre` is unaffected — it never
   wraps, and keeps its own horizontal scroll. */
.markdown {
	overflow-wrap: anywhere;
}

/* Prose reads one step below the app's medium, in both themes. Dropping the
   -webkit-font-smoothing override handed macOS back the rasterization every
   other app gets, and that puts roughly 10% more ink in each stem: solid on a
   label you scan, thick across a paragraph you read. A running text block is
   the one place where that reads as weight rather than as presence, so it is
   the one place that steps down. In light 450 lands the stroke back where the
   500 sat before (measured stem fill 55.1% before, 56.2% here, 58.0% at 500);
   in dark it takes about 8% of the ink back out, which is as far as weight
   goes there. The rest of dark's heft is the rasterizer, so 400 buys little
   more and would reach the clients that never got the darkening at all
   (Windows, Linux, the iOS PWA). Bold and headings set their own 600, so only
   running text moves. */
.markdown {
	font-weight: 450;
}

.markdown p {
	/* 8px on a 15px/24px body ≈ the 0.47em block rhythm the native app uses
	   (blockSpacing 8 against 17pt). 6px left paragraphs reading as one slab.
	   Both this and the 24px leading are absolute, so the gap between two
	   paragraphs stayed 32px when the body moved 14 -> 15; the words just fill
	   more of the line box (1.6 leading now, was 1.71). */
	margin: 0 0 8px;
}

.markdown a {
	color: var(--link);
	text-decoration: none;
}
.markdown a:hover {
	text-decoration: underline;
}

.markdown strong {
	font-weight: 600;
	color: var(--text);
}

.markdown pre {
	background: #0c0c10;
	border: 1px solid var(--border);
	border-radius: calc(18px * var(--rf)); corner-shape: var(--cs);
	padding: 10px 12px;
	margin: 2px 0 8px;
	/* Preserve authored indentation and line breaks, but soft-wrap long lines so
	   a fence reads at the conversation width instead of becoming a scroller.
	   `anywhere` also catches URLs, hashes, and generated identifiers that have
	   no natural wrap point. */
	white-space: pre-wrap;
	overflow-wrap: anywhere;
	overflow-x: auto;
	font-family: var(--mono);
	/* The same ratio the inline chip below uses, so a block and an inline run
	   of code are one size, and both scale with the prose they sit in rather
	   than pinning a second absolute size into the scale. */
	font-size: 0.9em;
	line-height: 1.55;
}

/* An inline code chip is small enough that a 4px corner reads as a square
   slab, and a paragraph of them reads as a row of boxes rather than words
   with a highlight. At 7px (9.45 once --rf scales it) the corner takes the
   full height of the chip, so it reads as a capsule around the word. The
   wash is tinted with the chip's own ink rather than neutral black/white:
   a grey fill under coloured text is a second, unrelated colour in the
   sentence, and it is what made these read as slabs. */
.markdown code {
	font-family: var(--mono);
	font-size: 0.9em;
	background: rgba(221, 225, 240, 0.07);
	border-radius: calc(7px * var(--rf)); corner-shape: var(--cs);
	padding: 1.5px 6px;
	color: #dde1f0;
}
.markdown pre code {
	background: none;
	padding: 0;
	color: inherit;
}

/* Live code tints in the composer draft — the same case as `.markdown` above,
   and here for the same reason: a mirror div paints the styled draft behind a
   transparent-text textarea (so caret, selection, undo and IME stay native),
   and its spans are written as an innerHTML string by lib/composer-highlight.ts.
   There is no JSX to hang utilities on, so this cannot become a utility; it is
   renderer-emitted markup, which is what base.css keeps.

   The tints paint only: colour, background, and the one outline below, all of
   which leave the line box alone. Any font, padding or size change here would
   desync the painted glyphs from the caret in the textarea behind them — the
   mirror and the field have to agree on every metric. */
.composer-hl .cmp-code {
	background: rgba(255, 255, 255, 0.09);
	/* Same capsule corner the chip gets once the draft is sent. */
	border-radius: calc(7px * var(--rf)); corner-shape: var(--cs);
	color: #e8b3b9;
}
.composer-hl .cmp-fence {
	background: rgba(255, 255, 255, 0.06);
	border-radius: calc(4px * var(--rf)); corner-shape: var(--cs);
	color: #dde1f0;
}
/* A finished @-mention of a teammate, the draft's counterpart to the
   `person-chip` a sent message renders. It reads as a pill without being one:
   the span may not take a pixel of padding, because the mirror has to stay
   glyph-identical to the textarea behind it. So the wash is painted by a
   pseudo-element instead, inset negatively past the box — which an `outline`
   also does, but only by the same amount on every side, and the room this
   needs is horizontal. One element painting one translucent colour also stays
   flat where two overlapping shadows would have doubled, and `z-index: -1`
   puts it behind the name, in front of the mirror.

   How far it reaches is arithmetic rather than taste. The only room beside a
   mention is the space character next to it, and that one space has to cover
   both the pill's own padding and the gap to the neighbouring word. A 14px
   draft sets words 3.7px apart, which is not enough for either: a wash of any
   real width sits ON the next word, which is what reads as the pill being cut
   off. So the composer widens the space itself (`word-spacing`, on the field
   as well as the mirror so the two still agree glyph for glyph; a margin on
   this span alone would slide the painted text off the caret behind it), and
   wears it only while a mention is in the draft, so an ordinary sentence keeps
   the type's own spacing. See `composerMentionSpacing` in
   lib/composer-classes.ts.

   That sets a space at 7.2px, and the wash takes 5px of it on each side. What
   is left, 2.2px, is the gap a neighbouring word sees. The two ends look even
   at the same number even though they are not the same shape, because the
   face at one end and the name's flat stem at the other are each 1px inside
   their own edge of the box.

   The mirror carries 6px of padding against a matching negative margin
   (Composer.tsx): its content origin is unchanged, so nothing moves, but
   `overflow: hidden` then clips past the reach — otherwise a mention at the
   start of a line has its left edge cut off by the mirror's own bounds.

   Hover is set by the composer, which hit-tests these spans through the
   textarea covering them. */
.composer-hl .cmp-mention {
	position: relative;
	color: var(--accent-ink);
}
.composer-hl .cmp-mention::before {
	content: "";
	position: absolute;
	inset: -2px -5px;
	z-index: -1;
	/* Squircle capsule, the shape the sent `person-chip` takes. */
	border-radius: 999px; corner-shape: var(--cs);
	background: color-mix(in srgb, var(--accent) 12%, transparent);
}
.composer-hl .cmp-mention[data-hover]::before {
	background: color-mix(in srgb, var(--accent) 22%, transparent);
}
/* The face. It is painted rather than laid out, for the same reason: there is
   nowhere to put a picture, so the `@` goes transparent and lends the face
   its slot. The face hangs off THAT span rather than off the pill, so it
   stays fixed to the slot whatever the wash around it does, and the gap either
   side of it is the slot's own arithmetic: 13.8px at the composer's 14px type,
   plus the 5px the wash reaches to its left, less a 0.8em face set 1px past
   the box, leaves 4px of wash before the face and 3.6px between it and the
   first letter. The size is in `em` because the slot is: the phone composer
   sets 16px, where a fixed size would sit small in a wider gap. Absolute, so
   it takes no part in layout. */
.composer-hl .cmp-at {
	position: relative;
	color: transparent;
}
.composer-hl .cmp-faced .cmp-at::before {
	content: "";
	position: absolute;
	left: -1px;
	top: 50%;
	width: 0.8em;
	height: 0.8em;
	transform: translateY(-50%);
	/* UserAvatar's squircle, the same shape the picture takes in the mention
	   picker and in the sent chip. */
	border-radius: calc(32% * var(--rp)); corner-shape: var(--cs);
	background: var(--cmp-face) center / cover no-repeat, var(--bg-active);
	box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 14%, transparent);
}
/* A session id in the draft, the counterpart to the `session-link` chip a sent
   message renders — and where a pasted session link lands, since the composer
   shortens one to the id it carries (`pastedSessionId` in lib/session-url.ts).
   The pill is the receipt for that: someone just watched a URL they pasted
   turn into an id, and the wash is what says the composer understood it rather
   than mangled it.

   Built like the mention pill above, and tinted deliberately unlike it. That
   chip is accent because a mention is addressed AT somebody; this one takes
   the same neutral wash the sent chip does, because a session reference is a
   destination rather than an alert (see `.session-link[data-session-id]`).
   Accent on both would spend the one distinction the two pills exist to draw.

   It reaches 4px rather than the mention's 5px, and that one pixel keeps it out
   of the word-spacing trade. The projection reserves one thin display-only
   space outside each end of the painted token, in both the field and mirror.
   That gives this link real margin without widening every ordinary space in the
   draft or moving the painted text away from the native caret.

   Where the mention paints its wash on a pseudo-element, this one paints on
   the span, and that is the difference between the two that is NOT taste. A
   pseudo-element is one rectangle over the whole inline box, which is fine for
   a name and wrong for a forty-character id: on a phone it wraps, and one
   rectangle over two lines is a slab lying across the sentence rather than a
   pill around the id. `box-decoration-break: clone` gives each line fragment
   its own capsule.

   The painted 4px still has to come from somewhere that is not padding, because
   horizontal padding on an inline box moves every glyph after it and the
   mirror may not move a glyph. Two offset copies of the box do it instead: an
   outer box-shadow is clipped to OUTSIDE the border box, so each copy shows
   only as a 5px sliver at one end, the two never overlap, and the wash stays
   one flat translucent value rather than doubling where they meet. Vertical
   padding is safe (it does not affect line layout) and is what gives the
   capsule its height. */
.composer-hl .cmp-session {
	--cmp-session-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6.75 5.25H17.25C18.3546 5.25 19.25 6.14543 19.25 7.25V14.25C19.25 15.3546 18.3546 16.25 17.25 16.25H11.25L7.25 19.25V16.25H6.75C5.64543 16.25 4.75 15.3546 4.75 14.25V7.25C4.75 6.14543 5.64543 5.25 6.75 5.25Z'/%3E%3C/svg%3E");
	padding: 2px 0;
	/* Squircle capsule, like the mention pill above and the sent chip. The
	   `corner-shape` is not optional decoration here: this rule is
	   hand-written, so it gets none of the grant base.css gives a `rounded-*`
	   utility, and without it the pill is the one round-ended object in a
	   surface of squircles. */
	border-radius: 999px; corner-shape: var(--cs);
	background: var(--hover);
	box-shadow:
		4px 0 0 var(--hover),
		-4px 0 0 var(--hover);
	-webkit-box-decoration-break: clone;
	box-decoration-break: clone;
}
/* Once an omitted archived reference resolves, the archive crate replaces the
   conversation glyph without changing the mirror's metrics. */
.composer-hl .cmp-session.cmp-archived {
	--cmp-session-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23000' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='4' y='4.75' width='16' height='4' rx='1'/%3E%3Cpath d='M5.5 8.75V17.25C5.5 18.3546 6.39543 19.25 7.5 19.25H16.5C17.6046 19.25 18.5 18.3546 18.5 17.25V8.75'/%3E%3Cpath d='M10 12.25H14'/%3E%3C/svg%3E");
}
.composer-hl .cmp-session[data-hover] {
	background: var(--hover-strong);
	box-shadow:
		4px 0 0 var(--hover-strong),
		-4px 0 0 var(--hover-strong);
}
/* The glyph, painted into the slot the `os-` / `bks-` prefix lends it — the
   same trade the face makes with the `@`, and for the same reason: the mirror
   may not take a pixel of width, so a picture can only go where the text
   already is. The prefix is what the draft can most afford to lose, because
   the glyph beside it names a session far better than three characters of it
   did, and the text underneath is untouched, so the caret, the selection and a
   copy still see the whole id.

   Sized in `em` because the slot is (the phone composer sets 16px), and
   centred rather than pinned left: `bks-` is a character wider than `os-`, and
   centring is what keeps the glyph off both edges of either one. Masked rather
   than drawn as a background image so it takes `--text-dim` from the same
   token the sent chip's glyph uses, in both themes. */
.composer-hl .cmp-sid {
	position: relative;
	color: transparent;
}
.composer-hl .cmp-sid::before {
	content: "";
	position: absolute;
	left: 50%;
	top: 50%;
	width: 1.25em;
	height: 1.25em;
	transform: translate(-50%, -50%);
	background: var(--text-dim);
	-webkit-mask: var(--cmp-session-glyph) center / contain no-repeat;
	mask: var(--cmp-session-glyph) center / contain no-repeat;
}
/* The same glyph for a named reference, which has no prefix to lend it a slot:
   the projection reserves one in front of the title instead
   (SESSION_GLYPH_SLOT in lib/composer-highlight.ts). Narrower than the id's,
   because that one is centred over three or four characters of room while this
   sits in a slot cut to its own size. */
.composer-hl .cmp-sglyph {
	position: relative;
	color: transparent;
}
.composer-hl .cmp-sglyph::before {
	content: "";
	position: absolute;
	left: 50%;
	top: 50%;
	width: 1.1em;
	height: 1.1em;
	transform: translate(-50%, -50%);
	background: var(--text-dim);
	-webkit-mask: var(--cmp-session-glyph) center / contain no-repeat;
	mask: var(--cmp-session-glyph) center / contain no-repeat;
}
html[data-theme="light"] .composer-hl .cmp-code {
	background: rgba(149, 59, 57, 0.1);
	color: #953b39;
}
html[data-theme="light"] .composer-hl .cmp-fence {
	background: rgba(0, 0, 0, 0.05);
	color: #1f2328;
}

.markdown ul,
.markdown ol {
	margin: 0 0 8px;
	padding-left: 20px;
}
.markdown li {
	margin: 3px 0;
}
.markdown li > ul,
.markdown li > ol {
	margin: 3px 0 0;
}

/* Agent answers carry their structure in h2/h3, so those have to read as
   section breaks at a glance. Two things were missing: size (h3/h4 were
   SMALLER than the 14px body, so a heading was distinguishable only by
   weight) and space above (6px, which collapsed with the previous
   paragraph's own margin — a heading sat as close to the block it ended as
   to the one it introduces). The ramp mirrors the native app's, scaled from
   its 17pt body to ours: +29% / +14% / +7% / body, semibold throughout, with
   optical tracking tightening as the size grows. Space is asymmetric on
   purpose — a heading belongs to what follows it. */
.markdown h1,
.markdown h2,
.markdown h3,
.markdown h4 {
	margin: 16px 0 4px;
	line-height: 1.3;
	font-weight: 600;
	letter-spacing: -0.012em;
	color: var(--text);
}
.markdown h1 {
	font-size: var(--type-section-title);
	letter-spacing: -0.018em;
}
.markdown h2 {
	font-size: var(--type-dialog-title);
	letter-spacing: -0.016em;
}
/* The deepest two levels share the body size and are told apart by weight and
   the space above them, which is what the scale has: section / dialog / body.
   A step of its own for h4 only bought a size nobody could see. */
.markdown h3,
.markdown h4 {
	font-size: var(--type-body);
}

.markdown > :first-child {
	margin-top: 0;
}
.markdown > :last-child {
	margin-bottom: 0;
}
/* A subhead directly under its parent heading is one unit, not a new section:
   drop the full section gap so h2 + h3 don't drift apart. */
.markdown :is(h1, h2, h3, h4) + :is(h1, h2, h3, h4) {
	margin-top: 8px;
}

.markdown blockquote {
	margin: 2px 0 8px;
	padding: 3px 12px;
	/* Not the accent: a red rule down the side of a quotation reads as a
	   warning callout. Same call as the session chip and the PR token. */
	border-left: 2px solid var(--border-strong);
	color: var(--text-dim);
}

.markdown table {
	border-collapse: collapse;
	margin: 2px 0 10px;
	font-size: var(--type-label);
	display: block;
	overflow-x: auto;
	max-width: 100%;
}
.markdown th {
	text-align: left;
	font-size: var(--type-meta);
	font-weight: 600;
	letter-spacing: -0.01em;
	color: var(--text-faint);
	border-bottom: 1px solid var(--border-strong);
	padding: 4px 16px 5px 0;
}
.markdown td {
	border-bottom: 1px solid var(--border);
	padding: 6px 16px 6px 0;
	vertical-align: top;
}
.markdown tr:last-child td {
	border-bottom: none;
}

.markdown hr {
	border: none;
	border-top: 1px solid var(--border);
	margin: 12px 0;
}

.markdown .md-details {
	margin: 8px 0;
	border: 1px solid var(--border);
	border-radius: calc(7px * var(--rf)); corner-shape: var(--cs);
	background: var(--bg-surface);
	overflow: hidden;
}
.markdown .md-details > summary {
	display: flex;
	align-items: center;
	gap: 7px;
	padding: 9px 11px;
	cursor: pointer;
	font-weight: 600;
	color: var(--text);
	list-style: none;
}
.markdown .md-details > summary::-webkit-details-marker {
	display: none;
}
.markdown .md-details > summary::before {
	content: "›";
	color: var(--text-faint);
	font-size: 17px;
	line-height: 1;
	transition: transform var(--dur-micro) var(--ease);
}
.markdown .md-details[open] > summary::before {
	transform: rotate(90deg);
}
.markdown .md-details-body {
	padding: 10px 12px;
	border-top: 1px solid var(--border);
}
.markdown sub {
	font-size: 0.78em;
	color: var(--text-faint);
}
.md-image-link {
	display: inline-block;
	line-height: 0;
}
.md-image,
.markdown img {
	max-width: 100%;
	max-height: 360px;
	height: auto;
	border-radius: calc(8px * var(--rf)); corner-shape: var(--cs);
	border: 1px solid rgba(127, 127, 127, 0.25);
	margin: 6px 0;
	display: block;
}
/* Chat images open the media lightbox (delegated handler in MediaLightbox.tsx) */
.md-image {
	cursor: zoom-in;
}
/* An <img> written as raw HTML in PR prose (html-sanitize.ts tags them). The
   block treatment above is right for a pasted screenshot and wrong here: bots
   use raw images for 16px avatars and status badges inside a table cell, where
   a block with a border and a 6px margin breaks the row. Keep the author's own
   sizing, only capped to the container. */
.markdown img.md-inline-image {
	display: inline-block;
	vertical-align: middle;
	max-height: none;
	margin: 0;
	border: 0;
	border-radius: 0;
}
/* An image in a table cell is a badge, not a figure: bots put a status dot
   beside the word it labels, and a block image breaks the row onto two lines. */
.markdown td img,
.markdown th img {
	display: inline-block;
	vertical-align: middle;
	margin: 0;
	border: 0;
	border-radius: 0;
}
/* Rendered ```mermaid diagram (MarkdownBody swaps the fence for the SVG).
   The diagram scrolls sideways inside its well, so the expand control is a
   sibling of that scroller rather than a child: inside it, a wide diagram
   would carry the button off the edge as you scrolled. */
.md-mermaid-wrap {
	position: relative;
	margin: 2px 0 8px;
}
.md-mermaid {
	padding: 12px;
	background: var(--diagram-canvas);
	border: 1px solid var(--border);
	border-radius: calc(8px * var(--rf)); corner-shape: var(--cs);
	overflow-x: auto;
	/* The whole chart opens the viewer, the way a picture in the transcript
	   does; the button beside it is the discoverable and keyboard-reachable
	   route to the same place (MediaLightbox.tsx). */
	cursor: zoom-in;
}
.md-mermaid svg {
	display: block;
	margin: 0 auto;
	max-width: 100%;
	height: auto;
}
.md-video {
	max-width: 100%;
	max-height: 480px;
	border-radius: calc(8px * var(--rf)); corner-shape: var(--cs);
	border: 1px solid rgba(127, 127, 127, 0.25);
	margin: 6px 0;
	display: block;
	background: #000;
}
.md-video-wrap {
	position: relative;
	line-height: 0;
}
/* Always visible (not hover-gated) so it works on touch/iOS. top offsets the
   video's own 6px top margin inside the wrapper; a diagram's margin is on the
   wrapper instead, so it sits at the plain inset. */
.md-video-expand,
.md-diagram-expand {
	position: absolute;
	top: 12px;
	right: 6px;
	display: grid;
	place-items: center;
	width: 28px;
	height: 28px;
	padding: 0;
	border: 0;
	border-radius: 999px;
	background: rgba(0, 0, 0, 0.55);
	color: #fff;
	cursor: pointer;
	opacity: 0.75;
}
.md-diagram-expand {
	top: 6px;
}
.md-video-expand:hover,
.md-diagram-expand:hover {
	opacity: 1;
	background: rgba(0, 0, 0, 0.75);
}

/* ── Controls on a rendered code fence ─────────────────────────
   Copy and per-block wrapping are built by lib/code-copy.ts as siblings of
   the <pre>. They therefore stay fixed while an unwrapped block scrolls, and
   never become part of selected or copied code. Renderer-emitted markup is
   also why these styles live here rather than in a component utility. */
.md-code-wrap {
	position: relative;
}
.md-code-wrap[data-wrapped="false"] > pre {
	white-space: pre;
	overflow-wrap: normal;
}
/* Keep the first line clear of both visible icon controls. */
.md-code-wrap > pre {
	padding-right: 80px;
}
.md-code-controls {
	position: absolute;
	top: 6px;
	right: 6px;
	display: flex;
	align-items: center;
	gap: 4px;
}
.md-code-copy,
.md-code-settings-trigger {
	display: grid;
	place-items: center;
	width: 32px;
	height: 32px;
	padding: 0;
	border: 0;
	border-radius: calc(12px * var(--rf)); corner-shape: var(--cs);
	background: transparent;
	color: var(--text-faint);
	cursor: pointer;
}
.md-code-settings-trigger {
	position: relative;
}
.md-code-settings-trigger::before {
	content: "";
	position: absolute;
	inset: 4px;
	border-radius: calc(7px * var(--rf)); corner-shape: var(--cs);
	background: transparent;
	pointer-events: none;
}
.md-code-settings-trigger > svg {
	position: relative;
}
.md-code-copy:hover {
	background: var(--hover);
	color: var(--text-dim);
}
.md-code-settings-trigger:hover,
.md-code-settings-trigger[data-open] {
	color: var(--text-dim);
}
.md-code-settings-trigger:hover::before,
.md-code-settings-trigger[data-open]::before {
	background: var(--hover);
}
.md-code-copy:focus-visible,
.md-code-settings-trigger:focus-visible {
	outline: 2px solid var(--accent);
	outline-offset: 2px;
}
/* Both glyphs share one cell, so the swap to the check carries no layout and
   the button cannot shift under the pointer mid-click. */
.md-code-copy-glyph {
	grid-area: 1 / 1;
	display: grid;
	place-items: center;
}
.md-code-copy-glyph[data-state="done"] {
	opacity: 0;
	color: var(--green);
}
.md-code-copy[data-copied] .md-code-copy-glyph[data-state="idle"] {
	opacity: 0;
}
.md-code-copy[data-copied] .md-code-copy-glyph[data-state="done"] {
	opacity: 1;
}

/* This popup cannot use the Base UI primitive because MarkdownBody owns the
   fence as injected HTML rather than JSX. Its material, edge, radius and type
   are the same tokens as ui/popover. */
.md-code-settings {
	position: absolute;
	top: 40px;
	right: 0;
	z-index: 2;
	display: flex;
	align-items: center;
	gap: 16px;
	width: min(280px, calc(100vw - 24px));
	box-sizing: border-box;
	padding: 12px;
	border-radius: calc(16px * var(--rf)); corner-shape: var(--cs);
	background: var(--popup-surface);
	box-shadow:
		0 2px 6px -2px color-mix(in srgb, black 5%, transparent),
		0 10px 28px -8px color-mix(in srgb, black 10%, transparent),
		0 0 0 1px color-mix(in srgb, var(--popup-ring) 65%, transparent);
	color: var(--text);
}
.md-code-settings[hidden] {
	display: none;
}
.md-code-settings-copy {
	min-width: 0;
	flex: 1;
}
.md-code-settings-title {
	font-size: var(--type-label);
	font-weight: 600;
	line-height: 1.35;
}
.md-code-settings-description {
	margin-top: 2px;
	font-size: var(--type-meta);
	line-height: 1.4;
	color: var(--text-dim);
}

/* A compact version of the shared switch shape. role=switch and aria-checked
   are set in lib/code-copy.ts. The 44px button is its hit target; the 44×20
   track remains the same compact macOS shape as ui/switch. */
.md-code-wrap-toggle {
	position: relative;
	flex: none;
	width: 44px;
	height: 44px;
	padding: 0;
	border: 0;
	background: transparent;
	cursor: pointer;
}
.md-code-wrap-toggle::before,
.md-code-wrap-toggle::after {
	content: "";
	position: absolute;
	left: 0;
	top: 12px;
	height: 20px;
	border-radius: 999px;
}
.md-code-wrap-toggle::before {
	width: 44px;
	background: var(--bg-active);
}
.md-code-wrap-toggle::after {
	left: 2px;
	top: 14px;
	width: 26px;
	height: 16px;
	background: var(--on-accent-control, var(--on-accent));
	box-shadow:
		0 1px 3px color-mix(in srgb, var(--text) 22%, transparent),
		0 0 0 1px color-mix(in srgb, var(--text) 7%, transparent);
	transition: translate var(--dur-micro) var(--ease);
}
.md-code-wrap[data-wrapped="true"] .md-code-wrap-toggle::before {
	background: var(--accent-control, var(--accent));
}
.md-code-wrap[data-wrapped="true"] .md-code-wrap-toggle::after {
	translate: 14px 0;
}
.md-code-wrap-toggle:focus-visible {
	outline: 2px solid var(--accent);
	outline-offset: 2px;
	border-radius: 999px;
}

/* Both icon controls remain visible: code wrapping moved behind settings, so
   copy immediately to its left also needs to read as a stable paired action.
   Touch keeps a 44px target for both. */
@media (hover: none) and (pointer: coarse) {
	.md-code-controls {
		top: 4px;
		right: 4px;
	}
	.md-code-copy,
	.md-code-settings-trigger {
		width: 44px;
		height: 44px;
	}
	.md-code-settings {
		top: 52px;
	}
	.md-code-wrap > pre {
		padding-right: 104px;
	}
}

/* ── Chips inside rendered markdown ──────────────────────────
   Both of these style markup that lib/markdown.ts writes as an innerHTML
   string, so there is no JSX to hang utilities on — the same case as
   `.markdown` above, and the reason they live here rather than migrating.

   Position in this file is load-bearing: `a.pr-ref` ties `.markdown a` on
   specificity (0,1,1) and wins only by sitting later, so keep this block
   below the `.markdown` rules. */

/* The baseline the whole family sits on. An inline-flex takes its baseline
   from its first flex item, and for every chip below that item is a glyph box
   with no text in it, so the chip's own words rode above the sentence's and
   each one carried a hand-tuned `vertical-align` in em to put them back. Those
   were measured in one font, and the app renders in SF, Inter or Segoe
   depending on the machine, so they were only ever right on the machine they
   were tuned on — the person chip's face fallback was 5px out.

   A zero-width text run as the first item hands the container its LABEL's
   baseline instead, which is correct in any font and needs no number. The
   negative margin cancels the `gap` it would otherwise open before the glyph.
   Measured at exactly 0 against the surrounding sentence for all four. */
a.asset-ref::before,
.session-link[data-session-id]::before,
.automation-link[data-automation-id]::before,
a.person-chip::before,
a.pr-ref::before {
	content: "\200b";
	flex: none;
	width: 0;
	margin-inline-end: -4px;
}

/* One set of numbers for the four of them, in one place, because four copies
   of "about 6 and about 8" drifted into 5/6 on one side and 8/9 on the other
   and a sentence holding two chips showed it. What the eye compares is the air
   at the two ENDS of the pill, so the label side is the larger number: the
   glyph carries ~3px of its own inside its box, and a letter ends on a stem
   with almost none. That lands every chip within a pixel of even.

   The one shape this cannot make even is a chip whose leading item is a
   PHOTO, which is dense to the edge of its box with no air of its own, so the
   face sits closer to the pill than a glyph does. Growing the number to match
   would leave the face floating in a hole rather than reading as the head of
   the chip. */
a.asset-ref,
.session-link[data-session-id],
.automation-link[data-automation-id],
a.person-chip,
a.pr-ref {
	display: inline-flex;
	align-items: center;
	gap: 4px;
	/* Never wider than the bubble it sits in; the label ellipsizes instead of
	   pushing the chip past the edge. */
	max-width: 100%;
	/* Its own line box. Without this the paragraph's 24px lands inside the pill
	   and pads it out, half of that under the baseline. */
	line-height: 1.15;
	/* Top and bottom are unequal on purpose, and that is what makes them LOOK
	   equal: descenders fill the room under the baseline while nothing fills
	   the room over the caps, so a balanced pill reads tight up there. */
	padding: 2.5px 7px 1px 4.5px;
	cursor: pointer;
	text-decoration: none;
	white-space: nowrap;
}

/* Every chip below is as tall as the words inside it and no taller: the label's
   own line box at 1.15, a glyph no bigger than that box, and 2.5px of room over
   it against 1px under. At the old 1.3 leading with an 18px glyph they stood
   23-25px tall inside a 24px line, so a paragraph carrying one had its leading
   pushed apart and read as a slab dropped into the sentence. They now land at
   19-21px, next to the 19px of an inline `code` capsule, which is the other
   object a sentence can contain. */

/* A scratch asset named in an answer. The file glyph and contained surface
   distinguish it from an ordinary web link without turning a sentence into a
   row of large cards; the full path remains in the link's title. */
a.asset-ref {
	border: 0;
	border-radius: calc(9px * var(--rf)); corner-shape: var(--cs);
	background: var(--accent-soft);
	color: var(--text);
	transition: background var(--dur-micro) var(--ease);
}
a.asset-ref:hover {
	background: color-mix(in srgb, var(--accent) 20%, transparent);
	text-decoration: none;
}
a.asset-ref:active {
	background: color-mix(in srgb, var(--accent) 26%, transparent);
}
a.asset-ref:focus-visible {
	outline: 2px solid var(--accent-ink);
	outline-offset: 2px;
}
.asset-ref-icon {
	display: inline-grid;
	width: 16px;
	height: 16px;
	flex: none;
	place-items: center;
	color: var(--accent-ink);
	/* Onto the cap band, same 1px as the three chips below. */
	transform: translateY(-1px);
}
.asset-ref-icon svg {
	width: 16px;
	height: 16px;
	stroke: currentColor;
	stroke-width: 1.5;
	stroke-linecap: round;
	stroke-linejoin: round;
}
.asset-ref-label {
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
}
.markdown a.asset-ref code {
	padding: 0;
	border-radius: 0;
	background: transparent;
	color: inherit;
	font-family: inherit;
	font-size: inherit;
}

/* A session reference rendered by markdown.ts as a clickable link into that
   session (e.g. an orchestrator jumping into the worker it spawned). Labelled
   with the session's title where we know it, and built from the same parts as
   the PR chip below: a quiet wash, a glyph naming what opens, and room around
   the label. Deliberately not accent-coloured: a cross-reference is a
   destination, not an alert, and the accent chip read as an error inside a
   sentence. The outlined, near-padless earlier form read as a stray border
   rather than an object you can click. Scoped on
   the chip's data-session-id attribute: bare .session-link is ALSO the viewer
   header's Linear/Plain pills (which carry their own literal ↗), and an
   unscoped rule here clobbered them into mono accent chips with a second
   arrow. */
.session-link[data-session-id],
.automation-link[data-automation-id] {
	font-size: 0.95em;
	background: var(--hover);
	border: 0;
	color: var(--text);
	/* Same corner as the asset chip above: at this height 9px (12.15 once
	   --rf scales it) takes the full box, so the chip reads as a capsule
	   around its label rather than a rounded rectangle sitting in prose. */
	border-radius: calc(9px * var(--rf)); corner-shape: var(--cs);
	transition:
		background var(--dur-micro) var(--ease),
		scale var(--dur-micro) var(--ease);
}
.session-link[data-session-id]:hover,
.automation-link[data-automation-id]:hover {
	background: var(--hover-strong);
	/* the chip lights up instead: .markdown a:hover would underline it, which
	   reads as a text link striking through the chip's own fill */
	text-decoration: none;
}
.session-link[data-session-id]:active,
.automation-link[data-automation-id]:active {
	background: var(--hover-strong);
	scale: 0.96;
}
.session-link[data-session-id]:focus-visible,
.automation-link[data-automation-id]:focus-visible {
	outline: 2px solid var(--accent-ink);
	outline-offset: 2px;
}
.session-link-icon,
.automation-link-icon {
	display: inline-grid;
	/* Never taller than the label's own line box, or the glyph rather than the
	   words decides how tall the chip is — which is what made it a slab. */
	width: 16px;
	height: 16px;
	flex: none;
	place-items: center;
	color: var(--text-dim);
	/* Flex centres the box on the LINE box, which sits 1px lower than the cap
	   band the eye reads the label by (half the 2px ascent/descent gap above).
	   Undo that, or the glyph looks like it is sinking past the words. */
	transform: translateY(-1px);
}
.session-link-icon svg,
.automation-link-icon svg {
	width: 16px;
	height: 16px;
	stroke: currentColor;
	stroke-width: 1.5;
	stroke-linecap: round;
	stroke-linejoin: round;
}
.session-link-label,
.automation-link-label {
	min-width: 0;
	overflow: hidden;
	text-overflow: ellipsis;
}
/* No title yet (archived, not polled): the label is a raw id, and mono is what
   makes it read as one rather than as a mangled sentence. */
.session-link[data-session-label="id"] .session-link-label {
	font-family: var(--mono);
	font-size: 0.95em;
}
.automation-link[data-automation-label="id"] .automation-link-label {
	font-family: var(--mono);
	font-size: 0.95em;
}
/* That session's agent is mid-run. The glyph becomes the same yellow dot the
   sidebar uses for a live run, so the chip answers "is it still going?"
   before you click. Static here: a dot breathing inside a paragraph pulls the
   eye off the sentence. */
.session-link[data-session-running] .session-link-icon svg {
	display: none;
}
/* Painted into the glyph's own box instead of being a 7px box of its own, so
   the running chip is the same size as the idle one and the dot lands where
   the glyph did. */
.session-link[data-session-running] .session-link-icon::before {
	content: "";
	width: 16px;
	height: 16px;
	background: radial-gradient(circle at 50% 50%, var(--yellow) 3.5px, transparent 3.6px);
}

/* An @-mention of a teammate, minted by markdown.ts. Third member of the chip
   family above, and it follows their rules: the face is the first flex item,
   so the sentence takes its baseline from that box rather than from the label,
   and the padding is unequal on purpose so the name reads centred. The accent
   fill is deliberate — a mention is addressed at somebody, so it should carry
   more weight in a paragraph than a session or a PR does. */
a.person-chip {
	/* The two numbers the family cannot share. Every other chip leads with a
	   glyph, which carries about 3px of its own air inside its box; a face is a
	   picture, dense to the edge of its box, so it needs that room from the
	   padding instead and gives it back on the label side. Sharing the family's
	   pair left the pill lopsided: 4.5px of air before the face against 7.4px
	   after the last letter. At 5.5 and 6 it measures even. */
	padding-right: 6px;
	padding-left: 5.5px;
	font-size: 0.95em;
	font-weight: 560;
	background: color-mix(in srgb, var(--accent) 12%, transparent);
	color: var(--accent-ink);
	border: 0;
	/* The same corner as the three chips around it. It was a true capsule,
	   which at this height reads as a different object in the same sentence:
	   a squircle capsule is what every other pill in the app is. */
	border-radius: calc(9px * var(--rf)); corner-shape: var(--cs);
	transition: background var(--dur-micro) var(--ease);
}
a.person-chip:hover {
	background: color-mix(in srgb, var(--accent) 20%, transparent);
	text-decoration: none;
}
a.person-chip:focus-visible {
	outline: 2px solid var(--accent-ink);
	outline-offset: 2px;
}
.person-chip-face {
	/* Two smaller than the session chip's glyph box: a picture is dense to the
	   edge of its box, so at the glyph's 16px it filled the pill and the two
	   sets of corners collided. */
	width: 14px;
	height: 14px;
	flex: none;
	/* The same picture the app draws everywhere else: UserAvatar's squircle
	   tile at 32%, not a circle. A round face here was the one person-shaped
	   thing in the product that was not the product's own avatar. */
	border-radius: calc(32% * var(--rp)); corner-shape: var(--cs);
	object-fit: cover;
	/* Same 1px lift as the session chip's glyph: flex centres on the line box,
	   which sits below the cap band the eye reads the name by. */
	transform: translateY(-1px);
}
/* `.markdown img` ties this on class count and wins by being later, so inside
   a transcript it dragged the styling of a POSTED PICTURE onto the face: 6px
   of vertical margin, which stood a chip carrying a photo at 31px against the
   initial fallback's 19px, plus a hairline and the 8px image corner. Reset
   what that rule sets; same case as `.markdown img.md-inline-image`. */
.markdown img.person-chip-face {
	height: 14px;
	max-height: none;
	margin: 0;
	border: 0;
	border-radius: calc(32% * var(--rp)); corner-shape: var(--cs);
}
.person-chip-initial {
	display: inline-grid;
	place-items: center;
	background: var(--bg-active);
	color: var(--text-dim);
	font-size: 10px;
	line-height: 1;
}

/* A pull-request reference (`#5528` or an explicit GitHub PR link) linked into
   our own review surface by markdown.ts. The branch glyph and quiet wash make
   it read as a PR object rather than another highlighted word; its live state
   colours the surface while the full state stays in the tooltip. Not
   scoped under .markdown: the chip turns up in every surface that renders
   session markdown, and a few of those containers don't carry that class. */
a.pr-ref {
	background: var(--hover);
	color: var(--text);
	border: 0;
	/* Capsule corner, same as the session chip above. */
	border-radius: calc(9px * var(--rf)); corner-shape: var(--cs);
	font-weight: 600;
	font-variant-numeric: tabular-nums;
	transition:
		background var(--dur-micro) var(--ease),
		scale var(--dur-micro) var(--ease);
}
a.pr-ref:hover {
	background: var(--hover-strong);
	/* the token lights up instead — an underline reads as a strike through
	   its own fill (same call as the session chip) */
	text-decoration: none;
}
a.pr-ref:active {
	background: var(--hover-strong);
	scale: 0.96;
}
a.pr-ref:focus-visible {
	outline: 2px solid var(--accent-ink);
	outline-offset: 2px;
}
.pr-ref-icon {
	display: inline-grid;
	width: 16px;
	height: 16px;
	flex: none;
	place-items: center;
	color: var(--text-dim);
	/* Flex centres the glyph on the line box, which hangs lower than the cap
	   band the eye reads the label by. Onto that band, same 1px as the
	   session chip above. */
	transform: translateY(-1px);
}
.pr-ref-icon svg {
	width: 16px;
	height: 16px;
	stroke: currentColor;
	stroke-width: 1.5;
	stroke-linecap: round;
	stroke-linejoin: round;
}
a.pr-ref[data-pr-tone="green"] {
	background: var(--green-soft);
	color: var(--green);
}
a.pr-ref[data-pr-tone="green"]:hover {
	background: color-mix(in srgb, var(--green) 20%, transparent);
}
a.pr-ref[data-pr-tone="purple"] {
	background: color-mix(in srgb, var(--purple) 12%, transparent);
	color: var(--purple);
}
a.pr-ref[data-pr-tone="purple"]:hover {
	background: color-mix(in srgb, var(--purple) 20%, transparent);
}
a.pr-ref[data-pr-tone="red"] {
	background: var(--red-soft);
	color: var(--red);
}
a.pr-ref[data-pr-tone="red"]:hover {
	background: color-mix(in srgb, var(--red) 18%, transparent);
}
a.pr-ref[data-pr-tone="yellow"] {
	background: var(--yellow-soft);
	color: var(--yellow);
}
a.pr-ref[data-pr-tone="yellow"]:hover {
	background: color-mix(in srgb, var(--yellow) 20%, transparent);
}
a.pr-ref[data-pr-tone="green"] .pr-ref-icon {
	color: var(--green);
}
a.pr-ref[data-pr-tone="purple"] .pr-ref-icon {
	color: var(--purple);
}
a.pr-ref[data-pr-tone="red"] .pr-ref-icon {
	color: var(--red);
}
a.pr-ref[data-pr-tone="yellow"] .pr-ref-icon {
	color: var(--yellow);
}

/* A commit sha, which markdown.ts leaves in the monospace capsule it was
   written in rather than making a pill of it. A sha IS a code identifier, and
   the sentences these turn up in name two or three at a time ("reverting
   `4ed1ef09` + `437cba77`"), which as pills reads as a row of boxes.

   So the capsule below repeats `.markdown code` on purpose (it is an <a>, not
   a <code>, and it also has to hold up on the surfaces that render session
   markdown without the `.markdown` class), and the only thing added is the
   affordance: the dotted underline a term with a definition has always had,
   solid on hover. `data-commit-unknown` is set by the hover card when no
   checkout has the sha, which takes the promise back off a reference we
   cannot answer.

   Both element names are spelled out for the same reason `a.pr-ref` is: at
   (0,1,0) a bare `.commit-ref` loses the underline to `.markdown a`'s
   `text-decoration: none`, and only ties it at (0,1,1). */
a.commit-ref,
span.commit-ref {
	font-family: var(--mono);
	font-size: 0.9em;
	padding: 1.5px 6px;
	border-radius: calc(7px * var(--rf)); corner-shape: var(--cs);
	background: rgba(221, 225, 240, 0.07);
	color: #dde1f0;
	cursor: pointer;
	text-decoration-line: underline;
	text-decoration-style: dotted;
	/* Its own ink, held back so the line reads as a hint under the word
	   rather than a second stroke through it. */
	text-decoration-color: color-mix(in srgb, currentColor 40%, transparent);
	text-underline-offset: 2px;
	transition: background var(--dur-micro) var(--ease);
}
a.commit-ref:hover,
span.commit-ref:hover {
	background: rgba(221, 225, 240, 0.13);
	text-decoration-style: solid;
	text-decoration-color: color-mix(in srgb, currentColor 55%, transparent);
}
a.commit-ref:focus-visible,
span.commit-ref:focus-visible {
	outline: 2px solid var(--accent-ink);
	outline-offset: 2px;
}
a.commit-ref[data-commit-unknown],
span.commit-ref[data-commit-unknown] {
	cursor: text;
	text-decoration-line: none;
}
html[data-theme="light"] a.commit-ref,
html[data-theme="light"] span.commit-ref {
	background: rgba(149, 59, 57, 0.09);
	color: #953b39;
}
html[data-theme="light"] a.commit-ref:hover,
html[data-theme="light"] span.commit-ref:hover {
	background: rgba(149, 59, 57, 0.16);
}

/* The line-number gutter shiki writes into highlighted output
   (lib/shiki-engine.ts). Renderer-emitted, same as the chips above. The code
   well overrides the colour for its own darker surface, from the markup. */
.shiki-gutter {
	color: var(--text-faint);
	user-select: none;
}

/* ── Docs (`.markdown wiki-doc`) ─────────────────────────────
   A wider, roomier variant of `.markdown` for a whole document rather than a
   message: a longer measure and looser leading, with the heading scale reset
   to suit a page that opens on an h1. `.wiki-doc` is a class on JSX, but
   everything it styles is rendered markdown, so it belongs here beside
   `.markdown` rather than becoming utilities.

   These tie `.markdown`'s own heading rules on specificity (0,1,1) and win by
   sitting later — keep them below that block. */
.wiki-doc {
	max-width: 780px;
	font-size: var(--type-body);
	line-height: 1.65;
}
.wiki-doc h1 {
	font-size: var(--type-section-title);
	margin: 0 0 14px;
}
.wiki-doc h2 {
	font-size: var(--type-dialog-title);
	margin: 22px 0 10px;
}
.wiki-doc h3 {
	/* Body size, the same as `.markdown h3`: weight and the space above carry
	   it. Restated here only because this block re-declares its own margins. */
	font-size: var(--type-body);
	margin: 18px 0 8px;
}
.wiki-doc img {
	max-width: 100%;
	border-radius: calc(8px * var(--rf)); corner-shape: var(--cs);
}
.wiki-doc a {
	color: var(--link);
}

/* ── Transcript liveness ─────────────────────────────────────
   This rule is the only implementation of the streaming caret. It hangs off
   the `.msg-streaming` ancestor, so it cannot be expressed as a utility on the
   element that paints. The class name survives on the markup purely as its
   hook (see lib/msg-classes.ts).

   It lives here rather than in legacy.css, which is empty, and being
   here puts its duration in the same file as the exception that restates it:
   the two 1s values have to agree or the indicator changes rate for anyone
   with the preference set.

   The caret animates `msg-caret-pulse`, not the bare `pulse` the legacy rule
   used. `pulse` was never ours: tailwind.css emits its own
   `@keyframes pulse { 50% { opacity: .5 } }` unlayered and last, which had been
   quietly beating legacy.css's `0%,100% { 1 } 50% { 0.35 }` — so 0.5 is what
   shipped, and it is what this restates, the same call ui/status.tsx and
   lib/session-tab-classes made when they hit this. Those two keep spelling it
   `animate-[pulse_…]`, which is the only reason the utility sheet emits those
   keyframes at all; a rule here should not be resting on that. */
.msg-streaming .msg-body-assistant::after {
	content: "";
	display: inline-block;
	width: 7px;
	height: 14px;
	margin-left: 3px;
	vertical-align: -2px;
	border-radius: calc(2px * var(--rf)); corner-shape: var(--cs);
	background: var(--accent);
	animation: msg-caret-pulse 1s ease-in-out infinite;
}
@keyframes msg-caret-pulse {
	0%,
	100% {
		opacity: 1;
	}
	50% {
		opacity: 0.5;
	}
}

/* ── Keyframes the markup reaches directly ───────────────────
   These are referenced only from Tailwind utilities in the components —
   `animate-[preview-spin_0.7s_linear_infinite]`, or the longhand
   `[animation:session-info-in_var(--dur)_var(--ease)]` — so there is no CSS
   rule left in legacy.css to carry them, and that file is being emptied.
   Grep for BOTH spellings before assuming one of these is unused: the
   underscores in an arbitrary value hide the name from a naive search. */

@keyframes composer-agents-pulse {
	0%, 100% { opacity: 1; }
	50% { opacity: 0.35; }
}

/* A ghost holds its place before it shows itself. Referenced from the
   longhand in ui/state's `Skeleton`, the frame every skeleton in the app
   wears.

   Most of the lists these stand in for arrive fast enough that a placeholder
   would flash and go, which is more distracting than the gap it filled — so
   the ghost is laid out at once but stays invisible for a beat, and only a
   wait long enough to notice ever gets stood in for. `both` is what does the
   holding: it keeps the 0% opacity through the delay, so the shape reserves
   its height from the first paint and the real rows replace it in place
   instead of dropping the page down when they land.

   Under prefers-reduced-motion the block above zeroes the delay along with
   the duration and the ghost simply appears. That is the right answer here:
   the beat is a comfort, not information, and there is nothing to lose by
   skipping it. */
@keyframes ghost-in {
	from { opacity: 0; }
	to { opacity: 1; }
}

/* The pixel spinner's wave. Referenced from the longhand
   `[animation-name:pixel-snake-trail]` in lib/pixel-spinner-classes. */
@keyframes pixel-snake-trail {
  0% {
    background: var(--glow-color);
    box-shadow: none;
    opacity: 0.78;
  }
  15% {
    background: var(--glow-color);
    box-shadow: none;
    opacity: 0.7;
  }
  30% {
    background: color-mix(in srgb, var(--glow-color) 70%, transparent);
    box-shadow: none;
    opacity: 0.6;
  }
  45% {
    background: color-mix(in srgb, var(--glow-color) 50%, transparent);
    box-shadow: none;
    opacity: 0.5;
  }
  60%,
  100% {
    background: color-mix(in srgb, var(--glow-color) 35%, transparent);
    box-shadow: none;
    opacity: 1;
  }
}

@keyframes preview-spin {
	to {
		transform: rotate(360deg);
	}
}

/* A highlight travelling across a word, for a status label that is its own
   loading indicator ("Queueing", "Sending"). The gradient is twice the box
   wide, so sliding the position from 100% to 0% walks the crest left to
   right across the letters. Referenced from `composerQueueSendingShimmer`
   in lib/composer-classes. */
@keyframes text-shimmer {
	from {
		background-position: 100% center;
	}
	to {
		background-position: 0% center;
	}
}

/* Deploy is on its way but butler hasn't posted the preview URL yet — there's
   nothing to link to, so the globe just breathes (a skeleton-style shimmer) in
   a muted tone to read as "loading" until the amber Building link takes over. */
@keyframes staging-shimmer {
	0%,
	100% {
		opacity: 0.35;
	}
	50% {
		opacity: 0.85;
	}
}

/* Was nested inside `@media (max-width: 720px)` in legacy.css. Top level here,
   which is equivalent: the only thing that uses it is INFO_PAGE, and that page
   renders solely when `useIsPhone()` is true, so the element cannot exist at a
   width where this would have been undefined. Un-nesting also removes a trap —
   a media-scoped keyframe whose consumer is not scoped the same way animates
   nothing, silently. */
@keyframes session-info-in {
	from {
		opacity: 0;
		transform: translateY(8px);
	}
	to {
		opacity: 1;
		transform: translateY(0);
	}
}

/* A transcript block arriving at the live edge fades up into place instead of
   popping. Applied one-shot via VirtualTranscriptList's `enter` flag (see
   newTailBlockKeys): only blocks whose key was not mounted by the previous
   build play it, so opening a session, hydrating history, and virtualizer
   remounts never re-run it. Travel matches session-info-in; duration --dur
   because a whole block arriving is anchored movement, not an in-place
   change. prefers-reduced-motion freezes it in the global block above. */
@keyframes transcript-enter {
	from {
		opacity: 0;
		transform: translateY(8px);
	}
	to {
		opacity: 1;
		transform: translateY(0);
	}
}

/* Edge fades so a tab clipped by the scroll boundary reads as "there's more this
   way" rather than as a broken, half-drawn tab — the strip hides its scrollbar,
   so without this an overflowing strip gives no hint that it scrolls at all.
   Same technique as the transcript's fades: a CSS scroll timeline drives the two
   widths, so there are no JS scroll listeners and no React re-renders, and each
   fade appears only when content really is past that edge. The mask sits on the
   scroller's own box, which is what puts the fade at the tabs' edge instead of
   under the pinned "+". The mask hangs off [data-overflow] (set by SessionTabs)
   rather than on the timeline alone, because a scroll timeline that goes
   inactive HOLDS its last value instead of reverting: without that gate, a strip
   that stopped overflowing would keep a stale fade dimming its first tab.
   Browsers without scroll-driven animations never get a mask at all. The fades
   track scroll position rather than elapsed time, so they're left active under
   prefers-reduced-motion.

   The two widths must be REGISTERED to animate: an unregistered custom property
   has no type, so a keyframe can only flip it at 50% instead of interpolating
   it, and the mask would jump rather than fade. Registration is also what gives
   an element that never sets one the 0px resting value. They live here beside
   the keyframes that drive them — @property is a global declaration, not
   component styling, and it has no class to hang on. */
@property --tabs-fade-start {
	syntax: "<length>";
	inherits: false;
	initial-value: 0px;
}
@property --tabs-fade-end {
	syntax: "<length>";
	inherits: false;
	initial-value: 0px;
}

@keyframes session-tabs-fade-start {
	from {
		--tabs-fade-start: 0px;
	}
	to {
		--tabs-fade-start: 24px;
	}
}

@keyframes session-tabs-fade-end {
	from {
		--tabs-fade-end: 24px;
	}
	to {
		--tabs-fade-end: 0px;
	}
}

/* Entrance for the sidebar's filter popover and the mini-menus it opens (see
   components/sidebar/Filters.tsx, which spells it as `animate-[hovercard-in_…]`).
   A keyframe set has no class to hang on, so it can't be a utility; it sits here
   with the rest of them rather than in the component. Only the `from` step is
   written — the resting state is the element's own, so the animation can't
   overwrite whatever the popover settles at. */
@keyframes hovercard-in {
	from {
		opacity: 0;
		transform: translateY(2px);
	}
}

@keyframes tab-drop-slot-in {
	from {
		opacity: 0;
	}
	to {
		opacity: 1;
	}
}

@keyframes update-toast-in {
	from {
		opacity: 0;
		transform: translateY(12px) scale(0.97);
	}
	to {
		opacity: 1;
		transform: translateY(0) scale(1);
	}
}

/* ─────────────────────────────────────────────────────────────
   legacy.css — empty, and staying that way.

   This file held the app's component styling before the Tailwind
   system landed. It is now at zero rules: every class that lived
   here is either a utility string beside its component (src/frontend/
   lib/*-classes.ts), a primitive in src/frontend/ui/, or a genuinely
   global rule in base.css.

   Nothing goes back in. New UI is written as utilities on the markup;
   the only rules that belong in a stylesheet at all are the global and
   theme-level ones base.css owns — tokens, resets, platform chrome,
   keyframes and @property registrations, which have no element to hang
   on. See AGENTS.md, "Frontend UI system".

   The file itself stays for now, still concatenated after base.css by
   src/server/frontend-build.ts, so scripts/css-audit.ts keeps a target
   and the "never add here" contract keeps a home. An empty file costs
   nothing at runtime: the two sheets are concatenated into one.
   ───────────────────────────────────────────────────────────── */


/* ── vendored @xterm/xterm/css/xterm.css (Shell tab) ── */
/**
 * Copyright (c) 2014 The xterm.js authors. All rights reserved.
 * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
 * https://github.com/chjj/term.js
 * @license MIT
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * Originally forked from (with the author's permission):
 *   Fabrice Bellard's javascript vt100 for jslinux:
 *   http://bellard.org/jslinux/
 *   Copyright (c) 2011 Fabrice Bellard
 *   The original design remains. The terminal itself
 *   has been extended to include xterm CSI codes, among
 *   other features.
 */

/**
 *  Default styles for xterm.js
 */

.xterm {
    cursor: text;
    position: relative;
    user-select: none;
    -ms-user-select: none;
    -webkit-user-select: none;
}

.xterm.focus,
.xterm:focus {
    outline: none;
}

.xterm .xterm-helpers {
    position: absolute;
    top: 0;
    /**
     * The z-index of the helpers must be higher than the canvases in order for
     * IMEs to appear on top.
     */
    z-index: 5;
}

.xterm .xterm-helper-textarea {
    padding: 0;
    border: 0;
    margin: 0;
    /* Move textarea out of the screen to the far left, so that the cursor is not visible */
    position: absolute;
    opacity: 0;
    left: -9999em;
    top: 0;
    width: 0;
    height: 0;
    z-index: -5;
    /** Prevent wrapping so the IME appears against the textarea at the correct position */
    white-space: nowrap;
    overflow: hidden;
    resize: none;
}

.xterm .composition-view {
    /* TODO: Composition position got messed up somewhere */
    background: #000;
    color: #FFF;
    display: none;
    position: absolute;
    white-space: nowrap;
    z-index: 1;
}

.xterm .composition-view.active {
    display: block;
}

.xterm .xterm-viewport {
    /* On OS X this is required in order for the scroll bar to appear fully opaque */
    background-color: #000;
    overflow-y: scroll;
    cursor: default;
    position: absolute;
    right: 0;
    left: 0;
    top: 0;
    bottom: 0;
}

.xterm .xterm-screen {
    position: relative;
}

.xterm .xterm-screen canvas {
    position: absolute;
    left: 0;
    top: 0;
}

.xterm-char-measure-element {
    display: inline-block;
    visibility: hidden;
    position: absolute;
    top: 0;
    left: -9999em;
    line-height: normal;
}

.xterm.enable-mouse-events {
    /* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
    cursor: default;
}

.xterm.xterm-cursor-pointer,
.xterm .xterm-cursor-pointer {
    cursor: pointer;
}

.xterm.column-select.focus {
    /* Column selection mode */
    cursor: crosshair;
}

.xterm .xterm-accessibility:not(.debug),
.xterm .xterm-message {
    position: absolute;
    left: 0;
    top: 0;
    bottom: 0;
    right: 0;
    z-index: 10;
    color: transparent;
    pointer-events: none;
}

.xterm .xterm-accessibility-tree:not(.debug) *::selection {
  color: transparent;
}

.xterm .xterm-accessibility-tree {
  font-family: monospace;
  user-select: text;
  white-space: pre;
}

.xterm .xterm-accessibility-tree > div {
  transform-origin: left;
  width: fit-content;
}

.xterm .live-region {
    position: absolute;
    left: -9999px;
    width: 1px;
    height: 1px;
    overflow: hidden;
}

.xterm-dim {
    /* Dim should not apply to background, so the opacity of the foreground color is applied
     * explicitly in the generated class and reset to 1 here */
    opacity: 1 !important;
}

.xterm-underline-1 { text-decoration: underline; }
.xterm-underline-2 { text-decoration: double underline; }
.xterm-underline-3 { text-decoration: wavy underline; }
.xterm-underline-4 { text-decoration: dotted underline; }
.xterm-underline-5 { text-decoration: dashed underline; }

.xterm-overline {
    text-decoration: overline;
}

.xterm-overline.xterm-underline-1 { text-decoration: overline underline; }
.xterm-overline.xterm-underline-2 { text-decoration: overline double underline; }
.xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; }
.xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; }
.xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; }

.xterm-strikethrough {
    text-decoration: line-through;
}

.xterm-screen .xterm-decoration-container .xterm-decoration {
	z-index: 6;
	position: absolute;
}

.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer {
	z-index: 7;
}

.xterm-decoration-overview-ruler {
    z-index: 8;
    position: absolute;
    top: 0;
    right: 0;
    pointer-events: none;
}

.xterm-decoration-top {
    z-index: 2;
    position: relative;
}



/* Derived from vs/base/browser/ui/scrollbar/media/scrollbar.css */

/* xterm.js customization: Override xterm's cursor style */
.xterm .xterm-scrollable-element > .scrollbar {
    cursor: default;
}

/* Arrows */
.xterm .xterm-scrollable-element > .scrollbar > .scra {
	cursor: pointer;
	font-size: 11px !important;
}

.xterm .xterm-scrollable-element > .visible {
	opacity: 1;

	/* Background rule added for IE9 - to allow clicks on dom node */
	background:rgba(0,0,0,0);

	transition: opacity 100ms linear;
	/* In front of peek view */
	z-index: 11;
}
.xterm .xterm-scrollable-element > .invisible {
	opacity: 0;
	pointer-events: none;
}
.xterm .xterm-scrollable-element > .invisible.fade {
	transition: opacity 800ms linear;
}

/* Scrollable Content Inset Shadow */
.xterm .xterm-scrollable-element > .shadow {
	position: absolute;
	display: none;
}
.xterm .xterm-scrollable-element > .shadow.top {
	display: block;
	top: 0;
	left: 3px;
	height: 3px;
	width: 100%;
	box-shadow: var(--vscode-scrollbar-shadow, #000) 0 6px 6px -6px inset;
}
.xterm .xterm-scrollable-element > .shadow.left {
	display: block;
	top: 3px;
	left: 0;
	height: 100%;
	width: 3px;
	box-shadow: var(--vscode-scrollbar-shadow, #000) 6px 0 6px -6px inset;
}
.xterm .xterm-scrollable-element > .shadow.top-left-corner {
	display: block;
	top: 0;
	left: 0;
	height: 3px;
	width: 3px;
}
.xterm .xterm-scrollable-element > .shadow.top.left {
	box-shadow: var(--vscode-scrollbar-shadow, #000) 6px 0 6px -6px inset;
}
