/* ══════════════════════════════════════════════════════════════════
   FLYING OX — "The Break" · surface layer
   Loaded after tokens.css. Component-level restyling that tokens
   cannot express — always ADDITIVE on stable, existing selectors
   (ids and app-authored classes). No rule here hides or removes a
   control — paint and, where the user has directed it, spacing.
   ══════════════════════════════════════════════════════════════════ */

/* ─── The rail, Venice rhythm ──────────────────────────────────────
   User-directed spacing pass (2026-09-01): the icon rail gets the
   Venice treatment — a touch wider, icons centered, doubled vertical
   air, and the mark large at the top. Every control stays present
   and clickable; only geometry of the column breathes.
   Selector = the rail's own class chain (ExpandedPanel.tsx:150).    */
div.flex.h-full.flex-shrink-0.flex-col.gap-2.border-r.border-border-light.bg-surface-primary-alt {
  width: 88px;
  /* No separator line: rail and ground are one continuous graphite
     surface now, so the hairline is noise (user-directed). Color to
     transparent rather than width 0 so nothing shifts 1px. */
  border-right-color: transparent;
  align-items: center;
  gap: 12px;
  padding-top: 10px;
  padding-bottom: 12px;
}

/* Buttons and glyphs stay stock size (user-directed: only the rail
   itself and the mark scale up). */

/* The sidebar SLOT is sized by JS inline styles: width/min/max 52px
   collapsed, 360px expanded, animated 300ms. A blanket !important
   would pin it and break the expand animation — so match ONLY the
   collapsed state via its own inline value. Expanded keeps its JS
   sizing untouched, and the transition still animates both ways
   (the attribute flips at each toggle, so 88↔360 tweens cleanly).
   Without this, the slot clips the 88px rail back to 52.           */
.relative.flex.h-full.flex-shrink-0.overflow-hidden[style*='width: 52px'] {
  width: 88px !important;
  min-width: 88px !important;
  max-width: 88px !important;
}

/* Centering the rail's children collapses the auto-width divider —
   stretch it back to full width. */
div.flex.h-full.flex-shrink-0.flex-col.gap-2.border-r.border-border-light.bg-surface-primary-alt
  > .mx-2.border-b {
  align-self: stretch;
}

/* The nav icon column: Venice spaces its icons roughly a full icon
   apart. gap-1 (4px) → 12px. The column already scrolls (its own
   overflow-y-auto), so short windows lose nothing. */
div.flex.h-full.flex-shrink-0.flex-col.gap-2.border-r.border-border-light.bg-surface-primary-alt
  > .flex.flex-col.gap-1.overflow-y-auto {
  gap: 12px;
}

/* ─── The mark IS the toggle (Venice pattern) ──────────────────────
   Venice's logo doubles as the sidebar toggle: collapsed, the mark
   sits where the toggle lives; hovering reveals the toggle glyph and
   its tooltip; expanded, the mark yields to the wordmark in the
   panel. Same here — the app's own "Open sidebar" button (the rail's
   first child) wears the ox head. The control itself is untouched:
   same button, same click, same tooltip, same aria-label. Only its
   clothing changes, and hover/keyboard-focus always reveal the
   standard affordance.
   State comes from the sidebar slot's own inline width (52px only
   when collapsed), so the expanded state keeps the stock glyph.
   ?v= busts the 2-day /assets/ cache when artwork changes.          */
.relative.flex.h-full.flex-shrink-0.overflow-hidden[style*='width: 52px']
  div.flex.h-full.flex-shrink-0.flex-col.gap-2.border-r.border-border-light.bg-surface-primary-alt
  > button:first-of-type {
  width: 64px;
  height: 52px;
  background-image: url('./ox-mark-outline.png?v=1');
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
  border-radius: 10px;
}
.relative.flex.h-full.flex-shrink-0.overflow-hidden[style*='width: 52px']
  div.flex.h-full.flex-shrink-0.flex-col.gap-2.border-r.border-border-light.bg-surface-primary-alt
  > button:first-of-type svg {
  opacity: 0;
  transition: opacity 0.18s ease;
}
.relative.flex.h-full.flex-shrink-0.overflow-hidden[style*='width: 52px']
  div.flex.h-full.flex-shrink-0.flex-col.gap-2.border-r.border-border-light.bg-surface-primary-alt
  > button:first-of-type:hover,
.relative.flex.h-full.flex-shrink-0.overflow-hidden[style*='width: 52px']
  div.flex.h-full.flex-shrink-0.flex-col.gap-2.border-r.border-border-light.bg-surface-primary-alt
  > button:first-of-type:focus-visible {
  background-image: none;
  background-color: rgb(var(--surface-hover));
}
.relative.flex.h-full.flex-shrink-0.overflow-hidden[style*='width: 52px']
  div.flex.h-full.flex-shrink-0.flex-col.gap-2.border-r.border-border-light.bg-surface-primary-alt
  > button:first-of-type:hover svg,
.relative.flex.h-full.flex-shrink-0.overflow-hidden[style*='width: 52px']
  div.flex.h-full.flex-shrink-0.flex-col.gap-2.border-r.border-border-light.bg-surface-primary-alt
  > button:first-of-type:focus-visible svg {
  opacity: 1;
}

/* ─── Exposure on the rail mark ────────────────────────────────────
   The artwork is chrome linework and it was sitting too dark against
   the rail: measured over the PNG's visible pixels (14% of the file —
   it is nearly all transparent), stroke luminance runs
   min 0 / p25 59 / median 101 / p95 255. A median of 101 against a
   ground near 15 is only about 3:1, so the mark read as grey rather
   than as metal.

   brightness() is the photographic move here: it scales, so the
   already-blown specular highlights stay at 255 and only the body of
   the linework lifts (median 101 -> ~150). The slight contrast bump
   afterwards re-separates the strokes that the lift flattens.

   Dark theme only. The light theme inverts this same asset into dark
   linework on paper (see "The mark on paper" below) — brightening
   there would fade it into the page, which is backwards. The two
   selectors are mutually exclusive, so neither has to out-specify the
   other.

   Hover and focus are excluded deliberately: those states swap the
   artwork for a plain hover chip, and `filter` applies to the whole
   element, so without this the chip and its revealed glyph would be
   brightened too. */
html.dark
  .relative.flex.h-full.flex-shrink-0.overflow-hidden[style*='width: 52px']
  div.flex.h-full.flex-shrink-0.flex-col.gap-2.border-r.border-border-light.bg-surface-primary-alt
  > button:first-of-type:not(:hover):not(:focus-visible) {
  filter: brightness(1.5) contrast(1.05);
}

/* ─── The wordmark, expanded (Venice pattern, part two) ────────────
   Extending the menu switches the brand from mark to name — in the
   SAME top-left corner where the ox sits collapsed. The expanded
   sidebar gets a 64px header band spanning its full width:
     · wordmark at the far left (abspos ::before on the aside, which
       is position:relative; the static overflow-hidden wrapper
       between them cannot clip an abspos escapee)
     · the toggle button relocates to the band's right edge, exactly
       Venice's layout — same control, same click, just repositioned
     · rail icons and panel content both start below the band.      */
/* Once ox-chrome-marks.js has a canvas up, the pseudo-element stops
   painting the flat SVG — a pseudo cannot hold a canvas, so the
   chrome is a sibling positioned to the same geometry and this rule
   gets the original out of its way. Gated on the class the script
   sets only after WebGL actually compiled, so a browser without it
   keeps the SVG and the rail looks exactly as it does today. */
html.ox-chrome-marks
  .relative.flex.h-full.flex-shrink-0.overflow-hidden:not([style*='width: 52px'])::before {
  background-image: none;
}
.relative.flex.h-full.flex-shrink-0.overflow-hidden:not([style*='width: 52px'])::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  height: 64px;
  width: 260px;
  background: url('./ox-wordmark.svg?v=1') no-repeat 34px center / auto 31.5px;
  z-index: 10;
  pointer-events: none;
}
/* The chrome canvases follow exactly the same state rules as the
   artwork they replace, or they leak into the wrong layout:

   · Expanded, the toggle button grows to the full 360px header band.
     A canvas at inset:0 would grow with it and paint an ox the width
     of the panel, so it is hidden in that state — the same state the
     PNG is already dropped in.
   · Collapsed, the wordmark's ::before does not apply, but the
     canvas mounted for it is still a child of the wrapper, so it
     would show the name on an 88px rail. Hidden there for the same
     reason.
   · On hover and keyboard focus the ox is meant to YIELD to the
     stock toggle glyph — that is the affordance that keeps this
     button legible as a button. The canvas has to yield with it. */
.relative.flex.h-full.flex-shrink-0.overflow-hidden:not([style*='width: 52px'])
  canvas.ox-chrome-head {
  display: none;
}
.relative.flex.h-full.flex-shrink-0.overflow-hidden[style*='width: 52px']
  canvas.ox-chrome-wordmark {
  display: none;
}
canvas.ox-chrome-head,
canvas.ox-chrome-wordmark {
  display: block;
}
canvas.ox-chrome-head {
  transition: opacity 0.18s ease;
}
button:hover > canvas.ox-chrome-head,
button:focus-visible > canvas.ox-chrome-head {
  opacity: 0;
}

/* Expanded, the toggle stretches across the whole header band —
   transparent, glyph pinned right — so clicking the wordmark (or
   anywhere in the band) closes the menu, mirroring how the ox
   opens it (user-directed). The wordmark ::before (z 10) shows
   through; the button (z 20) takes the clicks. */
.relative.flex.h-full.flex-shrink-0.overflow-hidden:not([style*='width: 52px'])
  div.flex.h-full.flex-shrink-0.flex-col.gap-2.border-r.border-border-light.bg-surface-primary-alt
  > button:first-of-type {
  position: absolute;
  top: 0;
  left: 0;
  /* explicit width: buttons shrink-to-fit and ignore left+right
     stretching, which parked a 36px button on top of the wordmark */
  width: 100%;
  height: 64px;
  z-index: 20;
  justify-content: flex-end;
  padding-right: 18px;
  border-radius: 0;
  background: transparent !important;
}
.relative.flex.h-full.flex-shrink-0.overflow-hidden:not([style*='width: 52px'])
  div.flex.h-full.flex-shrink-0.flex-col.gap-2.border-r.border-border-light.bg-surface-primary-alt
  > button:first-of-type:hover svg {
  color: rgb(var(--text-primary));
}
.relative.flex.h-full.flex-shrink-0.overflow-hidden:not([style*='width: 52px'])
  div.flex.h-full.flex-shrink-0.flex-col.gap-2.border-r.border-border-light.bg-surface-primary-alt {
  padding-top: 64px;
}

/* Panel content starts below the header band. padding (not a flow
   pseudo-element) because the panel's first child is h-full: with
   border-box it fits the reduced content box, nothing clips.        */
.min-h-0.flex-1.overflow-hidden.bg-surface-primary-alt {
  padding-top: 64px;
  position: relative; /* anchors the section title below */
}

/* ─── Section title ────────────────────────────────────────────────
   A small caps label in the band naming the open section. Each
   panel is recognized by an element it alone contains (the app's
   own aria-labels), via :has() — pure CSS, nothing injected into
   the DOM. Sits under the wordmark's baseline so the two never
   collide. The pseudo-element only renders when a section rule
   below sets its content.                                          */
.relative.flex.h-full.flex-shrink-0.overflow-hidden:not([style*='width: 52px'])::after {
  position: absolute;
  top: 46px;
  left: 88px; /* start after the rail: centered over the extended part only */
  right: 0;
  text-align: center;
  font-size: 10.5px;
  font-weight: 600;
  line-height: 1;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgb(var(--text-tertiary));
  pointer-events: none;
  z-index: 5;
}
.relative.flex.h-full.flex-shrink-0.overflow-hidden:not([style*='width: 52px']):has(div[aria-label='Chat History'])::after {
  content: 'Chats';
}
.relative.flex.h-full.flex-shrink-0.overflow-hidden:not([style*='width: 52px']):has(form[aria-label='Agent configuration form'])::after {
  content: 'Agents';
}
.relative.flex.h-full.flex-shrink-0.overflow-hidden:not([style*='width: 52px']):has(input[aria-label='Filter skills by name'])::after {
  content: 'Skills';
}
.relative.flex.h-full.flex-shrink-0.overflow-hidden:not([style*='width: 52px']):has(input[aria-label='Filter prompts by name'])::after {
  content: 'Prompts';
}
.relative.flex.h-full.flex-shrink-0.overflow-hidden:not([style*='width: 52px']):has(div[aria-label='Memories'])::after {
  content: 'Memories';
}
.relative.flex.h-full.flex-shrink-0.overflow-hidden:not([style*='width: 52px']):has(div[aria-label='Bookmarks'])::after {
  content: 'Bookmarks';
}

/* ─── Liquid glass ─────────────────────────────────────────────────
   The composer and the header controls become lenses: frosted and
   saturated for every browser, plus an SVG displacement pass that
   bends the live page behind them where the engine allows it
   (Chrome/Edge only — ox-glass.js sets .ox-glass-refract after
   testing support, so the url() is never added where it would
   invalidate the whole declaration).

   Glass needs something to see THROUGH, so these surfaces drop from
   solid black to a translucent black; over the liquid-chrome ground
   that is what produces the effect, and over plain black they simply
   read as very dark glass. The inset hairline is the lit top edge
   every real glass panel has. */
.relative.flex.w-full.flex-grow.flex-col.overflow-hidden.bg-surface-chat {
  background-color: rgb(var(--ox-glass-tint) / 0.52);
  backdrop-filter: blur(22px) saturate(165%);
  -webkit-backdrop-filter: blur(22px) saturate(165%);
  box-shadow: inset 0 1px 0 rgb(var(--ox-glass-edge) / 0.14);
}
html.ox-glass-refract
  .relative.flex.w-full.flex-grow.flex-col.overflow-hidden.bg-surface-chat {
  backdrop-filter: blur(22px) saturate(165%) url(#ox-glass-bar);
}

/* Header controls and the composer's own icon buttons. Same lens,
   smaller rim, so a 36px chip does not look like a blurred smudge. */
[data-testid='model-selector-button'],
[data-testid='add-multi-convo-button'],
[aria-label='Presets'],
[aria-label='Temporary Chat'] {
  background-color: rgb(var(--ox-glass-tint) / 0.42) !important;
  backdrop-filter: blur(16px) saturate(160%);
  -webkit-backdrop-filter: blur(16px) saturate(160%);
  border-color: rgb(var(--ox-glass-edge) / 0.22);
  box-shadow: inset 0 1px 0 rgb(var(--ox-glass-edge) / 0.12);
}
html.ox-glass-refract [data-testid='model-selector-button'],
html.ox-glass-refract [data-testid='add-multi-convo-button'],
html.ox-glass-refract [aria-label='Presets'],
html.ox-glass-refract [aria-label='Temporary Chat'] {
  backdrop-filter: blur(16px) saturate(160%) url(#ox-glass-chip);
}

/* ─── The contained chat card, Venice-style ────────────────────────
   Venice floats the whole chat surface as a rounded card: inset from
   the window edges, a seam of darker ground between it and the rail,
   rounded corners, hairline edge. LibreChat's entire chat column is
   one stable wrapper (`bg-presentation`, Presentation.tsx) — margins
   pull it off the edges, the ground (#050403 body) shows through the
   seam, and its own overflow-hidden clips children to the rounding.
   h-full/w-full are compensated so nothing scrolls off-screen.      */
div.h-full.relative.flex.w-full.grow.overflow-hidden.bg-presentation {
  /* isolate: the liquid-chrome canvas sits at z:-1 INSIDE this card —
     isolation keeps that negative layer above the card's own black
     ground instead of escaping beneath it. */
  isolation: isolate;
  margin: 10px 10px 10px 6px;
  height: calc(100% - 20px);
  width: calc(100% - 16px);
  border-radius: 16px;
  border: 1px solid rgb(var(--border-light));
}

/* ─── Standalone, i.e. Add to Home Screen ──────────────────────────
   ⚠️ index.html sets `apple-mobile-web-app-status-bar-style` to
   `black-translucent`, which means the status bar OVERLAYS the page rather
   than reserving space for it. That is what makes the ember reach the top
   edge — and it also means the app's own header, an `absolute top-0` bar
   52px tall, renders directly UNDER the clock and the Dynamic Island.

   So the header takes the top inset and grows by it. The background is
   deliberately NOT inset: the canvas keeps painting the whole screen, which
   is the entire point of a translucent status bar.

   env() is 0 anywhere without an inset, and the whole block only applies in
   standalone, so a browser tab is untouched. */
html.ox-standalone div.relative.flex.h-full.w-full.flex-col > div.absolute.top-0.z-10 {
  /* ADDED to the header's own 8px, not replacing it — measured before
     writing this, because `padding-top: env(...)` alone silently dropped
     that 8px and would have sat the controls tight against the inset. */
  padding-top: calc(8px + env(safe-area-inset-top, 0px));
  height: calc(52px + env(safe-area-inset-top, 0px));
}

/* ─── The visible viewport on iOS ──────────────────────────────────
   ⚠️ `height: 100%` IS NOT THE VISIBLE VIEWPORT ON iOS SAFARI. It resolves
   against an initial containing block that does not track the collapsing
   toolbars, so the app is sized to one height while the screen shows
   another — a band of body background under the composer.

   ⚠️ AND THIS RULE ONLY WORKS FROM HERE. Written in index.html's inline
   <style> it did nothing: LibreChat's compiled stylesheet declares
   `body, html { height: 100% }` (style.css:1362 AND :1561) and is linked
   BELOW that block, so at equal specificity the later sheet wins. Measured
   in the cascade — the inline rule was present and losing:

     order 0  INLINE <style>          html, body { height: 100dvh }
     order 3  index.V_Z6_fWp.css      body, html { height: 100% }   <- winner

   ox-surfaces.css is the LAST stylesheet on the page, which is the only
   reason this takes effect. That ordering is load-bearing.

   ⚠️ A FIXED-VIEWPORT EMULATOR CANNOT SEE THIS. 100% and 100dvh compute to
   the same number there, so the override is invisible and every measurement
   reads as correct while the phone stays broken. Two rounds were lost to
   exactly that. */
html,
body {
  height: 100dvh;
}

/* ⚠️ THE CARD IS A DESKTOP IDEA. It had no width query, so a phone got the
   same 10px/6px inset, 16px radius and hairline border — which on a
   400px-wide screen is not a floating card, it is the app failing to fill
   the screen, and that is exactly how it was reported (2026-09-08).

   An inset that reads as deliberate framing against a large window reads as
   a bug against a small one: there is no surrounding room for the seam to be
   a seam, so it presents as a margin nobody asked for. Full-bleed below the
   md breakpoint, matching where LibreChat's own layout switches. */
@media (max-width: 767px) {
  div.h-full.relative.flex.w-full.grow.overflow-hidden.bg-presentation {
    margin: 0;
    height: 100%;
    width: 100%;
    border-radius: 0;
    border: 0;
  }

  /* viewport-fit=cover lets the app paint the whole screen, INCLUDING the
     strip behind the home indicator — which is the point, and which would
     otherwise leave the composer partly under it. The background stays
     full-bleed; only the composer is lifted clear.

     env() is 0 on every device without an indicator, so this is inert on
     desktop and on Android. The fallback keeps it 0 in engines that do not
     know env() at all. */
  form {
    margin-bottom: env(safe-area-inset-bottom, 0px);
  }
}

/* ─── Send button ──────────────────────────────────────────────────
   Stock LibreChat paints it with the ink color (`bg-text-primary`) —
   an inverted circle. The Break spends its one bold stroke here:
   ember, lit from above, same recipe as the mockup's release button.
   Selector: the app's own id="send-button" (SendButton.tsx).        */
/* The Smoke Orb IS the circle now (brand/ox-send-orb.js paints it),
   so the button hands over its own fill and keeps only the seat. The
   arrow is left exactly as the app draws it — same markup, same
   colour token — just lifted above the canvas. Nothing about the
   control changes.

   No `overflow: hidden` here, deliberately. Clipping the canvas to a
   rounded box is what made the orb's edge look jagged: the sphere is
   sized to land exactly on the button's diameter, so the clip ran
   straight along the silhouette — and Chromium masks a composited
   canvas with a hard cut-out, which stair-cases a circle no matter
   how clean the pixels beneath it are. The shader now draws its own
   antialiased rim and is fully transparent past it, so there is
   nothing to trim and only one edge in play. */
#send-button {
  background: transparent;
  box-shadow: 0 3px 12px -4px rgb(var(--ox-ember) / 0.45);
  position: relative;
  transition: box-shadow 0.22s ease;
}
/* Hover lights the rim: a hairline ember ring hugging the edge, then
   a soft bloom just outside it, over the existing lift. Outer
   box-shadows draw beyond the border box and are not clipped by this
   element's own overflow:hidden — which is what keeps the orb canvas
   trimmed to the circle — so the glow sits outside the metal rather
   than washing across it. */
#send-button:hover:not(:disabled) {
  box-shadow:
    0 0 0 1px rgb(var(--ox-ember) / 0.6),
    0 0 10px 1px rgb(var(--ox-ember) / 0.45),
    0 6px 20px -5px rgb(var(--ox-ember) / 0.6);
}
/* The Drop shader draws its sphere at radius 0.86 of the canvas
   half-extent, centred. Sized 1:1 the sphere would cover only 86% of
   the button and read hollow, so the canvas is scaled to 1/0.86
   (116.3%) — the sphere then measures exactly the button's diameter.
   No vertical nudge: unlike the Smoke Orb this one sits on centre.
   Nothing is clipped; the shader's own alpha ends the circle. */
#send-button > canvas.ox-send-orb {
  position: absolute;
  left: 50%;
  top: 50%;
  width: 116.3%;
  height: 116.3%;
  transform: translate(-50%, -50%);
  z-index: 0;
  pointer-events: none;
}
/* the arrow sits above the orb; it keeps its own inverted-ink colour */
#send-button > span {
  position: relative;
  z-index: 1;
}
/* Disabled keeps the stock treatment: the app dims it with opacity,
   which reads correctly over the ember fill as well. */
/* Disabled: the orb hides itself (its loop idles too) and the stock
   flat treatment returns, so a greyed-out control never animates. */
#send-button:disabled {
  background: rgb(var(--text-secondary));
  box-shadow: none;
}

/* ══════════════════════════════════════════════════════════════════
   ALIVE — the concept's accent pass (user-directed, 2026-09-01).
   The mockup's sidebar breathes ember: the active section's icon is
   lit, the open manuscript carries a glowing bullet, the New-draft
   plus is warm, the composer wakes on focus, selection is ember.
   All hooks are the app's own semantics (aria-pressed, aria-current,
   currentColor icons) — states the app already maintains; we only
   paint them. Colors ride the token triplets, so both themes work.
   ══════════════════════════════════════════════════════════════════ */

/* Active section in the rail: lit ember icon on a soft active card
   (concept: the "Manuscripts" row). Scoped to the rail's nav column
   so no other aria-pressed control in the app is touched. */
div.flex.h-full.flex-shrink-0.flex-col.gap-2.border-r.border-border-light.bg-surface-primary-alt
  > .flex.flex-col.gap-1.overflow-y-auto
  button[aria-pressed='true'] {
  background-color: rgb(var(--surface-active));
  color: rgb(var(--ox-ember-hi));
  border-radius: 10px;
}

/* New chat: the plus carries the accent, like the concept's
   "New draft" row. currentColor reaches the glyph. */
a[href='/c/new'] svg {
  color: rgb(var(--ox-ember-hi));
}

/* The open conversation: ember bullet with a soft glow, lit text
   (concept: the active manuscript line). The row is already a
   gap-2 flex, so the bullet slots in as its first item. */
.min-h-0.flex-1.overflow-hidden.bg-surface-primary-alt [aria-current='page'] {
  color: rgb(var(--text-primary));
}
.min-h-0.flex-1.overflow-hidden.bg-surface-primary-alt [aria-current='page']::before {
  background: rgb(var(--ox-ember));
  border-color: rgb(var(--ox-ember));
  box-shadow: 0 0 6px rgb(var(--ox-ember) / 0.5);
}

/* Resting edge: a chrome HAIRLINE. The app's own border is
   --border-light, a near-black that vanishes against the glass, so
   the box had no defined edge until it was focused. 0.5px renders as
   a true hairline on retina and reads thinner than the focused 1px,
   which is what separates the two states now that the app
   auto-focuses the textarea and the ring is otherwise always lit.

   Alpha does the rest of the work: at DPR 1 Chrome floors 0.5px back
   to a whole pixel, so thinness there has to come from a lighter
   line (0.20) rather than a narrower one. On retina you get both. */
.relative.flex.w-full.flex-grow.flex-col.overflow-hidden.bg-surface-chat {
  border-width: 0.5px; /* a true hairline on retina; floored to 1px at DPR 1 */
  border-color: rgb(var(--ox-glass-edge) / 0.3);
  transition: border-color 0.2s ease, border-width 0.2s ease,
    box-shadow 0.2s ease;
}
/* Lit only once something has been typed.
   Not :focus-within — LibreChat focuses the textarea on load, so it
   is true before the user has done anything. Not :hover either: the
   composer is a wide target that the pointer crosses on the way to
   anything else, so the edge kept flaring for no reason. Content is
   the one signal that means the box is genuinely in use. */
.relative.flex.w-full.flex-grow.flex-col.overflow-hidden.bg-surface-chat:has(
    textarea:not(:placeholder-shown)
  ) {
  border-width: 1px;
  border-color: rgb(var(--ox-glass-edge) / 0.55);
  box-shadow:
    0 0 0 1px rgb(var(--ox-glass-edge) / 0.22),
    0 10px 32px -14px rgb(var(--ox-glass-edge) / 0.3);
}
/* The hairline sweep above the composer — the concept's seam, now
   chrome to match the ring.
   Absolute inside the card (it is position:relative + overflow
   hidden), so nothing shifts. */
.relative.flex.w-full.flex-grow.flex-col.overflow-hidden.bg-surface-chat::before {
  content: '';
  position: absolute;
  top: 0;
  left: 10%;
  right: 10%;
  height: 1px;
  background: linear-gradient(
    90deg,
    transparent,
    rgb(var(--ox-glass-edge) / 0.35) 50%,
    transparent
  );
  pointer-events: none;
  z-index: 1;
}

/* Text selection is ember, everywhere (concept ::selection). */
::selection {
  background: rgb(var(--ox-ember) / 0.3);
}

/* Warm scrollbars instead of stock grey — thin, quiet, rounded. */
::-webkit-scrollbar {
  width: 8px;
  height: 8px;
}
::-webkit-scrollbar-track {
  background: transparent;
}
::-webkit-scrollbar-thumb {
  background: rgb(var(--gray-650));
  border-radius: 8px;
}
::-webkit-scrollbar-thumb:hover {
  background: rgb(var(--gray-600));
}

/* ─── The chats list, concept treatment ────────────────────────────
   The mockup's manuscript list: quiet small-caps group labels, rows
   marked by 5px bullets — hollow at rest, ember-filled and glowing
   on the open one — and ink that lifts on hover. Mapped onto
   LibreChat's virtualized conversation list. Row heights stay stock:
   ReactVirtualized positions rows in JS, so CSS-shrinking them would
   only tear gaps in the layout.                                     */

/* Date group labels ("Yesterday", "Previous 7 days") — the concept's
   section-label voice. */
:is(.ReactVirtualized__List, .bg-surface-primary-alt ul.list-none) h2 {
  font-size: 10.5px;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.12em;
  color: rgb(var(--text-tertiary));
}

/* Conversation rows: concept .it — 13px, ink-3 at rest, ink-1 on
   hover, soft rounded hover card. */
:is(.ReactVirtualized__List, .bg-surface-primary-alt ul.list-none) button[aria-label$='conversation'] {
  font-size: 13px;
  color: rgb(var(--text-secondary-alt));
  border-radius: 7px;
}
:is(.ReactVirtualized__List, .bg-surface-primary-alt ul.list-none) button[aria-label$='conversation']:hover {
  color: rgb(var(--text-primary));
}

/* Every chat here runs the same OX endpoint, so the per-row model
   icon repeats one glyph down the whole list — the concept replaces
   it with the bullet (user-directed concept match). The row itself
   stays a full-width click target. */
:is(.ReactVirtualized__List, .bg-surface-primary-alt ul.list-none) button[aria-label$='conversation'] > div:first-child {
  display: none;
}
:is(.ReactVirtualized__List, .bg-surface-primary-alt ul.list-none) button[aria-label$='conversation']::before {
  content: '';
  flex: none;
  width: 5px;
  height: 5px;
  margin-left: 7px;
  border-radius: 50%;
  border: 1px solid rgb(var(--text-tertiary) / 0.65);
  transition: border-color 0.18s, background 0.18s;
}
:is(.ReactVirtualized__List, .bg-surface-primary-alt ul.list-none) button[aria-label$='conversation']:hover::before {
  border-color: rgb(var(--text-secondary));
}

/* Row cards: the concept's slim quiet rows. The virtualized slot is
   a fixed 48px (JS-measured), so the visible card slims to 40px and
   centers in its slot — the remaining 8px reads as the concept's
   breathing room between rows. */
:is(.ReactVirtualized__List, .bg-surface-primary-alt ul.list-none) .group.relative.flex.h-12.w-full.items-center {
  height: 40px;
  margin-top: 4px;
  border-radius: 7px;
}

/* No bar on the open conversation (user-directed): the dot IS the
   state. Buttons and cards stay transparent in every state except a
   whisper of hover; the ember bullet marks the open chat, and it
   turns amber and pulses while the AI is generating. */
:is(.ReactVirtualized__List, .bg-surface-primary-alt ul.list-none) button[aria-label$='conversation'] {
  background-color: transparent !important;
}
:is(.ReactVirtualized__List, .bg-surface-primary-alt ul.list-none)
  .group.relative.flex.h-12.w-full.items-center:hover {
  background: rgb(var(--surface-secondary) / 0.55);
}
:is(.ReactVirtualized__List, .bg-surface-primary-alt ul.list-none)
  .group.relative.flex.h-12.w-full.items-center:has(> button[aria-current='page']) {
  background: transparent;
}

/* Thinking/writing: the open chat's dot goes warm amber and
   breathes. Generation is visible in the DOM as the stop control
   (stream) or the during-run button (agent runs) — only the current
   conversation can be generating, so keying the current row off a
   root-level :has() is exact. */
html:has([data-testid='stop-generation-button'], #during-run-send-button)
  :is(.ReactVirtualized__List, .bg-surface-primary-alt ul.list-none)
  button[aria-current='page']::before {
  background: rgb(224 176 75);
  border-color: rgb(224 176 75);
  box-shadow: 0 0 7px rgb(224 176 75 / 0.55);
  animation: ox-think 1.15s ease-in-out infinite;
}
@keyframes ox-think {
  0%,
  100% {
    opacity: 1;
  }
  50% {
    opacity: 0.4;
  }
}
@media (prefers-reduced-motion: reduce) {
  html:has([data-testid='stop-generation-button'], #during-run-send-button)
    :is(.ReactVirtualized__List, .bg-surface-primary-alt ul.list-none)
    button[aria-current='page']::before {
    animation: none;
  }
}

/* Titles at the concept's 13px even where a nested utility says
   otherwise. */
:is(.ReactVirtualized__List, .bg-surface-primary-alt ul.list-none) button[aria-label$='conversation'],
:is(.ReactVirtualized__List, .bg-surface-primary-alt ul.list-none) button[aria-label$='conversation'] * {
  font-size: 13px;
}

/* The pinned action icons on the open row (archive/delete) sit
   quiet until the row is hovered — present, just not shouting. */
:is(.ReactVirtualized__List, .bg-surface-primary-alt ul.list-none)
  .group.relative.flex.h-12.w-full.items-center:has(> button[aria-current='page'])
  > div:last-child {
  opacity: 0.5;
  transition: opacity 0.15s ease;
}
:is(.ReactVirtualized__List, .bg-surface-primary-alt ul.list-none)
  .group.relative.flex.h-12.w-full.items-center:has(> button[aria-current='page']):hover
  > div:last-child {
  opacity: 1;
}

/* The title's right-edge fade: the app paints an 80px gradient
   overlay whose end color is the STOCK active surface — over our
   darker card it reads as a leftover light block. The concept fades
   with a mask instead (background-independent, works over rest,
   hover, and open alike), so: mask the title, blank the overlay.   */
:is(.ReactVirtualized__List, .bg-surface-primary-alt ul.list-none)
  button[aria-label$='conversation']
  .pointer-events-none.absolute.bottom-0.right-0.top-0.w-20 {
  background-image: none;
}
:is(.ReactVirtualized__List, .bg-surface-primary-alt ul.list-none)
  button[aria-label$='conversation']
  > div.relative.flex-1.grow.overflow-hidden.whitespace-nowrap {
  -webkit-mask-image: linear-gradient(to left, transparent, #000 28px);
  mask-image: linear-gradient(to left, transparent, #000 28px);
}

/* THE white bar: the row container's own ::before — a near-white
   highlight pill LibreChat paints on the active row. Killed; the
   ember dot is the state, and our card hover supplies feedback.    */
:is(.ReactVirtualized__List, .bg-surface-primary-alt ul.list-none)
  .group.relative.flex.h-12.w-full.items-center::before {
  content: none !important;
}

/* ─── Projects tree ────────────────────────────────────────────────
   Same row component, different container — the scope group above
   covers it. Plus the concept touches of its own: the folder glyph
   runs small and ember (user-directed). The folder is the second
   svg in the project button (first is the chevron). */
.bg-surface-primary-alt ul.list-none li button > svg:nth-of-type(2) {
  width: 12px;
  height: 12px;
  color: rgb(var(--ox-ember));
}

/* ─── The ember seam ───────────────────────────────────────────────
   User-directed: when the menu is open, a thin fading ember line
   marks the boundary between the rail and the extended panel —
   vertical twin of the composer's hairline sweep. Lives on the
   PANEL's left edge (the panel is already position:relative) —
   NOT on the rail: making the rail positioned would capture the
   band button's containing block and shrink its width:100% to the
   rail. Fading out at both ends. */
.min-h-0.flex-1.overflow-hidden.bg-surface-primary-alt::before {
  content: '';
  position: absolute;
  top: 72px;
  bottom: 16px;
  left: 0;
  width: 1px;
  background: linear-gradient(
    180deg,
    transparent,
    rgb(var(--ox-ember) / 0.25) 30%,
    rgb(var(--ox-ember) / 0.25) 65%,
    transparent
  );
  pointer-events: none;
}

/* While the liquid chrome is live, the ONE full-size layer sitting
   over the canvas goes transparent — an in-flow background paints
   above a negative-z child, which is why the shader rendered
   invisibly beneath solid black. The card keeps its own black as the
   backstop.

   Scoped to that single direct child, not every descendant
   `.bg-presentation`: the broad version also stripped the background
   from the header's model chip, Presets and Temporary-Chat buttons,
   leaving them transparent with the chrome pouring through. */
div.h-full.relative.flex.w-full.grow.overflow-hidden.bg-presentation:has(
    > #ox-chrome-bg[style*='block']
  )
  > .h-full.w-full.relative.flex-1.bg-presentation {
  background-color: transparent;
}

/* ─── The assistant's mark ─────────────────────────────────────────
   LibreChat draws a generic lucide "bot" glyph wherever the
   assistant/endpoint is represented — message headers, the model
   chip. The outline ox takes its place: the svg is hidden rather
   than removed (the app keeps its aria-hidden icon slot and every
   size class intact) and the mark is painted on the slot itself.
   The artwork is wide (3:2), so `contain` seats it in the square
   slot without distortion or cropping.                              */
/* ─── The assistant's header line ──────────────────────────────────
   User-directed: the mark and the sender name come out of the
   transcript entirely. Every message here is from the same
   assistant, so the repeated "OX Test" line is noise — the reply
   speaks for itself, the way the concept's transcript reads.
   Verified before hiding: the h2 holds zero interactive elements
   (no buttons, links or focusable nodes), so this removes a label,
   never a control. The model chip and menu keep their stock glyph;
   the rail keeps its mark. */
h2:has(svg.lucide-bot) {
  display: none;
}

/* ─── Mic ──────────────────────────────────────────────────────────
   The one ember thing in the composer, now that the box edge and the
   send orb are both chrome — so voice input reads as a deliberate
   accent rather than another grey control.
   The glyph is drawn with `stroke`, not `fill`, so stroke is what
   has to change; `#audio-recorder` out-specifies the app's
   `.stroke-text-secondary` utility without needing !important. */
#audio-recorder svg {
  stroke: rgb(var(--ox-ember));
  transition: stroke 0.18s ease;
}
#audio-recorder:hover svg,
#audio-recorder:focus-visible svg {
  stroke: rgb(var(--ox-ember-hi));
}

/* ─── Mic, recording ───────────────────────────────────────────────
   The app swaps in lucide MicOff while listening — a mic with a
   SLASH through it, in error red. That is the universal symbol for
   "muted", so the control reads as off at the exact moment it is on.
   Replaced with the ordinary mic whose capsule is filled solid:
   same silhouette, no slash, and "filled = live" is unambiguous.
   The state hook is the app's own aria-pressed (AudioRecorder.tsx),
   so nothing about the control changes — the svg is hidden, not
   removed, and the button keeps its label, focus and click. */
#audio-recorder[aria-pressed='true'] svg {
  visibility: hidden;
}
#audio-recorder[aria-pressed='true'] {
  background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23E4703F' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z' fill='%23E4703F'/><path d='M19 10v2a7 7 0 0 1-14 0v-2'/><line x1='12' x2='12' y1='19' y2='22'/></svg>");
  background-repeat: no-repeat;
  background-position: center;
  background-size: 24px 24px;
}

/* ─── Greeting icon ────────────────────────────────────────────────
   The landing greeting carried a generic robot head in a 40px ring
   (a .shadow-stroke bubble inside a size-10 wrapper). Removed: the
   headline reads better alone, and the rail already carries the ox.
   The whole size-10 wrapper goes, not just the bubble — hiding the
   inner one alone would leave a 40px hole in the flex row and the
   headline would sit off-centre. Verified first: zero interactive
   elements inside, so this drops decoration, never a control. */
.relative.size-10:has(.shadow-stroke svg.lucide-bot) {
  display: none;
}

/* ─── Account avatar: no flash of the stock picture ────────────────
   The account button renders a DiceBear "initials" SVG — a solid
   coloured square with the letters on it — and ox-avatar-orb.js
   replaces it with the orb. It used to hide that image in JS, but the
   scan runs on a timer, so React painted the square first and the orb
   arrived after: a flash of the old avatar on every refresh.

   Hiding it here instead means it is never painted at all, because
   this stylesheet is render-blocking and the class is set
   synchronously by the script before React mounts.

   `visibility`, not `display`, so the image keeps its layout box —
   the orb is positioned against it — and its alt text stays available
   to a screen reader.

   The class gate matters: the image is the fallback. Without WebGPU
   nothing is coming to replace it, so the class is never set and the
   image stays visible. If WebGPU exists but fails anyway, the script
   sets `visibility: visible` inline, which outranks this rule. */
html.ox-orb-capable img.rounded-full[alt$="avatar"] {
  visibility: hidden;
}

/* ─── Greeting type ────────────────────────────────────────────────
   The landing headline takes the concept's display face. In
   design/console.html the h1 is Newsreader at weight 300 with -.018em
   tracking and a 1.02 line-height, and that combination is the whole
   effect: a light serif at display size reads as considered, where
   the app's default (a medium-weight UI sans) reads as a system
   message. The negative left margin is the concept's own optical
   correction — a serif capital starts with visible side bearing, so
   without it the centred line sits a hair right of true.

   `font-medium` on the element is weight 500; this rule is a single
   class of equal specificity and ox-surfaces.css is linked after the
   app's stylesheet, so source order carries the 300. SplitText is
   used nowhere but Landing.tsx, so .split-parent cannot catch
   anything else.

   Newsreader is VARIABLE on an optical-size axis (opsz 6..72), and
   that axis is the whole reason a first attempt at this looked like
   the wrong typeface. With `font-optical-sizing: auto` the browser
   picks opsz from the rendered px, so at the app's 24px it served the
   text cut — sturdy, low contrast, tight apertures — while the
   concept's 60px h1 served the display cut, with fine hairlines and
   high stroke contrast. Same font file, two completely different
   faces.

   So opsz is PINNED rather than left on auto, at 60 — the value the
   concept's own `auto` resolves to at the top of its clamp, which is
   the rendering being matched. Pinning also holds that cut at narrow
   widths, where the concept would itself slide back down toward the
   text cut. font-optical-sizing is set to none alongside it so no
   engine quietly re-derives the axis.

   Everything else is the concept's h1 verbatim, clamp included. */
p.split-parent {
  font-family: 'Newsreader', Georgia, 'Times New Roman', serif;
  font-optical-sizing: none;
  font-variation-settings: 'opsz' 60;
  font-weight: 300;
  font-size: clamp(38px, 4.6vw, 60px);
  letter-spacing: -0.018em;
  line-height: 1.02;
  margin-left: -0.028em;
  text-wrap: balance;
  /* SplitText ships overflow:hidden here, to mask its letters while
     they animate up from below. At line-height 1.02 that clip lands
     right on the italic f's ascender and slices it flat — the cut
     letter in "off".

     First attempt bought headroom with padding (overflow clips at the
     padding box) and handed it back with a negative margin. It worked,
     but it only ever bought a fixed 0.16em: any size, weight or glyph
     that reached further would clip again, and it left negative
     margins on a flex item for a future change to trip over.

     Dropping the clip is the honest fix — the element has no reason to
     clip in the first place. The mask it provided is redundant:
     SplitText animates opacity from 0 alongside the transform, so a
     letter is invisible for the part of its travel the mask covered.
     Verified by A/B — overflow hidden vs visible on the real element
     render identically once nothing is being cut. */
  overflow: visible;
}
/* The concept sets the last word, and only that word, in italic:
   `The yoke is <i>off</i>.` — the full stop stays upright. SplitText
   leaves no markup to hook, but it does give the structure to do it
   properly: each WORD becomes a nowrap span, and each glyph inside it
   its own span. So the last word span is "off." and its final child
   is the full stop, which :not(:last-child) leaves alone.

   This holds whatever name precedes it, because the sentence always
   ends on that word — the name changes the first span, never the
   last. */
p.split-parent > span:last-child > span:not(:last-child) {
  font-style: italic;
}
/* The per-character spans inherit the family but SplitText sets them
   `inline-block`, which makes each glyph its own box — so a serif's
   side bearings stop overlapping and the word looks loose. Restoring
   the natural advance width is what keeps it reading as set type. */
p.split-parent span {
  letter-spacing: inherit;
}

/* ─── Temporary chat ───────────────────────────────────────────────
   The dashed bubble marks a conversation that will not be kept — a
   privacy affordance, so it earns the accent alongside the mic
   rather than sitting grey with the ordinary controls.
   Drawn with stroke="currentColor", so `color` is the lever, and it
   rides --ox-ember like everything else: change the accent in
   Settings and this follows. */
[aria-label='Temporary Chat'] svg.lucide-message-circle-dashed {
  color: rgb(var(--ox-ember));
  transition: color 0.18s ease;
}
[aria-label='Temporary Chat']:hover svg.lucide-message-circle-dashed,
[aria-label='Temporary Chat']:focus-visible svg.lucide-message-circle-dashed {
  color: rgb(var(--ox-ember-hi));
}

/* ─── Header scrim ─────────────────────────────────────────────────
   The header band ships a black gradient (rgba(0,0,0,.8) fading to
   transparent) to lift its controls off scrolling content. Over the
   liquid chrome it reads as a smear rather than a scrim, and in
   light mode it is a dark stripe across a pale page. Dropped — the
   controls now carry their own glass, so they no longer need it. */
.absolute.top-0.z-10.flex.w-full.items-center.gap-2 {
  background-image: none !important;
}

/* ─── The mark on paper ────────────────────────────────────────────
   The rail mark is chrome linework — bright strokes meant for a dark
   ground, and effectively invisible on the light theme's paper.
   Inverting turns the same artwork into dark linework, so one asset
   serves both themes rather than needing a second file. */
html:not(.dark)
  .relative.flex.h-full.flex-shrink-0.overflow-hidden[style*='width: 52px']
  div.flex.h-full.flex-shrink-0.flex-col.gap-2.border-r.border-border-light.bg-surface-primary-alt
  > button:first-of-type {
  filter: invert(1);
}

/* ─── Mic on paper ─────────────────────────────────────────────────
   Light theme only: the mic drops the accent and goes to ink.
   Ember is tuned to glow on a near-black ground; on warm paper it
   turns muddy, and the composer already carries the accent in its
   recording fill and the temporary-chat mark. Black reads cleanly
   and keeps the light theme quiet. */
html:not(.dark) #audio-recorder svg {
  stroke: rgb(var(--text-primary));
}
html:not(.dark) #audio-recorder:hover svg,
html:not(.dark) #audio-recorder:focus-visible svg {
  stroke: rgb(var(--text-primary));
  opacity: 0.7;
}
/* Same for the temporary-chat bubble — it is the composer's other
   accent glyph, and the pair has to match or the row looks
   half-finished. Colour, not stroke: this one is drawn with
   currentColor. */
html:not(.dark) [aria-label='Temporary Chat'] svg.lucide-message-circle-dashed {
  color: rgb(var(--text-primary));
}
html:not(.dark) [aria-label='Temporary Chat']:hover svg.lucide-message-circle-dashed,
html:not(.dark)
  [aria-label='Temporary Chat']:focus-visible
  svg.lucide-message-circle-dashed {
  color: rgb(var(--text-primary));
  opacity: 0.7;
}

/* ─── Wordmark on paper ────────────────────────────────────────────
   logo.svg is filled pure white for the dark rail, so on the light
   theme's panel it disappears. Inverting flips it to black; alpha is
   untouched by `invert`, so the transparent ground stays transparent
   and one asset serves both themes — same trick as the rail mark. */
html:not(.dark)
  .relative.flex.h-full.flex-shrink-0.overflow-hidden:not([style*='width: 52px'])::before {
  filter: invert(1);
}

/* ─── Footer ───────────────────────────────────────────────────────
   The tagline is a signature, not a message — it should be legible
   when looked for and invisible when not. LibreChat draws it in
   text-text-primary, the same ink as body copy, which on an empty
   landing makes the loudest thing on screen a line nobody needs to
   read.

   Alpha on the token rather than a different token or an `opacity`
   rule: the colour stays keyed to the theme, so it dims identically
   in light mode, and alpha leaves the element's own compositing
   alone — `opacity` would make it a separate layer, which on a
   backdrop-filtered ground can shift how the blur behind it reads.

   The separator inherits the same treatment, for when a second
   segment or a policy link puts one back. */
div.absolute.bottom-0.left-0.right-0.text-xs.text-text-primary {
  color: rgb(var(--text-primary) / 0.42);
}
div.absolute.bottom-0.left-0.right-0.text-xs.text-text-primary .border-border-medium {
  border-color: rgb(var(--text-primary) / 0.16);
}

/* ─── Tables in chat ───────────────────────────────────────────────
   The header row shipped as rgb(87 81 74) — a mid brown-grey that
   appears nowhere else in the interface, so a table read as a foreign
   object dropped into the answer. It takes the rail's own surface
   now: the darkest raised tone the app has, which already means
   "chrome, not content" wherever else it appears.

   SPECIFICITY IS THE WHOLE PROBLEM HERE. The app styles these with
   `.dark .markdown th` — two classes and an element, (0,2,1). A rule
   written as `.markdown-table-wrapper table thead th` looks far more
   specific and is not: one class, three elements, (0,1,3). Class
   count is compared first, so the app's rule wins and the header
   stays brown. Matching their selector shape and prefixing `html`
   gives (0,2,2), which is the smallest thing that actually beats it.

   Worth recording how this got missed: it was first verified against
   a table built by hand in the console, which had no `.markdown`
   ancestor — so the app's rule never applied to the probe, the probe
   passed, and the real table was untouched. A test that avoids the
   conflict cannot detect the conflict.

   Light mode cannot use the rail token. The rail there is pure white
   by design, matching the page ground, so a header painted with it
   would vanish — surface-secondary is the warm paper tone that reads
   as a band on white without adding a colour the palette lacks. */
html.dark .markdown th {
  background-color: rgb(var(--surface-primary-alt));
  border-color: rgb(var(--border-medium));
}
html:not(.dark) .markdown th {
  background-color: rgb(var(--surface-secondary));
  border-color: rgb(var(--border-medium));
}
/* The borders needed the same treatment and then some. The app sets
   them with `.dark .markdown td, .dark .markdown td:last-child` — and
   that pseudo-class counts as a class, making the second half (0,3,1).
   Beating it takes three classes of our own, which `html:not(.dark)`
   supplies because :not() contributes its argument's specificity.
   Getting this wrong the first time left the header fixed and every
   rule around it still drawn in the old brown. */
html.dark .markdown table,
html.dark .markdown td,
html.dark .markdown td:last-child,
html.dark .markdown th:last-child {
  border-color: rgb(var(--border-medium));
}
html:not(.dark) .markdown table,
html:not(.dark) .markdown td,
html:not(.dark) .markdown td:last-child,
html:not(.dark) .markdown th:last-child {
  border-color: rgb(var(--border-medium));
}

/* ─── The sign-in mark ─────────────────────────────────────────────
   The ox head on login / register / forgot-password. The artwork swap
   itself needs no rule here — docker-compose mounts it over
   LibreChat's own `assets/logo.svg`, which the auth layout hardcodes.
   This is only its size and position.

   The layout is `<div class="mt-6 h-10 w-full"><img class="h-full
   w-full object-contain"></div>` — 24px down, 40px tall, and at the
   ox's 260x187 that renders about 56px wide. Too quiet for the one
   piece of brand on the page.

   Size the WRAPPER, not the image. The img is already `h-full`, so it
   follows automatically, and the taller box reserves real space in the
   flow. Overriding the img's height instead would overflow a box that
   is still 40px tall and let the mark ride down into the heading —
   correct-looking at one viewport and broken at the next.

   `:has()` is the only way to reach a parent from its child, and it
   degrades correctly: where it is unsupported the rule is dropped and
   the mark simply stays at its stock size. */
div:has(> img[src='assets/logo.svg']) {
  height: 184px;
  margin-top: 84px;
}

/* ═══ THE SIGN-IN SCREEN ═══════════════════════════════════════════
   Scoped throughout by `.w-authPageWidth`, a class only the auth card
   carries, so none of this can reach the chat app. It restyles
   login, register and forgot-password together.

   WHAT WAS WRONG. Measured on the stock screen: the page ground and
   the card were BOTH rgb(0,0,0) — the same token. So there was no
   card. There were controls floating on black, each outlined with a
   single 1px #1F1C18 hairline, and one flat ember fill. No material,
   no hierarchy. That is the whole of why it read as cheap.

   THE RULE THIS OBEYS — and the one the first attempt broke.
   ui/console.html contains no visible `border` on any surface. Not
   one. Depth is always the same two layers stacked:

     · a dark LIP underneath        box-shadow: 0 1px 0 rgba(0,0,0,.55)
     · a lit 1px TOP EDGE above     the masked gradient in `.lit`

   Borders are what make a surface read as drawn rather than lit, and
   the concept's whole thesis is "one light source, always above".
   Every value below is transplanted from that file rather than
   invented; where the app's tokens already hold the same value
   (--ox-ember is exactly the concept's #BD4D28) the token is used, so
   the light theme and the accent picker keep working. */

.w-authPageWidth {
  /* The concept's single easing curve — it uses exactly one. */
  --ox-e-out: cubic-bezier(0.16, 1, 0.3, 1);
  --ox-lip: 0 1px 0 rgb(0 0 0 / 0.55);
}

/* ─── The surround ─────────────────────────────────────────────────
   The card is lit from above, so the ground has to sit BELOW it in
   value or there is nothing for the lip to fall onto. Ember arrives
   as a bloom from over the top edge — the same direction as every
   highlight on the page. A gradient rather than a shader: this is the
   one screen a user meets before any WebGPU has loaded. */
#root > div:has(.w-authPageWidth) {
  background-color: rgb(var(--surface-primary-contrast));
  background-image:
    radial-gradient(116% 66% at 50% -14%, rgb(var(--ox-ember) / 0.14), transparent 62%),
    radial-gradient(88% 48% at 50% 110%, rgb(var(--ox-ember-deep) / 0.10), transparent 72%);
  background-attachment: fixed;
}
html:not(.dark) #root > div:has(.w-authPageWidth) {
  background-color: rgb(var(--surface-secondary));
  background-image:
    radial-gradient(116% 66% at 50% -14%, rgb(var(--ox-ember) / 0.10), transparent 62%);
}

/* ─── The card ─────────────────────────────────────────────────────
   `.ms` from the concept, at sign-in scale: no border, the universal
   lip, and a long soft drop with heavy negative spread so the shadow
   reads as depth rather than as a halo. `sm:rounded-lg` means the
   stock card is a full-bleed rectangle below 640px — on a phone there
   is no card at all — so the shape is set unconditionally here. */
.w-authPageWidth {
  position: relative;
  width: calc(100% - 2rem);
  max-width: 26rem;
  margin-inline: auto;
  padding: 2.25rem 1.75rem 1.5rem;
  background-color: rgb(var(--surface-primary-alt));
  border: 0;
  border-radius: 1.125rem;
  box-shadow: var(--ox-lip), 0 1.125rem 2.5rem -1.75rem rgb(0 0 0 / 0.9);
}
html:not(.dark) .w-authPageWidth {
  background-color: rgb(var(--surface-primary));
  box-shadow: 0 1px 0 rgb(0 0 0 / 0.06), 0 1.125rem 2.5rem -1.5rem rgb(60 50 38 / 0.3);
}

/* ─── "One light source, always above" ─────────────────────────────
   The concept's `.lit`, verbatim. A 1px padding-box masked out of a
   content-box mask leaves a 1px ring; the ring is filled with a
   vertical gradient that is bright at the very top, nearly gone by
   42%, and absent by 70% — so only the top edge and the upper
   shoulders catch the light. `border-radius: inherit` makes it work
   on any shape. This is the recipe that replaces borders. */
.w-authPageWidth::before,
.w-authPageWidth form div:has(> input#email)::before,
.w-authPageWidth form div:has(> input#password)::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  padding: 1px;
  background: linear-gradient(
    180deg,
    rgb(255 252 246 / 0.14),
    rgb(255 252 246 / 0.02) 42%,
    transparent 70%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  z-index: 1;
}
@media (min-resolution: 2dppx) {
  .w-authPageWidth::before,
  .w-authPageWidth form div:has(> input#email)::before,
  .w-authPageWidth form div:has(> input#password)::before { padding: 0.5px; }
}
/* On paper the light source is still above, but a near-white rim is
   invisible — the top edge reads by going darker instead. */
html:not(.dark) .w-authPageWidth::before,
html:not(.dark) .w-authPageWidth form div:has(> input#email)::before,
html:not(.dark) .w-authPageWidth form div:has(> input#password)::before {
  background: linear-gradient(180deg, rgb(60 50 38 / 0.16), rgb(60 50 38 / 0.03) 42%, transparent 70%);
}

/* ─── The title IS the wordmark ────────────────────────────────────
   "Welcome back" is replaced by the mark. The text stays in the DOM
   and in the accessibility tree — only `font-size: 0` and a
   transparent colour hide it — so the h1 still announces the page and
   still translates.

   The artwork is `fill="white"`, not currentColor, so it cannot be
   recoloured by inheritance and would be invisible on paper.
   invert() is the only lever the asset leaves, and it is the same
   treatment surfaces.css already uses for this exact file on the
   expanded rail. */
/* Every auth heading takes the display face — that alone lifts it off
   stock Inter semibold. */
.w-authPageWidth > h1 {
  margin: 0 0 1.5rem;
  font-family: 'Newsreader', Georgia, serif;
  font-weight: 300;
  font-size: 1.75rem;
  letter-spacing: 0.005em;
  color: rgb(var(--text-primary));
}

/* Only the SIGN-IN heading becomes the mark. Register's h1 reads
   "Create your account" and forgot-password's names itself too — swap
   those for the wordmark and every auth screen becomes identical
   apart from how many fields it has, which is the one cue a person
   uses to know where they are. The login page is identified by the
   footer link it carries, which is the only stable difference in the
   DOM: login offers /register, the others offer /login. */
.w-authPageWidth:has(> p a[href='/register']) > h1 {
  height: 1.625rem;
  margin: 0 0 1.75rem;
  font-size: 0;
  color: transparent;
  user-select: none;
  background: url('./ox-wordmark.svg?v=2') no-repeat center center / auto 1.625rem;
}
html:not(.dark) .w-authPageWidth:has(> p a[href='/register']) > h1 {
  filter: invert(1);
}

/* ─── Fields ───────────────────────────────────────────────────────
   The concept's composer pattern: a wrapper carries every visual
   state and the input itself is completely chromeless. That is what
   lets focus be one ring on one element instead of a fight with the
   input's own outline, and it is why the field can lift its
   background without the text moving. */
.w-authPageWidth form div:has(> input#email),
.w-authPageWidth form div:has(> input#password) {
  position: relative;
  border-radius: 0.8125rem;
  background-color: rgb(var(--surface-primary));
  box-shadow: var(--ox-lip);
  transition:
    background-color 0.22s var(--ox-e-out),
    box-shadow 0.28s var(--ox-e-out);
}
html:not(.dark) .w-authPageWidth form div:has(> input#email),
html:not(.dark) .w-authPageWidth form div:has(> input#password) {
  background-color: rgb(var(--surface-secondary));
  box-shadow: 0 1px 0 rgb(0 0 0 / 0.05);
}

/* Chromeless input. The display face, at the concept's composer size
   — the one place in the product a person types, so it gets the
   serif rather than the UI font. The app loads Newsreader at 300
   only, so 300 it is. */
/* No z-index here, deliberately. Raising the input above the lit ring
   also raised it above the password field's show/hide button, which
   sits later in the DOM — the eye kept painting but the input won hit
   testing, so the control was dead. The ring is pointer-events:none
   and is a 1px edge, so it can paint on top harmlessly. */
.w-authPageWidth form input#email,
.w-authPageWidth form input#password {
  height: 3.375rem;
  background-color: transparent;
  border: 0;
  outline: none;
  border-radius: 0.8125rem;
  font-family: 'Newsreader', Georgia, serif;
  font-weight: 300;
  font-size: 1.0625rem;
  color: rgb(var(--text-primary));
}

/* Focus is the concept's, exactly: the surface LIFTS a step and a 1px
   ember ring appears at 28% — no glow, no growing spread. The lip is
   retained underneath so the field does not lose its footing. */
.w-authPageWidth form div:has(> input#email):focus-within,
.w-authPageWidth form div:has(> input#password):focus-within {
  background-color: rgb(var(--surface-secondary));
  box-shadow: var(--ox-lip), 0 0 0 1px rgb(var(--ox-ember) / 0.28);
}
html:not(.dark) .w-authPageWidth form div:has(> input#email):focus-within,
html:not(.dark) .w-authPageWidth form div:has(> input#password):focus-within {
  background-color: rgb(var(--surface-tertiary));
  box-shadow: 0 1px 0 rgb(0 0 0 / 0.05), 0 0 0 1px rgb(var(--ox-ember) / 0.35);
}
/* The app's own 2px white ring is the one moment this page shouted,
   and in the wrong colour. The ring above replaces it. */
.w-authPageWidth form input:focus,
.w-authPageWidth form input:focus-visible {
  outline: none;
  box-shadow: none;
}

/* The floating label. Its `bg-surface-primary` exists only to punch a
   hole through the field's top border — and there is no border any
   more, so the chip is not just unnecessary, it would stamp a block
   of the wrong colour across a lit edge. Italic display face, because
   the concept's placeholder is italic and says so twice. */
.w-authPageWidth form label {
  background-color: transparent;
  font-family: 'Newsreader', Georgia, serif;
  font-style: italic;
  font-weight: 300;
  color: rgb(var(--text-tertiary));
  transition: color 0.18s var(--ox-e-out);
}
.w-authPageWidth form div:has(> input#email):focus-within label,
.w-authPageWidth form div:has(> input#password):focus-within label {
  color: rgb(var(--ox-ember-hi));
}
html:not(.dark) .w-authPageWidth form div:has(> input#email):focus-within label,
html:not(.dark) .w-authPageWidth form div:has(> input#password):focus-within label {
  color: rgb(var(--ox-ember-ink));
}

/* ─── The action ───────────────────────────────────────────────────
   A black pill with one thin chrome outline travelling around it.

   SHAPE. A pill, not a rounded rectangle. Two reasons and both are
   about the outline: a continuous curve has no corners for the glint
   to stall in, so the motion stays even the whole way round; and the
   fields are 13px rounded rects, so a pill stops the action reading
   as a third input. It also puts the button in the same family as the
   composer's send control, which is a circle.

   NO SHADOW. Deliberately none — not a lip, not a drop. A black mass
   on a near-black card cannot be separated by a shadow, only muddied
   by one; the chrome hairline is what draws the object, and it draws
   it exactly. This is the one surface on the page that is defined by
   its edge instead of its ground.

   TYPE. Uppercase at 0.18em tracking. The label has to hold a
   full-width bar on its own now that the fill carries no colour, and
   letterspaced caps are what give a small type size presence against
   a long horizontal edge. It pairs with the italic serif on the
   fields the way a machined plate pairs with a printed page — which
   is the whole ground/air idea the concept is named for.

   The outline is the SAME masked-ring construction as `.lit` above —
   a 1px padding-box XORed out of a content-box mask — so the button
   is built from the recipe the rest of the page already uses. Only
   the fill inside the ring changes: a conic sweep, rotated by an
   animated angle, so a bright glint orbits the perimeter while the
   rest of the ring rests at a dim chrome value.

   WHY @property. A plain custom property is a string to the cascade
   and cannot be interpolated — animating it would snap 0->360 with no
   motion in between. Registering it with `<angle>` syntax is what
   makes it a real animatable value.

   AND WHY --ox-spin IS ALSO DECLARED NORMALLY. Where @property is
   unsupported the registration is skipped, and an unregistered
   var(--ox-spin) inside conic-gradient() is invalid at computed-value
   time — which would throw away the whole `background` declaration
   and leave no outline at all. Declaring it as an ordinary property
   too keeps the gradient valid, so the button degrades to a static
   chrome ring rather than to nothing. */
@property --ox-spin {
  syntax: '<angle>';
  inherits: false;
  initial-value: 0deg;
}

.w-authPageWidth form button[type='submit'] {
  --ox-spin: 0deg;
  position: relative;
  isolation: isolate;
  height: 3.25rem;
  border: 0;
  border-radius: 999px;
  background-color: rgb(var(--black));
  background-image: none;
  box-shadow: none;
  color: rgb(var(--ox-chrome));
  font-size: 0.75rem;
  font-weight: 500;
  line-height: 1;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  text-indent: 0.18em; /* balances the tracking's trailing space so the label sits optically centred */
  transition:
    color 0.18s var(--ox-e-out),
    transform 0.14s var(--ox-e-out);
}

.w-authPageWidth form button[type='submit']::before {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  padding: 1px;
  /* The resting chrome sits at 0.26, not lower. At 0.13 the stretch
     between glints fell below the point where a 1px line survives
     against black, so the pill looked half-drawn — a missing border
     rather than a dim one. The outline must read as continuous at all
     times; only the HIGHLIGHT travels. */
  background:
    conic-gradient(
      from var(--ox-spin),
      rgb(var(--ox-chrome) / 0.26) 0deg,
      rgb(var(--ox-chrome) / 0.26) 32deg,
      rgb(var(--ox-chrome-hi) / 0.95) 60deg,
      rgb(var(--ox-chrome) / 0.26) 90deg,
      rgb(var(--ox-chrome) / 0.26) 212deg,
      rgb(var(--ox-chrome-hi) / 0.5) 238deg,
      rgb(var(--ox-chrome) / 0.26) 266deg,
      rgb(var(--ox-chrome) / 0.26) 360deg);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask-composite: exclude;
  animation: ox-outline-spin 11s linear infinite;
}
@media (min-resolution: 2dppx) {
  .w-authPageWidth form button[type='submit']::before { padding: 0.5px; }
}
@keyframes ox-outline-spin {
  to { --ox-spin: 360deg; }
}

/* Two glints, roughly opposite and of unequal brightness, so the
   motion reads as a highlight running over a curved metal edge rather
   than as a dot on a loop. Hover only quickens it and brings the
   label up to white — no new shadow, no fill. */
.w-authPageWidth form button[type='submit']:hover {
  color: rgb(255 255 255);
}
.w-authPageWidth form button[type='submit']:hover::before {
  animation-duration: 6s;
}
.w-authPageWidth form button[type='submit']:active {
  transform: scale(0.988);
}

/* The travelling ring is present whether or not the control is
   focused, so it cannot double as a focus indicator. Focus stops the
   motion and lights the whole perimeter instead — unmistakable, and
   still made of the same chrome. */
.w-authPageWidth form button[type='submit']:focus-visible {
  outline: none;
}
.w-authPageWidth form button[type='submit']:focus-visible::before {
  background: rgb(var(--ox-chrome-hi));
  animation: none;
}

/* Motion is ornament here, so it goes entirely when asked — leaving
   the ring lit from above, like every other edge on the page. */
@media (prefers-reduced-motion: reduce) {
  .w-authPageWidth form button[type='submit']::before {
    animation: none;
    background: linear-gradient(
      180deg,
      rgb(var(--ox-chrome) / 0.55),
      rgb(var(--ox-chrome) / 0.26) 50%,
      rgb(var(--ox-chrome) / 0.3));
  }
}

/* ─── The footer line ──────────────────────────────────────────────
   An aside, so it sits quieter than the form it follows. */
.w-authPageWidth > p {
  margin: 1.25rem 0 0.25rem;
  font-size: 0.8125rem;
  color: rgb(var(--text-tertiary));
}

/* ═══ THE MODEL PICKER — pills, and nothing else ═══════════════════
   Scoped by `:has(canvas.ox-model-orb)`, so it matches exactly the
   four rows ox-model-orbs.js has marked and cannot reach any other
   combobox. If the orb script is not running, none of this applies
   and the picker falls back to stock — the styling and the thing it
   styles ship together.

   Flat on purpose: no lip, no lit top edge, no ember ring. The pills
   carry themselves on shape and fill alone. */

/* The panel. LibreChat draws a black rounded rectangle with a border
   behind the list; with the rows now being pills that was a container
   around four containers, so it goes and the pills float over the
   page.

   overflow-x MUST be hidden. The dialog is `overflow: auto`, and the
   pills' own side margins make the content 4px wider than the box —
   enough for a horizontal scrollbar to appear as a bright bar under
   the last pill. It read as a stray divider, which is exactly what it
   was not. */
div[role='dialog']:has([role='option'] canvas.ox-model-orb) {
  background-color: transparent;
  border: 0;
  box-shadow: none;
  overflow-x: hidden;
  scrollbar-width: none;
}
div[role='dialog']:has([role='option'] canvas.ox-model-orb)::-webkit-scrollbar {
  width: 0;
  height: 0;
}

/* The search field goes entirely. It exists to filter a long list;
   there are four models, all visible at once, so it was a text input
   guarding nothing. It also carried `bg-inherit`, so with the panel
   removed it had no background of its own and the page showed
   straight through its text. */
div[role='dialog']:has([role='option'] canvas.ox-model-orb) > div.sticky {
  display: none;
}

/* Horizontal padding lives on the LIST, not on the pills. Side
   margins on the rows made the content 4px wider than the dialog,
   which is what produced the horizontal scrollbar; hiding overflow
   only masked it and left the list scrollable sideways by 4px. */
div[role='dialog']:has([role='option'] canvas.ox-model-orb) [role='listbox'] {
  padding-inline: 0.375rem;
}
[role='option']:has(canvas.ox-model-orb) {
  position: relative;
  margin: 0.25rem 0;
  padding: 0.5rem 1rem 0.5rem 0.5rem;
  border-radius: 999px;
  background-color: rgb(var(--black));
  box-shadow: none;
  transition: background-color 0.22s cubic-bezier(0.16, 1, 0.3, 1);
}

/* LibreChat marks the active row with a 2px bar pinned to the row's
   left edge (`before:left-0 before:w-0.5`). On a stadium that sits
   outside the curve and reads as a stray tick. */
[role='option']:has(canvas.ox-model-orb)::before { display: none; }

/* Black in EVERY state, selected included. The row already carries a
   tick when it is the live model, so lifting the fill to grey was
   saying the same thing twice — and grey is the one thing these pills
   should never be, since the whole point is four coloured objects on
   black. Hover gets the smallest possible acknowledgement: a warm
   near-black, not a step toward grey. */
[role='option'][aria-selected='true']:has(canvas.ox-model-orb) {
  background-color: rgb(var(--black));
}
[role='option']:has(canvas.ox-model-orb):hover {
  background-color: rgb(var(--surface-primary-alt));
}

/* The name leads, the description follows quieter. Both were the same
   weight, which is what made the row read as a paragraph. */
[role='option']:has(canvas.ox-model-orb) span.truncate {
  font-weight: 500;
  letter-spacing: 0.005em;
}
[role='option']:has(canvas.ox-model-orb) span.break-words {
  color: rgb(var(--text-tertiary));
}

/* ─── The sidebar toggles ──────────────────────────────────────────
   Both of them — the header's `open` and the drawer's `close`, which
   are the same control in two states and must not disagree. Stock
   LibreChat gives each `rounded-xl border border-border-light`, a
   hairline square drawn around an icon that is already legible.

   Only the outline goes. Size, hover fill and the focus-visible ring
   are all untouched, so the control keeps its affordance and stays
   keyboard-navigable — the border was decoration, the ring is not. */
#open-sidebar-button,
#close-sidebar-button {
  border-color: transparent;
}
