/* AudiophileGPT page styles. Markup: index.html. Behavior: app.js. Serves the
   app page, the two selling pages (features.html, signup.html), the
   post-checkout page (billing-welcome.html) and the three legal
   pages (privacy, terms, refunds).

   Look 2026-09-20: the neutral product look. Tj revoked the warm palette in
   person -- "it looks fucking horrible and i hate all this ugly brown/green
   shit" -- and the ruling lands on top of a short history: the 09-14 build
   was violet and he called it AI generated, the 09-17 "Listening Room"
   replacement went warm (paper, olive, brass) and he revoked it a week
   later. The lesson is that personality in the PALETTE is the risk, not in
   the structure, so the colours are now true neutrals: a cool grey canvas,
   white surfaces, near-black ink, hairline rules, and one restrained signal
   red. What carries the identity is the structure, which stays: dead square
   corners, dense editorial layout, 18.4px Outfit, one drawer shadow.
   docs/AI-SPEAK-DETECTION.md still governs.

   What this file used to be was the 2026-07-30 canon that Tj revoked in person
   after seeing the result and calling it AI made, and every part of it was
   still in here: the 6px rainbow bar on all five pages, two blurred purple
   orbs painted on the body, backdrop-filter glass on both sticky bars, Inter
   and Bricolage Grotesque, uppercase tracked micro-labels on four elements,
   one 16px radius on every object in the file, and three dots bouncing on an
   infinite loop. Glass, gradients, tag pills and emoji are out. If a gradient
   turns up in this file again it is a bug, unless a comment says why.

   TWO COLOURS, TWO JOBS, and they are not interchangeable. --accent is a FILL
   and nothing else: it is the ink a primary button is painted with, and its
   label is --accent-ink, never a literal #fff (the fill is light in the dark
   theme and a white label would vanish). --signal is the SIGNAL: the rule
   beside an answer the page is flagging, the live dot, the mark. At its raw
   value it is a mark and never a word: it clears the 3:1 non-text floor and
   not the 7:1 text one. Text that has to read as signal wears --signal-ink
   instead, which clears 7:1 on every ground it touches.

   Three rules do not move with the palette, because they are about who reads
   this page rather than how it looks:

     * 18.4px base type and the three-step size control. The base comes from
       `body` font-size; the control multiplies the root from app.js
       SIZE_STEPS, so nothing here may set a fixed px size on body text.
     * AAA contrast on every line of text, the muted ones included. The repair
       palette's own muted (#6f637c) measures 4.99:1 on this canvas, under the
       7:1 floor, so the muted, link, danger and warning values below were
       darkened and measured instead of copied. Every one carries its ratio.
     * 48px minimum targets, from --tap.

   Comments in this file record measurements and reasons, not colors: the same
   hex appears in several rules and the ratio depends on which ground it sits
   on, which is the thing that is easy to get wrong later. */

/* The one typeface, served from this origin: Outfit, a variable font
   covering every weight the file uses (400 body, 600 buttons and labels,
   700 headlines). It is the latin subset Google served, downloaded once,
   so a font request is never a page view reported to a third party:
   section 8 of the privacy policy stopped having to list Google as a
   processor because of it.

   font-weight: 300 800 on the face is the variable range, not six separate
   downloads: the browser picks the instance it needs from the one file.
   display: swap keeps text visible while the file loads.

   Share Tech Mono was retired with the 2026-09-20 palette revocation: the
   display mono belonged to the Listening Room look. Code and figures now
   wear the visitor's own system mono through --mono, which cannot fail to
   load and adds no request. */
@font-face {
  font-family: 'Outfit';
  src: url('/static/fonts/outfit-var.woff2') format('woff2');
  font-weight: 300 800;
  font-style: normal;
  font-display: swap;
}

:root {
  /* Light until something says otherwise. "Otherwise" is the dark block that
     follows this rule: a data-theme attribute the visitor set by hand, or the
     operating system's own preference when they left it on automatic. app.js
     keeps the <meta name="color-scheme"> tag in step, which is what the
     things decided before the stylesheet arrives (scrollbars, form controls)
     read. */
  color-scheme: light;

  /* grounds. Four neutral planes, hairline-separated. The 2026-09-20 ruling
     killed the warm tints, so the ladder is cool grey and it does include
     white: the canvas is a grey, the answers sit on true white, and the
     sidebar is one step deeper so the log reads as a separate surface. */
  --canvas: #f7f6fc;
  --canvas-deep: #f0eefa;    /* sidebar */
  --surface: #ffffff;        /* header, panels, answers, inputs */
  --surface-hover: #f4f2fd;
  --bubble-mine: #f4f2fd;    /* the visitor's own question, one step off surface */

  /* Text, and the ground each value has to survive. The sidebar is the deepest
     plane in the file and it is where the muted values come tightest, so the
     floor is measured there rather than on the flattering surface. Each
     number below is the WORST ratio the token meets anywhere it is used, and
     every one of the text-on-ground pairs this stylesheet creates was measured
     at 7:1 or better. Re-measure before changing any hex here. */
  --ink: #191428;            /* worst 15.63:1, inline code on the tint */
  --muted: #4b4560;          /* worst 7.90:1, on the sidebar. */
  --link: #5b21b6;           /* worst 7.83:1, on the sidebar. Violet, and
                                deep enough to read as text rather than as
                                ink that happens to be blue. */
  --link-hover: #3f1580;     /* worst 11.23:1. Hover goes darker on light
                                ground, the reverse of what it does on dark. */
  --danger: #8a1f2a;         /* worst 7.74:1, on the sidebar */
  --warn: #6d4a03;           /* worst 7.38:1, on the canvas. A dark amber: a
                                bright one is unreadable at this size. */
  --line: rgba(76, 29, 149, .16);
  --line-strong: rgba(76, 29, 149, .34);

  /* The three washes that used to be typed as literals where they were used.
     Named now because the dark theme has to re-state every one of them: a red
     wash tuned for paper turns to mud on a near-black one, and a rule the dark
     blocks cannot reach is a rule that breaks the theme. */
  --danger-wash: rgba(138, 31, 42, .06);
  --danger-line: rgba(138, 31, 42, .32);
  --backdrop-wash: rgba(25, 20, 40, .5);

  --accent: #6d28d9;         /* THE ONE FILL. Ink, and never a word: it is what
                                a primary button is painted with and what a text
                                selection is drawn in. The label on it is
                                --accent-ink, below, so the dark theme can flip
                                the fill to paper without every rule in this
                                file that hardcoded a white label going blank. */
  --accent-ink: #ffffff;     /* white on the violet: 7.10:1. NOT a literal #fff. */
  --accent-dark: #5b21b6;    /* hover and active fill. White on it: 8.63:1. */
  --accent-soft: rgba(109, 40, 217, .08);

  /* The signal colour: Tomasetti violet (Tj, 2026-09-26 -- "i don't like the
     red -- it should be tomasetti purple and white"). It is the same value as
     --accent, and it is a MARK and never a word: #6d28d9 is 7.10:1 on white,
     so it clears both the 3:1 non-text floor it needs for a rule or an
     indicator dot and the 7:1 text floor it is held to anyway. Text that has
     to read as signal wears --signal-ink, #5b21b6, 7.83:1 on the sidebar.
     --danger stays red because an error is the one place the palette must be
     able to shout in a different voice than the brand. */
  --signal: #6d28d9;
  --signal-ink: #5b21b6;

  /* THE SIGNATURE GRADIENT + the aurora's light (2026-10-16, "the real app"
     pass). Tj set the bar by reference: "make audiophilegpt feel like a real
     app.. how does google make gemini look so cool? that's what I mean" and
     picked the flavor by name: house violet aurora. One gradient, used as a
     brand element everywhere -- the tube, the greeting, the send key, the
     Upgrade buttons, the quota pill -- plus three drifting fields behind the
     app page. The stops are tokens so the dark block can brighten them and
     nothing here is a frozen hex (the tube-lockup test holds the same line).
     --grad-text is the readable cut for TYPE: the bright amber is 2:1 on the
     canvas, so text gets the deeper amber (#b45309, 4.6:1) instead. */
  --stop-a: #6d28d9;
  --stop-b: #c026d3;
  --stop-c: #f59e0b;
  --grad: linear-gradient(100deg, var(--stop-a) 0%, var(--stop-b) 52%, var(--stop-c) 104%);
  --grad-text: linear-gradient(100deg, #6d28d9 0%, #a21caf 46%, #c026d3 72%, #b45309 100%);
  --aurora-a: rgba(109, 40, 217, .30);
  --aurora-b: rgba(192, 38, 211, .22);
  --aurora-c: rgba(245, 158, 11, .16);

  --sans: 'Outfit', 'Segoe UI', system-ui, sans-serif;
  --display: 'Outfit', 'Segoe UI', system-ui, sans-serif;
  /* The serif display register (2026-10-01 de-clone pass). The app shell's
     headlines wear the same face the landing page chose in Task 2: Fraunces
     600, self-hosted. The @font-face itself lives in landing.css, because
     this file is contractually held to exactly one @font-face block
     (test_page.py, the anti-CDN-drift pin) -- the token is defined here so
     the family is named once, beside the other type tokens, and every page
     that never loads landing.css simply never resolves it. On the app page
     the [data-app] block below re-points --display at this stack; the
     selling, legal and operator pages keep the sans --display. Numerals in
     the shell's stat surfaces wear --mono instead, the spec-sheet register.
     No new color pairs were introduced, so no contrast ratios moved: the
     face changes, the measured ink-and-ground pairs do not. */
  --display-serif: 'Fraunces', 'Iowan Old Style', Georgia, serif;
  --mono: ui-monospace, 'Cascadia Mono', 'SF Mono', Consolas, monospace;

  /* SQUARE. All three, and that is the point rather than a shorthand: 42
     rounded objects against one square corner was the measurement that
     started this redesign, and docs/AI-SPEAK-DETECTION.md names it outright,
     "12-20px border radius on EVERYTHING -- Rounded corners everywhere = AI
     design." A machined panel has square corners; the radii are kept as
     tokens because 40-odd rules name them and one value each is how the
     language stays consistent if that ever changes. The two pill radii that
     survived (the beta chip, the jump button) are squared separately where
     they are declared.

     That "if that ever changes" arrived on 2026-09-20, from the other
     direction: Tj's three reference screenshots of real chat products are
     rounded everywhere, and they are the standing spec for this app. The
     change lives in the :root[data-app] block below -- scoped to the app
     page, so the marketing and legal pages keep the square language this
     comment was written for. */
  --radius: 0;               /* panels, bubbles, the callout */
  --radius-sm: 0;            /* buttons, inputs, chips */
  --radius-xs: 0;            /* the small text-size buttons */

  --tap: 3rem;               /* 48px at the default root size, more at step 3 */

  /* The measure, named, because four rules have to agree on it: the reading
     column, the system pane, the post-checkout page, and the composer, whose
     asymmetric padding exists to re-centre its contents on the same box the
     answers sit in. It was 52rem typed into five places and a `calc((100% -
     52rem) / 2 + 22px)` that had to be edited in lockstep, which is the shape
     of a bug waiting.

     47rem, down from 52. An answer set as plain text runs the full width of
     this box rather than sitting inside a bubble narrower than it, and the
     text the visitor actually reads most is the text that suffers first when
     the line is too long. 752px less its padding is about 80 characters in
     Outfit at 17.6px, which is the top of the range a line of prose wants and
     the widest this audience should be asked to track. */
  --col: 47rem;

  /* One curve for everything in the file that moves. Fast out of the gate and
     slow to settle, so a control reads as a physical object coming to rest
     instead of a value being interpolated. The browser default `ease` is a
     compromise curve, and the mobile drawer is where that showed most.

     Presses run much shorter than hovers on purpose. A hover can take its
     time; a press has to be finished inside the tap itself or it reads as
     lag on a page whose whole job is answering fast. */
  --ease: cubic-bezier(.32, .72, 0, 1);

  /* One shadow in the file, on the mobile drawer, where something genuinely
     floats over something else. Everything else is separated by a hairline. */
  --drawer-shadow: 2px 0 20px rgba(20, 22, 24, .18);
}

/* ── dark theme ─────────────────────────────────────────────────────────
   The dark theme is not an inverted light build, it is the same layout with
   the lamp off: the canvas turns to neutral near-black, and the ink fill
   flips to paper while its label flips to ink. That inversion is the whole
   reason --accent-ink exists as a token -- every rule in this file reads it
   instead of a literal white.

   Two blocks carry the same list because there are two ways in: a visitor who
   picked Dark on the button, and one on automatic whose operating system said
   dark. The attribute block handles the first, the media query the second,
   and `:not([data-theme="light"])` is what keeps the hand-picked light
   setting winning over the OS inside the media query.

   The contrast floor does not move for dark: the same 7:1 rule, measured on
   the worst ground each token meets, which is still the sidebar
   (--canvas-deep, the deepest plane). Re-measure before changing any hex
   here, light or dark.

   The signal comes back up on this ground rather than down: at #c4b5fd it
   measures 10.36:1 on the surface, so the dark room paints its one loud
   colour with LILAC and labels it with near-black ink (--accent-ink,
   10.22:1) rather than white -- white on lilac is 2.1:1 and would have made
   the send arrow a smudge. --signal-ink goes lighter (#d3c4fb, 11.89:1)
   instead of darker, the same direction every other text token moves here. */
:root[data-theme="dark"] {
  color-scheme: dark;

  /* Repainted 2026-09-26 (Tj: "purple and black for dark mode"), which
     retires the warm 09-21 pass: the ground is now true black with the room
     lifted a few degrees above it, and the whole surface family is violet.
     Every text pair here was measured against its WORST ground and clears
     7:1 -- ink 15.77:1, muted 7.62:1, links 10.36:1. */
  --canvas: #08050e;         /* the room: black with the brand in it */
  --canvas-deep: #000000;    /* sidebar: true black, the deepest plane */
  --surface: #120c1f;        /* header, panels, answers, inputs */
  --surface-hover: #1b1330;
  --bubble-mine: #1a1229;    /* the visitor's own question, one step off surface */

  --ink: #ece7f5;            /* 15.77:1 on the surface */
  --muted: #a79fc0;          /* 7.62:1 on the surface, the worst ground */
  --link: #c4b5fd;           /* 10.36:1 on the surface */
  --link-hover: #ded5fe;     /* hover goes lighter on dark ground */
  --danger: #f2a49c;         /* 9.72:1 */
  --warn: #e6b766;           /* 9.94:1 */

  --line: rgba(196, 181, 253, .20);
  --line-strong: rgba(196, 181, 253, .36);

  --danger-wash: rgba(242, 164, 156, .08);
  --danger-line: rgba(242, 164, 156, .34);
  --backdrop-wash: rgba(0, 0, 0, .62);

  --accent: #c4b5fd;         /* the fill flips to lilac, not to paper. */
  --accent-ink: #131016;     /* ink on it: 10.22:1 */
  --accent-dark: #ded5fe;    /* hover goes brighter, not darker, on dark. */
  --accent-soft: rgba(196, 181, 253, .10);

  --signal: #c4b5fd;
  --signal-ink: #d3c4fb;     /* lighter, not darker: dark-ground rule. */

  /* The signature gradient, dark cut (2026-10-16): the stops brighten and the
     aurora's light doubles, which is where the glow showpiece lives. --grad
     itself reads these stops, so it follows with no second declaration. */
  --stop-a: #8b5cf6;
  --stop-b: #e879f9;
  --stop-c: #fbbf24;
  --grad-text: linear-gradient(100deg, #a78bfa 0%, #e879f9 50%, #fbbf24 100%);
  --aurora-a: rgba(139, 92, 246, .50);
  --aurora-b: rgba(232, 121, 249, .42);
  --aurora-c: rgba(251, 191, 36, .26);

  --drawer-shadow: 2px 0 20px rgba(0, 0, 0, .5);
}
@media (prefers-color-scheme: dark) {
  /* Two scopes opt in to following the operating system, and both are
     written on the <html> tag of the pages that mean it.

     2026-09-21, the app page LEFT this scope on Tj's "still looks pretty
     bad": auto used to follow the OS, which meant the default first
     impression for most visitors (and for Tj) was the cold near-black
     room. The default is now the warm light theme -- the face every
     reference product opens on -- and an explicit Dark choice still wins
     through the [data-theme="dark"] block above, so the toggle lost
     nothing. [data-auto] (legal, selling, post-checkout, 404) still
     follows the OS: those pages have no toggle, so the OS is the only
     vote they have.

     Deliberately NOT scoped: the operator screen (/admin), which carries
     inline styles written against paper. It stays light-only until
     someone reads it in dark on purpose.

     The :not([data-theme="light"]) keeps a hand-picked Light winning over
     the OS here, same as in the attribute block above. */
  :root[data-auto]:not([data-theme="light"]) {
    color-scheme: dark;

    --canvas: #08050e;
    --canvas-deep: #000000;
    --surface: #120c1f;
    --surface-hover: #1b1330;
    --bubble-mine: #1a1229;

    --ink: #ece7f5;
    --muted: #a79fc0;
    --link: #c4b5fd;
    --link-hover: #ded5fe;
    --danger: #f2a49c;
    --warn: #e6b766;

    --line: rgba(196, 181, 253, .20);
    --line-strong: rgba(196, 181, 253, .36);

    --danger-wash: rgba(242, 164, 156, .08);
    --danger-line: rgba(242, 164, 156, .34);
    --backdrop-wash: rgba(0, 0, 0, .62);

    --accent: #c4b5fd;
    --accent-ink: #131016;
    --accent-dark: #ded5fe;
    --accent-soft: rgba(196, 181, 253, .10);

    --signal: #c4b5fd;
    --signal-ink: #d3c4fb;

    /* Same dark cut as :root[data-theme="dark"] above (2026-10-16). */
    --stop-a: #8b5cf6;
    --stop-b: #e879f9;
    --stop-c: #fbbf24;
    --grad-text: linear-gradient(100deg, #a78bfa 0%, #e879f9 50%, #fbbf24 100%);
    --aurora-a: rgba(139, 92, 246, .50);
    --aurora-b: rgba(232, 121, 249, .42);
    --aurora-c: rgba(251, 191, 36, .26);

    --drawer-shadow: 2px 0 20px rgba(0, 0, 0, .5);
  }
}
/* ── the app's rounding ────────────────────────────────────────────────
   The 2026-09-14 SQUARE ruling is reversed for the app surface, by the
   owner's own references: three screenshots of real chat products (Tj,
   2026-09-20, "this is what I expect and AI chat to look like"). Rounded
   language is part of what makes those read as real products -- pills,
   rounded cards, a circular send key -- so the three radius tokens widen
   here, scoped to [data-app], the index page's <html> attribute.

   The scope is the point in both directions. The marketing and legal pages
   wear this same stylesheet and keep the square language they were ruled
   in; and the dark blocks above re-state colours only, so these numbers
   survive a theme switch untouched. docs/AI-SPEAK-DETECTION.md's
   "rounded everywhere" tell is overridden here by the references and
   nowhere else.

   --radius-hero is the composer's. 28px on a box that opens 60px tall is a
   full capsule, which is the shape every reference closes its page with;
   as the field grows past two lines the same number becomes a rounded
   rectangle, which is what those products do too. */
:root[data-app] {
  --radius: 20px;            /* panels, bubbles, the account card */
  --radius-sm: 12px;         /* buttons, inputs */
  --radius-xs: 9px;          /* the small text-size buttons, list rows */
  --radius-hero: 28px;       /* the composer */

  /* The serif register (2026-10-01 de-clone pass). One token re-point turns
     every app-shell headline -- the welcome hero, the thread title, the
     system pane, the rack sections, the quota wall -- from the ChatGPT-
     default geometric sans to the serif the landing page already wears. The
     scope is the whole point: only index.html carries [data-app], so the
     marketing, legal and operator pages keep the sans --display they were
     ruled in. The wordmark is exempted by hand (.brand h1 pins the sans
     below): a logo-like object whose lockup metrics are derived from
     Outfit's ink, not a headline. Fraunces ships one weight (600), so the
     serif rules state 600 rather than the 700 this file used to ask of
     Outfit -- a 700 request against a 600-only face is how synthetic bold
     slips in. */
  --display: var(--display-serif);
}

* { box-sizing: border-box; margin: 0; padding: 0; }

/* The `hidden` attribute has to beat the class rules further down. A UA
   `[hidden]` rule is the weakest thing in the cascade, so a class that sets
   `display` silently overrode it. Two real bugs came out of that: `.composer
   { display: flex }` meant the wall at zero free questions could not hide the
   ask box, so a spent visitor kept a working form and got a 402 out of it;
   and `.threadbar { display: flex }` meant the empty thread bar rendered on
   the welcome screen, putting a live "Delete this conversation" button in
   front of someone who had not asked anything yet. `!important` is the point
   here: it makes `hidden` mean hidden for every element, whatever the class
   says, which is what the attribute is for. test_page.py fails without it. */
[hidden] { display: none !important; }

html { font-size: 100%; }
html, body { height: 100%; }
body {
  font-family: var(--sans);
  font-size: 1.15rem;        /* 18.4px at the default root size */
  line-height: 1.65;
  color: var(--ink);
  min-height: 100vh;
  background: var(--canvas);
}
::selection { background: var(--accent); color: var(--accent-ink); }

.visually-hidden {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}
/* One focus style everywhere, thick enough to find without hunting. On this
   ground the accent itself is the ring: 6.90:1 against the canvas. */
a:focus-visible, button:focus-visible, textarea:focus-visible, input:focus-visible,
[tabindex]:focus-visible {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
  border-radius: var(--radius-xs);
}
/* Focus not obscured (WCAG 2.4.11): the app page pins the header at the
   top and the composer at the bottom, so a tab into a long thread can
   scroll a link to rest underneath one of them. scroll-padding on the
   scroller keeps every focus and anchor landing clear of both bars.
   Scoped to the app shell; the selling pages pin nothing. */
html[data-app] {
  scroll-padding-top: 96px;
  scroll-padding-bottom: 112px;
}
/* Motion, in one place, for every control in the file that takes a press.

   Two gaps this closes. There was no pressed state anywhere: every button
   changed on hover and then did nothing when the finger actually landed, and
   on a phone the press is the only state there is, because there is no hover.
   And the transitions that did exist ran on the browser default `ease`.

   Two controls are excluded by name, each for its own reason. `.backdrop` is
   the full-screen click catcher behind the mobile drawer, so scaling it pulls
   its edges in and shows a strip of the page around them. `.chip` fires its
   question on the press: the whole card is gone a frame later, replaced by the
   thread, so a shrink has nothing left to confirm anything to. (It was also a
   full-bleed hairline row when this was written, where a 2% shrink dragged the
   dividing line in from both ends while its neighbours' stayed put. The card
   shape took that reason away and left the one above, which is the real one.)
   Everything else here is a <button>, a `.ctabtn`, or an `a.cta`, and there
   are no icon-only controls to miss.

   This wins over the `transition` shorthands on `.newchat` and `.convopen`
   further down (two `:not()` classes against their one) and is a superset of
   both, so those were left alone rather than edited to say it twice.

   The press stays on under reduced motion, which is why it sits outside the
   block at the bottom of this file. That block zeroes transition durations, so
   the transform becomes an instant 2% step: the tap still answers back, there
   is just no travel. */
button:not(.backdrop):not(.chip), .ctabtn, a.cta {
  transition: background .15s var(--ease), border-color .15s var(--ease),
              color .15s var(--ease), transform .09s var(--ease);
}
button:not(.backdrop):not(.chip):active, .ctabtn:active, a.cta:active {
  transform: scale(.98);
}
a { color: var(--link); text-decoration: underline; text-underline-offset: 2px; }
a:hover { color: var(--link-hover); }

.skip {
  position: absolute; left: 8px; top: -100px; z-index: 40;
  background: var(--accent); color: var(--accent-ink);
  padding: 12px 18px; border-radius: var(--radius-sm); text-decoration: none;
  font-weight: 600;
}
.skip:focus { top: 8px; }

/* The no-JavaScript strip, the first thing on the page when scripting is off.
   It was an inline padding attribute, which is a style decision living in the
   markup; here it reads as the page talking, on a surface with a hairline
   under it like every other plane boundary. It carries real instructions (the
   API endpoint), so it stays readable prose at the page's own size rather
   than shrinking to a banner. */
.noscript {
  padding: 22px;
  background: var(--surface);
  border-bottom: 1px solid var(--line);
  color: var(--ink);
  max-width: none;
}
.noscript code {
  font-family: var(--mono);
  font-size: .92em;
  background: var(--canvas-deep);
  border: 1px solid var(--line);
  border-radius: 0;
  padding: 1px 5px;
}

/* ── shell ─────────────────────────────────────────────────────────── */
/* 17.5rem, down from the 19rem the shell opened with (2026-10-01 de-clone
   pass): the default sidebar width is the loudest ChatGPT tell on the page,
   and the column carries a conversation list, not a form -- the panel that
   did not fit here moved to the middle column in the 2026-09-14 pass. The
   48px row floor is untouched; only the column and its air got tighter. */
.app { display: grid; grid-template-columns: 17.5rem 1fr; min-height: 100vh; }

/* ── sidebar: the memory ───────────────────────────────────────────── */
.sidebar {
  background: var(--canvas-deep);
  border-right: 1px solid var(--line);
  display: flex; flex-direction: column; gap: 11px;
  padding: 16px 13px; height: 100vh; position: sticky; top: 0;
  overflow-y: auto;
}
/* The new-thread pill. It was a filled accent slab, which put the loudest
   button on the page in the quietest column of it; every reference Tj
   supplied (2026-09-20) starts a thread from a bordered pill that carries
   the weight of a list row. Surface ground, hairline, the row radius -- it
   reads as one more thing the column holds rather than as a call to action
   shouting across it. The + is markup, aria-hidden, so the accessible name
   stays exactly "Start a new conversation". */
.newchat {
  min-height: var(--tap); width: 100%;
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  font: inherit; font-size: .98rem; font-weight: 600;
  cursor: pointer; padding: 8px 13px;
  display: flex; align-items: center; gap: 9px;
  transition: background .15s, border-color .15s;
}
.newchat:hover { background: var(--surface-hover); border-color: var(--line-strong); }
.newchat .plus { font-size: 1.15rem; font-weight: 500; color: var(--muted); line-height: 1; }

/* The search over the conversation list. It sits between the new-thread
   button and the list it narrows, the same 19rem column every other sidebar
   control lives in. `type="search"` gives the platform's own clear
   affordance, and Escape empties it too (app.js, where the press is also
   kept from closing the drawer the box sits inside on a phone). */
.convsearch {
  width: 100%; min-height: var(--tap);
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  font: inherit; font-size: max(1rem, 16px);
  padding: 8px 12px;
}
.convsearch:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.convsearch::placeholder { color: var(--muted); }

/* The display settings row: theme icon left, text-size bank right. One row,
   not two stacked controls, because two settings a visitor touches a handful
   of times in the life of the account do not each earn a full-width row in a
   column that is mostly a conversation list. The theme glyph is the only
   icon the app surface draws; it carries its state by which of the three
   glyphs is showing (app.js sets data-mode on the button), and its
   accessible name in words through aria-label, so nobody meets the state by
   glyph alone. */
.sideopts {
  display: flex; align-items: center; gap: 4px;
}
.themebtn {
  min-width: 2.6rem; min-height: 2.6rem; cursor: pointer;
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent; color: var(--muted);
  border: 1px solid transparent; border-radius: var(--radius-xs);
  padding: 0;
  transition: background .15s, color .15s;
}
.themebtn:hover { color: var(--ink); background: var(--surface-hover); }
.themebtn .ticon { width: 1.15rem; height: 1.15rem; display: none; }
.themebtn[data-mode="auto"] .tauto,
.themebtn[data-mode="dark"] .tdark,
.themebtn[data-mode="light"] .tlight { display: block; }

/* Labels. Sentence case at a size that reads as a caption, not a heading,
   and -- since the 2026-10-01 de-clone pass -- in the serif display face:
   the drawer's headings ("Your conversations", "Account", "Reset your
   password") are the register the brief calls section labels, and Fraunces
   at caption size reads as a printed field label rather than a chat app's
   chrome. Still muted, still small, no text-transform anywhere below; the
   hierarchy against the rack h3s under the system pane is carried by size
   and ink exactly as before. */
.sideh {
  font-family: var(--display);
  font-size: .84rem; font-weight: 600;
  color: var(--muted);
  margin-top: 4px; padding: 0 4px;
}

/* The bench log. The rows were ruled edge to edge -- a log, not a feed of
   tiles -- until Tj's reference screenshots (2026-09-20) settled what a
   conversation list in a real chat product looks like: soft rounded rows
   that light up under the pointer. Rounding is the app's language now (see
   the :root[data-app] block), and a rounded row needs air around it to read
   as a row at all, so the full-width rules are gone and each entry carries
   its own radius and hover ground. The margin tick that marks the open
   conversation survives untouched: that is the one part of the log grammar
   the references do not speak to, and it is the fastest read on the page. */
.convlist {
  list-style: none; display: flex; flex-direction: column; gap: 2px;
}
.convlist li { display: flex; }
.convopen {
  flex: 1; text-align: left; cursor: pointer; font: inherit;
  background: transparent; border: 0; border-radius: var(--radius-xs);
  padding: 8px 11px 8px 14px;
  min-height: var(--tap); color: var(--ink);
  display: flex; flex-direction: column; gap: 3px;
  transition: background .15s;
}
.convopen:hover { background: var(--surface-hover); }
/* The entry you are reading is marked in the margin, the way a log marks the
   line you are on, and it is marked with nothing else. It carried a filled
   surface and a full border as well, and a marked row that is also a panel is
   a card among cards: the emphasis lands on the box instead of on the entry,
   and the eye has to compare two panel treatments to find the one it is in.
   One signal tick in the margin answers that in the time it takes to see it. */
.convopen[aria-current="true"] { box-shadow: inset 3px 0 0 var(--signal); }
.convopen[aria-current="true"] .convtitle { font-weight: 700; }
.convtitle {
  font-size: .98rem; line-height: 1.4; font-weight: 600;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;
  overflow: hidden;
}
/* The row's timestamp. Mono, in the spec-sheet register the 2026-10-01 pass
   puts the shell's numerals in (the quota pill and the system readout wear
   the same face): a bench log prints its figures in figures. Face only --
   the ink is the same muted token, so the measured 7:1 floor is untouched. */
.convmeta { font-family: var(--mono); font-size: .74rem; color: var(--muted); }
.empty {
  font-size: .95rem; color: var(--muted); line-height: 1.55;
  padding: 6px 6px 0;
}
/* The empty note is not an entry, so it gets the entry treatment and nothing
   more: no rule of its own in a list that no longer has any. */
.convlist li:has(> .empty) { border-bottom: 0; }
/* The sidebar foot: one line of trust claim, two links, nothing else. The
   two-sentence storage paragraph and the fleet attribution line both left in
   the reference pass (Tj, 2026-09-20: the sidebar is not a legal page) --
   the disclosure they carried lives on /privacy and /terms, and the links to
   both sit right here and under the composer both. Attribution lives on the
   marketing pages, which is where a visitor reads who makes this. */
.sidefoot {
  padding: 10px 5px 0;
  font-size: .82rem; color: var(--muted); line-height: 1.55;
}
.sidefoot a { color: var(--muted); }
.sidefoot a:hover { color: var(--ink); }

/* The anonymous visitor's standing account line (see index.html). Same
   register as the sidefoot it sits above, so the sidebar keeps one quiet
   voice; the button is a text action, not a chrome button, because a filled
   control here would sell harder than the line beside it. */
.sideacct {
  margin: 0; padding: 10px 5px 0;
  font-size: .82rem; color: var(--muted); line-height: 1.55;
}
.sideacctbtn {
  font: inherit; color: var(--muted); background: none; border: 0;
  padding: 0; margin-left: 4px; cursor: pointer;
  text-decoration: underline; text-underline-offset: 2px;
}
.sideacctbtn:hover { color: var(--ink); }
.sideacctbtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* ── main column ───────────────────────────────────────────────────── */
.col { display: flex; flex-direction: column; min-height: 100vh; min-width: 0; }

/* A solid white bar with a hairline under it. This was frosted glass
   (`backdrop-filter: blur(12px)` over a translucent ground), which is the
   single most reliable tell that a page was generated rather than designed,
   and it made every line under the bar soft at the edge. */
header {
  display: flex; align-items: center; gap: 14px; flex-wrap: wrap;
  padding: 13px 20px; border-bottom: 1px solid var(--line);
  background: var(--surface);
  position: sticky; top: 0; z-index: 5;
}
.brand { display: flex; flex-direction: column; }
/* The wordmark stays sans, deliberately, while every other headline on the
   app goes serif (2026-10-01): it is a logo-like object, and the tube
   lockup's numbers below are derived from Outfit's ink metrics (ascender
   .73em, cap .71em). It also keeps the app header and the landing page's
   header lockup identical, which is the fleet-consistent read. This pin is
   what the [data-app] --display flip above would otherwise override. */
.brand h1 {
  font-family: var(--sans);
  font-size: 1.35rem; font-weight: 700; letter-spacing: -.02em;
}

/* The lockup mark. Every number here comes from Outfit's real ink metrics,
   measured in the browser rather than eyeballed: ascender 0.73em, cap height
   0.71em, x-height 0.49em. The drawing's own box is the full glass-to-pin
   extent, so its height is the ascender plus however far the pins hang, and
   the pins are the only part of the mark allowed below the baseline.

     height .845em     = 0.73em of glass on the ascender + 0.115em of pins
     vertical-align    = the same 0.115em, so the box bottom lands with the
                         pins below the baseline and the glass top on the
                         ascender. One baseline, one top edge, whole lockup.
     margin-right .1em = air between the base's rounded corner and the A. The
                         mark is 0.55em wide against an A of 0.729em, so it
                         reads as a letter-width object and needs the gap or
                         it collides with the word.

   width stays auto so the browser keeps the viewBox ratio; the aspect is
   47.8 by 73, giving about 0.553em wide at this height.

   The color is the accent token, and this is the one place it is used as a
   stroke rather than a fill. The standalone mark.svg carries #6036b8 written
   in; inlined here it takes currentColor so the token stays the single
   source and the mark can sit on any ground the header ever gets. */
.brand h1 .tube {
  height: .845em;
  width: auto;
  vertical-align: -.115em;
  margin-right: .1em;
  color: var(--signal);
}

/* The beta chip. Tj, 2026-09-16: the site wears the label until the bigger
   kinks are worked out, so it is said plainly in the lockup rather than
   buried in a footer. Quiet means quiet: small sans, muted ink, a hairline
   in the page's own line color, a full radius like every other pill on the
   page. The squared mono-caps plate it wore read as a stamp screwed onto
   the wordmark. aria-hidden in the markup, so the heading's accessible name
   stays exactly "AudiophileGPT". */
.brand h1 .beta {
  font: 600 .52em/1 var(--sans);
  letter-spacing: .01em;
  color: var(--muted);
  border: 1px solid var(--line-strong);
  border-radius: 999px;
  padding: .28em .7em;
  margin-left: .55em;
  vertical-align: .42em;
  white-space: nowrap;
}

/* The text size control, as three bare glyphs, now in the sidebar's settings
   row beside the theme icon (the markup note there has the story of the
   move). It was in the header, first as a fieldset and then as a bare bank,
   and either way the bar spent its width on a setting a visitor touches
   once, while the account column -- where every other product keeps display
   settings -- held none.

   2.6rem square, which keeps the three of them plus the word on one row
   inside the 19rem column next to the theme button. It was 3rem in the
   header, where the budget was the whole bar; here the row shares the
   column with the theme control and the target floor is the one the
   sidebar's own rows set. */
.sizebtns { display: flex; gap: 2px; }
/* The word the glyphs never said. At the .8rem the sidebar's own labels
   wear, muted, so it reads as a caption on its own control: a visible name
   for the group, and through aria-labelledby the group's accessible name
   too, so the announced thing and the shown thing are one string. */
.sizelabel {
  font-size: .8rem;
  color: var(--muted);
  margin-left: auto;
}
.sizebtns button {
  min-width: 2.6rem; min-height: 2.6rem; cursor: pointer; font: inherit;
  background: transparent; border: 1px solid transparent;
  border-radius: var(--radius-xs); color: var(--ink); font-weight: 700; line-height: 1;
}
.sizebtns button:hover { background: var(--accent-soft); }
.sizebtns button[aria-pressed="true"] {
  background: var(--accent); color: var(--accent-ink); border-color: var(--accent);
}
.sizebtns button.s1 { font-size: 1rem; }
.sizebtns button.s2 { font-size: 1.2rem; }
.sizebtns button.s3 { font-size: 1.45rem; }

.menubtn {
  display: none; min-height: var(--tap); font: inherit; font-weight: 600;
  background: var(--surface); color: var(--ink); cursor: pointer;
  border: 1px solid var(--line-strong); border-radius: var(--radius-sm);
  padding: 10px 16px;
}
.whennarrow { display: none; }

main {
  flex: 1; width: 100%; max-width: var(--col); margin: 0 auto;
  padding: 20px 22px 8px;
  /* A flex column so the welcome screen can be centred in the space between
     the header and the composer with `margin-block: auto` rather than a magic
     top padding. Nothing else in here is a flex item that cares: the thread,
     the quota line and the wall panel are all block boxes that happen to be
     laid out by a flex parent, and a block in a column flex container with no
     margins of its own behaves exactly as it did in a block container. */
  display: flex; flex-direction: column;
}

/* ── the visitor's system (the sidebar's button and its own view) ────────
   The sidebar carries the button and the middle column carries the panel.
   The panel was built as a bordered card on the white plane above the hero,
   then moved under the account block in the sidebar, where it was ten fields
   and two lists in a 19rem column: at that width every field landed alone on
   its own row and the column read as clutter, because a column sized for a
   conversation list is not a form (Tj, 2026-09-14). The button and the view
   are the fix. Pressing the button swaps the answer for the panel; Back
   swaps it back.

   The swap is CSS, on body[data-view="system"], so app.js only sets the
   attribute. That keeps the composer's own hidden attribute meaning exactly
   one thing, the wall, instead of two: the wall and this view can both be
   true at once, and a show/hide pair in JS would have to remember which of
   the two put the composer away before it could put it back.

   Print is the one place both halves are wanted at once. The rule that hides
   #main on screen is body-qualified, so re-showing it on paper takes the
   same qualifier to beat.

   The panel keeps the hairline it wears here and does not go back to being a
   card. The two panels are sections of one screen now, and a card inside a
   screen is a frame drawn around the whole thing.

   The two section headings are h3 under the pane's h2 and stay in the
   display face rather than becoming .sideh like "Your conversations" and
   "Account". Those two name the lists they sit over; these two ask the
   visitor a question, and the question is why the panel gets opened at all.

   One field per row again, off a 14rem basis rather than the 11rem the rows
   were built with. The labels are sentences ("How far back you listen, in
   feet"), 14rem holds one whole at the 52rem column, and that basis lands
   the room's three sizes on one row and the rest in pairs. Under 60rem the
   basis is larger than the column and every row wraps to one field, which is
   the shape the panel had in the sidebar and the right one on a phone.

   Every color here is an existing token. The palette's comment says each of
   the 24 text-on-ground pairs was measured at 7:1 or better, so a new hex
   would arrive unmeasured. The fields are white on the canvas ground, the
   same as the composer below them. */

/* The way in, in the sidebar. A row shaped like the conversation rows it
   sits with -- rounded, no rule of its own -- and stacked rather than a line
   of text because the second line is the point: it answers "what does it
   know about me?" without the visitor opening anything. Since the landing
   nudge retired (Tj, 2026-09-20: the landing carries nothing about the
   rack), this row is the rack's only door, which is exactly the job a
   navigation row is for. It is aria-current, not a checked state, because
   it is a view switch like the conversation list's own items -- and the
   marker for that state is the same 3px signal inset edge the list's
   current row wears. */
.sysopen {
  padding: 8px 12px 8px 14px; width: 100%; cursor: pointer;
  display: flex; flex-direction: column; gap: 3px; text-align: left;
  font: inherit; background: transparent; color: var(--ink);
  border: 0; border-radius: var(--radius-xs);
}
.sysopen:hover { background: var(--surface-hover); }
.sysopen[aria-current="true"] {
  background: var(--surface-hover);
  box-shadow: inset 3px 0 0 var(--signal);
}
.sysname { font-weight: 600; font-size: 1rem; }
/* One line, always. The column is 17.5rem and a name can be any length the
   visitor types, so the second line clips rather than wraps: a button that
   grew a line taller with each long model number would move everything under
   it, and the list of what it holds is one press away. */
.sysmeta {
  font-family: var(--mono); font-size: .8rem; color: var(--muted);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* .sysmeta is .85rem muted, 11.6:1 on the surface; the hover and current
   states move it to --link, which is measured on the surface too. */
.sysopen:hover .sysmeta, .sysopen[aria-current="true"] .sysmeta { color: var(--link); }

.system {
  display: none;
  flex: 1; width: 100%; max-width: var(--col); margin: 0 auto; padding: 20px 22px 40px;
}
/* The pane is a display swap, so it cannot transition: nothing animates away
   from `display: none`. It arrives on a keyframe instead, which is also what
   puts it under the reduced-motion block at the foot of this file, since that
   rule zeroes animation-duration on elements.

   Why it moves at all: on a phone the button that opens this pane is inside
   the drawer, and showSystem() closes the drawer on the same press. The drawer
   slid out over .2s while the pane it opened into appeared in a single frame,
   so one press read as two unrelated things happening at once. .18s on the
   shared curve puts both halves in the same gesture.

   The fade starts at .35 rather than 0. This is a whole screen of form, and
   dissolving it up from nothing reads as a page load rather than as a panel
   being set down. Starting part-way visible is a confirmation that the press
   landed, which is all this needs to say. */
@keyframes panearrive { from { opacity: .35; } to { opacity: 1; } }
body[data-view="system"] .system {
  display: block;
  animation: panearrive .18s var(--ease);
}
body[data-view="system"] #main,
body[data-view="system"] .composer { display: none; }

.systemhead { padding-top: 4px; }
.sysback {
  min-height: var(--tap); cursor: pointer; font: inherit;
  font-weight: 600; font-size: .95rem;
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line-strong); border-radius: var(--radius-sm);
  padding: 10px 15px;
}
.sysback:hover { border-color: var(--accent); color: var(--link); }
/* The pane's title is the hero's size, because it is the same thing: the
   heading over the middle column. What sits under it is .welcome p's shape
   for the same reason. */
.system h2 {
  font-family: var(--display);
  font-size: 2rem; font-weight: 600; line-height: 1.25; letter-spacing: -.005em;
  margin-top: 14px;
}
.systemsub {
  color: var(--muted); margin-top: 14px;
  font-size: 1.05rem; line-height: 1.7; max-width: 40rem;
}

.rack { padding-top: 20px; margin-top: 26px; border-top: 1px solid var(--line); }
.rack h3 {
  font-family: var(--display);
  font-size: 1.2rem; font-weight: 600; line-height: 1.35; letter-spacing: 0;
}

.racklist { margin-top: 12px; list-style: none; display: flex; flex-direction: column; }
.racklist li {
  display: flex; align-items: center; justify-content: space-between;
  gap: 12px; border-top: 1px solid var(--line); padding: 6px 2px;
}
.racklist li:last-child { border-bottom: 1px solid var(--line); }
/* flex: 1 with min-width: 0 so a long component wraps inside its row rather
   than shoving the Remove button off the edge of the column. */
.racksys { flex: 1; min-width: 0; font-size: 1rem; line-height: 1.55; padding: 8px 0; }
/* Centring the action against the name is right for one line and wrong for
   two. A row whose name wraps puts Remove level with whichever line it
   landed beside, reading as if it belonged to that line. align-self puts the
   button on the first line and the bottom padding holds its label up there,
   so the box still carries the 48px tap target around a label sitting level
   with the name's first line. Single-line rows are unchanged: 48px was
   already the row height, and the label moves 4px. */
.rackremove {
  flex: none; align-self: flex-start;
  min-height: var(--tap); padding: 6px 4px 22px; cursor: pointer;
  background: transparent; border: 0; font: inherit;
  font-size: .9rem; color: var(--muted); text-decoration: underline;
}
.rackremove:hover { color: var(--danger); }

.rackform { display: flex; flex-direction: column; gap: 14px; margin-top: 16px; }
.rackrow { display: flex; gap: 12px; flex-wrap: wrap; }
.rackfield { flex: 1 1 14rem; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
.rackfield span { font-size: .9rem; font-weight: 600; color: var(--muted); padding-left: 2px; }
.rackfield input, .rackfield select {
  width: 100%; min-height: var(--tap);
  background: var(--surface); border: 1px solid var(--line-strong); color: var(--ink);
  border-radius: var(--radius-sm); padding: 10px 12px;
  font: inherit; font-size: max(1rem, 16px); outline: none;
}
.rackfield input::placeholder { color: var(--muted); opacity: 1; }
.rackfield input:focus, .rackfield select:focus {
  border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft);
}
.rackerr { color: var(--danger); font-size: .95rem; line-height: 1.5; }
/* One shape, unlike .acctbtns: these buttons are not a choice between two
   equals, they are the panel's one action plus, at most, the Core import.

   Their own width, not a share of the row. `flex: 1 1 auto` split the 52rem
   column into two 360px slabs with a nine-word label floating in the middle
   of each; the button is as wide as what it says and the row wraps when the
   column is narrow enough that it has to. */
.rackbtns { display: flex; gap: 10px; flex-wrap: wrap; }
.rackbtns button {
  flex: 0 0 auto; min-height: var(--tap); padding: 10px 18px; cursor: pointer;
  border-radius: var(--radius-sm); font: inherit; font-weight: 600; font-size: 1rem;
}
.rackbtns .rackadd { background: var(--accent); color: var(--accent-ink); border: 1px solid var(--accent); }
.rackbtns .rackadd:hover { background: var(--accent-dark); border-color: var(--accent-dark); }
.rackbtns .rackload {
  background: transparent; color: var(--link); border: 1px solid var(--line-strong);
}
.rackbtns .rackload:hover { border-color: var(--accent); background: var(--accent-soft); }
.rackbtns button[disabled] { opacity: .55; cursor: wait; }

/* The landing composition (Tj's reference anatomy, 2026-09-20): a big
   centred greeting, then the composer as the hero object with the starters
   under it -- one centred group, nothing parked between the greeting and
   the box. The DOM is already in that order (this section carries only the
   greeting now; the composer form follows it with the starters and the
   room plan inside it); what the CSS owns is the
   centring, and the centring is two auto margins: one above the greeting,
   one below the composer. Both autos have to live on items of .col itself
   (see the note on the hero .composer rule for why the composer's alone
   never worked), so the hero state moves them there and this rule's own
   `margin-block: auto` stays only as the no-:has() fallback: there the
   composer keeps its sticky band at the foot and the greeting centres in
   the space above it, the shape this shell wore for years. On a short
   window both autos resolve to zero and the group sits at the top the way
   it always did. */
.welcome {
  display: flex; flex-direction: column;
  margin-block: auto; padding: 18px 4px 14px;
}
.greet { text-align: center; }
.greet h2 {
  font-family: var(--display);
  /* The 2026-09-21 face lift capped the shout: a greeting that fills the
     window reads as a banner ad, and every modern chat reference greets at
     a size a sentence can live in. Serif since the 2026-10-01 de-clone
     pass (the token flip), at 600 against Fraunces' single weight, a half
     step larger to hold the hero the sans held, and Fraunces' own
     near-zero tracking rather than Outfit's -.015em. landing.css restates
     this rule for the app page and carries the same values. */
  font-size: clamp(1.8rem, 1.15rem + 2.3vw, 2.45rem);
  font-weight: 600; line-height: 1.18; letter-spacing: -.005em;
}

/* The drawing stands at the foot of the landing composition now, under the
   starters -- not beside the words, and never between the words and the
   box: in the reference anatomy (Tj, 2026-09-20) nothing sits between the
   greeting and the composer, and anything parked there pushes the hero,
   the box the visitor came to use, down the screen. It lives inside the
   composer form (before the can-make-mistakes line), which buys three
   things at once: the quota wall that hides the
   form hides the drawing too, the landing-only visibility is one CSS gate
   instead of a new element for app.js to carry, and the sticky in-thread
   composer never shows it. The old 700px phone reordering is gone with the
   grid it served; the DOM order is the composition at every width.

   It keeps its label and its numbers; it is the page's evidence that a
   person drew this, and the one thing on the landing that could not have
   been generated. The app landing no longer SHOWS it: Tj's reference
   anatomy (2026-09-20) has nothing that heavy between the composer and the
   footer, and the drawing was what pushed the presets and the nudge past
   the fold on a 900px window, and the first-run nudge rode with them. The
   markup stays in index.html because the
   three copies are kept identical on purpose (drift protection, pinned by
   test_the_room_drawing_is_the_same_drawing_on_all_three_pages); the
   marketing pages -- which share this stylesheet and this class -- keep
   the drawing visible at the 22rem they have always had. */
.roomplan { display: block; width: 100%; max-width: 22rem; height: auto; margin: 30px auto 0; }
.composer .roomplan { display: none; }

/* Two inks, the same split the shell uses, so the drawing is not a special
   case that drifts when the tokens move: signal is the geometry, ink is the
   furniture. Hairlines throughout, with exactly two filled faces, because a
   speaker baffle and a seat back are surfaces and a dimension line is not. */
.roomplan .rp-wall {
  fill: var(--ink); fill-opacity: .07;
  stroke: var(--line-strong); stroke-width: 1;
}
.roomplan .rp-center { fill: none; stroke: var(--line-strong); stroke-width: 1.2; stroke-dasharray: 3 5; }
.roomplan .rp-dim { fill: none; stroke: var(--line-strong); stroke-width: 1.2; }
.roomplan .rp-tri { fill: none; stroke: var(--signal); stroke-width: 2.2; }
.roomplan .rp-gear { fill: none; stroke: var(--ink); stroke-width: 2.2; }
.roomplan .rp-solid { fill: var(--ink); stroke: none; }
.roomplan .rp-soft { fill: var(--ink); stroke: none; opacity: .38; }
.roomplan .rp-label { font-family: var(--mono); font-size: 19px; letter-spacing: .04em; fill: var(--signal-ink); }

/* The rack's first-run nudge lived here for two passes -- a ruled row with a
   filled invitation button, most recently at the foot of the landing inside
   the form. It left in the second reference pass (Tj's ruling, 2026-09-20:
   the landing carries nothing about the rack). The sidebar's "Your system"
   row is the only door now; a banner that sells a side panel is furniture,
   and the row already says what the page knows. */

/* The caption over the starters. Sans, small, muted, centered over the pill
   row: the references label their suggestion rows with nothing louder than
   this. The mono caption it wore until this pass was the last letterspaced
   face on the landing; sentence case and no letterspacing is what keeps it
   a quiet legend rather than a marketing eyebrow. */
.chiplabel {
  font-size: .8rem;
  color: var(--muted);
  margin: 22px 0 0; padding: 0 4px;
  text-align: center;
}
/* The presets are nine quiet pills. Tj's call, 2026-09-20, when the two of
   us split the difference on this row: all nine stay on the landing, and
   they stop being a numbered key bank. The numerals left with the mono face
   (they were a CSS counter, so no DOM text changes and the suite still reads
   bare questions), the grid left with them, and what remains is a wrap of
   small bordered pills centered under the composer -- the shape every
   reference hangs under its own input. Muted ink, hairline, full radius;
   the hover darkens the line and sets the ink, and that is the whole of the
   treatment. */
.chips {
  display: flex; flex-wrap: wrap; justify-content: center;
  gap: 8px 8px; margin: 12px auto 10px; max-width: 50rem;
}
.chip {
  background: transparent; border: 1px solid var(--line);
  color: var(--muted);
  min-height: var(--tap); padding: 8px 15px; border-radius: 999px; font: inherit;
  font-size: 1rem; line-height: 1.4; text-align: left; cursor: pointer;
  transition: background .15s, color .15s, border-color .15s;
}
.chip:hover {
  border-color: var(--line-strong); background: var(--surface-hover);
  color: var(--ink);
}

/* The genre pickers (landing sidebar, signup form) reuse the pill. The
   pressed state is the house toggle -- the same accent fill .sizebtns uses
   for a chosen text size -- and it is scoped to .genrepick so the landing's
   starter pills, which carry a question and are never toggled, cannot pick
   the state up. aria-pressed is the one signal all three readers share: the
   CSS here, app.js in the sidebar, and the signup form's script. Left
   aligned rather than centered: the starter row is centered because it sits
   under a centered composer, while both genre groups sit under left-set
   labels in forms. */
.genrepick { justify-content: flex-start; margin: 10px 0 0; }
.genrepick .chip[aria-pressed="true"] {
  background: var(--accent); color: var(--accent-ink); border-color: var(--accent);
}
.acctform .chiplabel { text-align: left; padding: 0; margin-top: 18px; }
.chopt { color: var(--muted); font-weight: 400; }

/* The one comparison line on the landing. Tj's ruling, 2026-09-20: the
   landing carries nothing about the rack, and the ChatGPT name gets a
   single muted sentence rather than a banner. The argument lives on the
   features page; this line only hands a visitor who came here to compare
   the door to it. */
.greetteaser { margin: 12px 0 0; font-size: .95rem; color: var(--muted); }

/* First paint: the nine keys settle in as a quiet stagger. Transform only,
   no fade -- the house motion is mechanical, a key arriving on a console,
   not ethereal, a key materializing -- and the ladder is per-child so no
   script and no inline style is involved. `backwards` holds the offset
   through each delay, so the bank starts one key-depth low and lands level.
   The whole effect lives inside no-preference because it only exists in
   that world; the global reduce block is the belt to this suspenders. */
@media (prefers-reduced-motion: no-preference) {
  .chip { animation: chiparrive .45s var(--ease) backwards; }
  .chip:nth-child(2) { animation-delay: .04s; }
  .chip:nth-child(3) { animation-delay: .08s; }
  .chip:nth-child(4) { animation-delay: .12s; }
  .chip:nth-child(5) { animation-delay: .16s; }
  .chip:nth-child(6) { animation-delay: .20s; }
  .chip:nth-child(7) { animation-delay: .24s; }
  .chip:nth-child(8) { animation-delay: .28s; }
  .chip:nth-child(9) { animation-delay: .32s; }
}
@keyframes chiparrive {
  from { transform: translateY(9px); }
}

/* ── the transcript ──────────────────────────────────────────────────────
   Two speakers, two treatments, and only one of them is a container.

   Every message used to be a bordered card with a mono speaker label above it,
   so the answer to a question arrived in a box hugging its own text, narrower
   than the column it sat in, wearing the word "AudiophileGPT" (Tj, 2026-09-14,
   on the redesign). That is the shape of a support ticket. Every chat product
   worth copying does the opposite: what the visitor typed gets a container,
   and what the model wrote is set as plain text on the page at the full
   measure, because it is the thing they came to read.

   So `.bubble` is two different objects under one name, which is the price of
   the class names the Playwright suite reaches for (`#chat .msg.ai .bubble`,
   `.bubble.thinking`). On `.msg.user` it is the tinted card. On `.msg.ai` it
   is bare: no border, no ground, no padding, and the answer's first line lands
   on the same left edge as everything else in the column.

   The speaker labels are not gone, they are only no longer drawn. Both
   messages still carry one, and app.js moves it to `.visually-hidden`: with
   the cards and the labels both gone, position is the only thing telling the
   two apart, and position is not available to a screen reader reading a log.
   The distinction has to stay in the markup even as it leaves the picture. */
.chat { display: flex; flex-direction: column; padding: 26px 0 16px; }
.msg { display: flex; flex-direction: column; min-width: 0; }

/* The rhythm of a turn. An answer sits close under the question it answers,
   because it belongs to it, and the next question gets a clear band of air
   above it, because it does not. One flat gap between every pair, which is
   what this was, says all four of those distances are the same distance and
   leaves a long thread as one undifferentiated column of text. */
.msg + .msg { margin-top: 18px; }
.msg.ai + .msg.user { margin-top: 40px; }

.msg.user { align-self: flex-end; align-items: flex-end; max-width: min(88%, 34rem); }
.msg.ai { align-self: stretch; }

.bubble {
  font-size: 1.1rem; line-height: 1.7; overflow-wrap: break-word;
  min-width: 0;
}
/* The visitor's own words, in a card. The bottom-right corner is cut to the
   small radius so the bubble points back at nothing in particular, which is
   the one bit of shape every chat product uses to tie a message to its side
   of the column. */
.msg.user .bubble {
  background: var(--bubble-mine); border: 1px solid var(--line);
  border-radius: var(--radius) var(--radius) var(--radius-xs) var(--radius);
  padding: 12px 16px;
}
/* The answer, as text. No padding, and that is load-bearing: the Playwright
   suite measures the space under the closing paragraph against this padding
   and requires them to be equal, which is how it catches a margin creeping in
   where the stylesheet's rhythm should be. Zero on both sides satisfies it
   honestly rather than by accident, because there is no box here for a
   margin to be wrong about.

   `:not(.thinking)` because the wait indicator is a `.bubble` too (the suite
   waits on `.bubble.thinking` disappearing at the start of nearly every test),
   and it has to keep a box of its own to hold three dots where the answer is
   about to be. Without the exclusion this rule outranks the indicator's own
   padding, since it carries two classes to the indicator's one, and the dots
   would sit flush against the text above them. */
.msg.ai .bubble:not(.thinking) { padding: 0; background: none; border: 0; }
.bubble strong { font-weight: 700; }
.bubble code {
  font-family: var(--mono); font-size: .92em;
  background: var(--accent-soft); padding: 1px 5px; border-radius: 0;
}
/* The fenced block: test-point readings, pinouts, resistor ladders. The ground
   is the one the table headers wear and the face is the inline chip's, so a
   value reads the same in prose and in the block it came from. The lines are
   the model's: pre keeps rewrap out of a grid the visitor may be reading
   against the meter, and on a phone the block scrolls sideways rather than
   flowing a measurement onto a second line. */
.bubble pre {
  margin: 0 0 0.68em; padding: 10px 14px;
  background: var(--canvas-deep); border: 1px solid var(--line);
  border-radius: var(--radius-sm); overflow-x: auto;
}
.bubble pre code {
  background: none; padding: 0; border-radius: 0;
  white-space: pre; line-height: 1.55;
}
/* The quoted manual line, in the house emphasis shape (the 3px left rule the
   offtopic notice wears) but in line rather than accent: this is the manual
   speaking, quieter than the answer around it, not an alarm. */
.bubble blockquote {
  margin: 0 0 0.68em; padding: 1px 0 1px 15px;
  border-left: 3px solid var(--line-strong); color: var(--muted);
}
/* The rule the model drew between its sections. A hairline at the table
   border's weight, and spaced like a heading rather than a paragraph: it
   divides, and a divide that hugs the text above it reads as a stray dash. */
.bubble hr {
  border: 0; border-top: 1px solid var(--line-strong);
  margin: 1.36em 0 0.68em;
}
/* The struck part stays full ink, not washed grey: the visitor is comparing it
   against the replacement named beside it, and a quieter value is a harder
   comparison. */
.bubble del { text-decoration: line-through; }
/* Emphasis is a 3px rule on the left edge, which is the house pattern
   (repair-design.css `.wizard-banner`). It was a 5px neon border before, and
   then a border on the bubble, and that stopped working the moment the answer
   stopped being a box: a left border on something with no padding puts the
   text against the rule. So the rule moves out to the message and takes its
   own clearance with it, which holds the answer's left edge level with every
   other answer's and hangs the mark in the margin beside it. */
.msg.offtopic { border-left: 3px solid var(--signal); padding-left: 15px; }
/* What the page says when a question does not come back. Its own line under
   the bubble and not a colour on the bubble itself: the bubble is what the
   model wrote, and a failure painted into it reads as a reply from the model,
   which is how "HTTP 500" came to look like an answer to the question above
   it. The rule wears the house emphasis pattern like `.offtopic` does, in the
   danger colour, so it is recognisable at a glance as the same family of
   notice and not as text that happens to be red. The ground is the canvas,
   not a bubble (`.msg.ai .bubble` paints nothing), so the ratio is measurable
   rather than assumed: the 6% tint takes #f5f6f7 to a painted #efe9eb, and
   --danger reads 7.59:1 on it, which clears AAA for body text. The comment
   here used to claim 6.8:1 against the retired lavender canvas; recomputed
   for both, that ground read 7.33:1, so the old number was never right. */
.msg .notice {
  margin: 10px 0 0; padding: 9px 13px;
  border-left: 3px solid var(--danger); border-radius: var(--radius-xs);
  background: var(--danger-wash);
  color: var(--danger);
  font-size: 1rem; line-height: 1.6; max-width: 40rem;
}
/* The one notice that is not bad news: the visitor pressed Stop. Same shape,
   same place, so it still reads as the page talking rather than as part of the
   answer, in the accent instead of the alarm. --ink on the 8% accent tint is
   the same AAA reading the body text gets. */
.msg .notice.quiet {
  border-left-color: var(--signal);
  background: var(--accent-soft);
  color: var(--ink);
}
/* The two-step confirms, in the notice idiom on purpose. The three actions
   that used to open a browser confirm() box (delete a conversation, ask that
   question again, report that the fix did not work) now open one of these
   instead: the same left-stripe block the page already uses to say "this is
   the page talking", holding the question in plain text and two real buttons.
   The browser box was the one native dialog left on the page, it could not be
   styled, it ignored the text-size control, and it froze the whole tab until
   answered.

   Two tints, matching the notices: danger for the one that destroys work
   (delete), quiet accent for the two that spend a question (ask again, did
   not fix). The tint sets the --confirm-ink/--confirm-fill pair the buttons
   read, so a fourth confirm is two custom properties and a class. The buttons
   are real boxes on their block's tint, bordered like every actionable thing
   in the file, at the 48px floor.

   The selector is not scoped to .msg on purpose: delete's confirm mounts as
   the first child of the chat column itself, because the thing it is about is
   the whole conversation and not any one turn in it. The chat column carries
   no horizontal padding of its own, so the block lands on the same left edge
   either way. */
.confirm {
  margin: 10px 0 0; padding: 9px 13px;
  border-left: 3px solid var(--danger);
  border-radius: var(--radius-xs);
  background: var(--danger-wash);
  color: var(--danger);
  font-size: 1rem; line-height: 1.6; max-width: 40rem;
  --confirm-ink: var(--danger);
  --confirm-fill: var(--danger);
}
.confirm.quiet {
  border-left-color: var(--signal);
  background: var(--accent-soft);
  color: var(--ink);
  --confirm-ink: var(--link);
  --confirm-fill: var(--link);
}
.confirm .confirmrow { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 9px; }
.confirm .confirmrow button {
  min-height: var(--tap); padding: 4px 12px; cursor: pointer; font: inherit;
  font-size: .9rem; font-weight: 600; border-radius: var(--radius-xs);
  background: var(--surface); border: 1px solid var(--line-strong);
  color: var(--muted);
}
.confirm .confirmrow button:hover { color: var(--ink); background: var(--surface-hover); }
.confirm .confirm-yes { color: var(--confirm-ink); border-color: var(--confirm-fill); }
.confirm .confirm-yes:hover {
  background: var(--confirm-fill); color: #fff; text-decoration: none;
}
/* The visitor's bubble only, and the reason this is not one rule over
   `.bubble`. What they typed into the textarea is theirs, including the line
   breaks, and there is no markup in it to format. The answer is built into
   blocks by formatAnswer and every gap in it is one of the margins below, so
   a white-space rule reaching it would put that rhythm back in the browser's
   hands: each newline the model wrapped its own sentence at becomes a hard
   break, which is what a long answer looked like before. */
.msg.user .bubble { white-space: pre-wrap; }

/* The question's affordances. One word on one quiet row under the visitor's
   own question -- the same posture the answer's actions take under the
   answer, and the same reason it is a word and not a pencil glyph: every
   icon-only row this product has ever shipped got relabelled. */
.msgtools { display: flex; justify-content: flex-end; margin-top: 2px; }
.msgtools .tool {
  min-height: var(--tap); padding: 4px 10px; cursor: pointer; font: inherit;
  font-size: .9rem; font-weight: 600; border-radius: var(--radius-xs);
  background: transparent; color: var(--muted); border: 1px solid transparent;
}
.msgtools .tool:hover { color: var(--link); background: var(--accent-soft); }

/* The editor replaces the bubble in place, so the words being rewritten sit
   where the question was rather than down at the composer, and the answer
   they replace visibly leaves with them. */
.msgeditor { width: 100%; margin-top: 2px; display: grid; gap: 6px; }
.msgeditor textarea {
  width: 100%; min-height: 3.2em; resize: vertical;
  font: inherit; font-size: max(1.05rem, 16px); line-height: 1.5;
  color: var(--ink); background: var(--surface);
  border: 1px solid var(--line-strong); border-radius: var(--radius-sm);
  padding: 9px 12px;
}
.msgeditor textarea:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 1px;
}
.msgeditorbtns { display: flex; justify-content: flex-end; gap: 8px; }
button.msgsave, button.msgcancel {
  min-height: var(--tap); padding: 6px 14px; cursor: pointer;
  font: inherit; font-size: .95rem; font-weight: 600;
  border-radius: var(--radius-sm); flex: 0 0 auto;
}
button.msgsave {
  background: var(--accent); border: 1px solid var(--accent); color: #ffffff;
}
button.msgsave:hover {
  background: color-mix(in srgb, var(--accent) 86%, #000);
  border-color: color-mix(in srgb, var(--accent) 86%, #000);
}
button.msgcancel {
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line-strong);
}
button.msgcancel:hover { border-color: var(--danger); color: var(--danger); }

/* The answer's rhythm, all of it. One gap for every adjacency: a paragraph
   after a paragraph, a paragraph after a list, a list after a paragraph. The
   step is deliberately under half a line box, because a gap wide enough to be
   a paragraph of its own is how a long answer turns into a column of islands.

   The unit is em and not px because of the text size control, which raises the
   root font size and the bubble's 1.1rem with it. At the large step the line
   box is 38.9px, so a fixed 12px gap would sit at a third of a line instead of
   the 40% it is here: the answer would read tighter for the reader who turned
   the size up in the first place. 0.68em is about 14px against the default
   20.24px bubble, because the default step itself moved up to 115% with the
   old-friendly pass of 2026-09-20: the gap rides the bigger text rather than
   being left behind by it.

   The last block gives its margin back so the space under the answer is the
   bubble's own padding, even at the bottom of one; while the reply streams,
   that margin returns as soon as the next paragraph arrives, which the
   browser is already scrolling for. */
.bubble p { margin: 0 0 0.68em; }
.bubble ul, .bubble ol { margin: 0 0 0.68em; padding-left: 24px; }
.bubble > :last-child { margin-bottom: 0; }
/* Between items, not above the first one. The first item's gap is the block's
   own margin, so this would make the top item sit closer to the paragraph
   above it than every other item sits to its neighbour. Em for the reason
   above, and 0.34em is 6px at the default size. */
.bubble li { padding-left: 2px; }
.bubble li + li { margin-top: 0.34em; }
.bubble li::marker { color: var(--muted); }

/* Links inside an answer. The model writes URLs and now and then a labelled
   markdown link; both arrive here as real anchors with rel="noopener". An
   underline and the link token, the same pair the sidebar's fine print
   uses: colour alone is not a link marker at this size for this audience. */
.bubble a { color: var(--link); text-decoration: underline; text-underline-offset: 2px; }
.bubble a:hover { color: var(--link-hover); }

/* A comparison table the model writes out. display:block with a horizontal
   scroll of its own is the responsive-table trade: a spec sheet six columns
   wide either overflows the reading column on a phone or it scrolls, and
   scrolling keeps every cell readable, which squeezing does not. Hairline
   borders rather than zebra striping -- the stripe is a dashboard tell, and
   the hairline already says where a cell ends. */
.bubble table {
  display: block; width: max-content; max-width: 100%;
  overflow-x: auto; border-collapse: collapse;
  margin: 0 0 0.68em; font-size: .95em;
}
.bubble th, .bubble td {
  border: 1px solid var(--line-strong);
  padding: 7px 11px; text-align: left; vertical-align: top;
}
.bubble th { background: var(--canvas-deep); font-weight: 700; color: var(--ink); }
.bubble td { color: var(--ink); }

/* What can be done with a finished answer. One control so far, and it is
   text rather than an icon: an icon-only button is banned here, and the two
   glyph candidates ("⧉", "📋") are either an encoding gamble or an emoji.

   It is quiet on purpose. Under every answer in a long thread, a bordered
   button is a second thing competing with the answer for attention, so this
   one has no border until it is pointed at and sits at 48px tall invisibly,
   which keeps the target size without drawing the rectangle. It appears only
   on `.msg.ai` (app.js appends it there) and never under a question, where
   there is nothing to copy that the visitor did not just type. */
.acts { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
.acts button {
  min-height: var(--tap); padding: 4px 10px; cursor: pointer; font: inherit;
  font-size: .9rem; font-weight: 600; border-radius: var(--radius-xs);
  background: transparent; color: var(--muted); border: 1px solid transparent;
}
.acts button:hover { color: var(--link); background: var(--accent-soft); }
/* "Ask again" goes cold when the visitor is out of questions. It is disabled
   rather than hidden, because a control that vanishes is a control the visitor
   thinks they imagined, and the counter line above already says why. The hover
   rule is named so the cold button does not light up under the pointer. */
.acts button:disabled { cursor: default; opacity: .45; }
.acts button:disabled:hover { color: var(--muted); background: transparent; }

/* The two buttons the passes added to the footer, sharing the quiet .acts
   shape above. "Email me this" is hidden, not dimmed, until there is an
   account to send to: a disabled button raises the question "why?" and the
   answer ("sign in") is already a sidebar press away, so the button simply
   waits for the sign-in to happen and renderAccount un-hides it.
   `.confirmfix` gets the same hover as its neighbours; its after-state is
   the receipt rule below. */

/* "Saved to memory" is a receipt, not a cooled button. The blanket disabled
   rule above dims to .45 because a disabled action should look inert; this
   one is not an action any more, it is the answer to one, so it reads as
   state at full strength: tinted, bordered, and no longer a pressable
   offer. The hover rules are re-stated because the blanket ones above are
   named earlier and would otherwise dim the receipt on a pointer. */
.acts .confirmfix:disabled {
  opacity: 1; cursor: default;
  color: var(--muted);
  background: var(--accent-soft);
  border-color: var(--line);
}
.acts .confirmfix:disabled:hover {
  color: var(--muted); background: var(--accent-soft);
}

/* The answer's own follow-up questions, as pills under the footer. The
   starter pills under the composer invite a first question; these grow out
   of an answer and carry the next one, so they share the pill language at a
   size an older hand can land on: full --tap height, body-size text. They
   wrap rather than scroll, because a row that runs off the edge of a phone
   hides its own last choice. */
.followups {
  display: flex; flex-wrap: wrap; gap: 8px;
  margin-top: 10px;
}
.fchip {
  min-height: var(--tap); padding: 8px 16px; cursor: pointer; font: inherit;
  font-size: 1rem; line-height: 1.4; text-align: left;
  background: transparent; color: var(--ink);
  border: 1px solid var(--line); border-radius: 999px;
  transition: background .15s, color .15s, border-color .15s;
}
.fchip:hover {
  border-color: var(--line-strong); background: var(--surface-hover);
}

/* The wait state. The bubble holds its own height while it is empty: the
   "Reading your question." words live in a line beside it (.statusline
   below), and a bubble that collapsed to nothing would let the whole column
   under the question jump up and then back down when the first words land.
   36px is the box the old three-dot indicator held open, so nothing below
   the question moves at either end of the wait.

   It stays a rendered box on purpose. The suite waits on `.bubble.thinking`
   as a visible element at the start of nearly every test, and
   `display: none` would hang every one of them. */
.msg.ai .bubble.thinking { min-height: 36px; padding: 14px 0; }

/* The wait sweep (ui-motion-kit lift, 2026-09-17): a sheen crossing the empty
   bubble, so the wait reads as work happening and not as a stalled page. The
   pseudo moves by transform alone and carries no words, so the bubble Copy
   takes stays empty and the box the suite waits on is untouched: same
   element, same size, still visible. */
.msg.ai .bubble.thinking { position: relative; overflow: hidden; }
.msg.ai .bubble.thinking::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(100deg, transparent 30%, var(--accent-soft) 50%, transparent 70%);
  animation: waitsweep 1.6s ease-in-out infinite;
}
@keyframes waitsweep {
  from { transform: translateX(-72%); }
  to { transform: translateX(72%); }
}
/* The global reduced-motion freeze further down would leave this infinite
   animation cycling at .01ms, which reads as flicker, so it is turned off the
   way the streaming caret above is: RM readers wait on the status line's
   words alone, exactly as they did before the sweep existed. */
@media (prefers-reduced-motion: reduce) {
  .msg.ai .bubble.thinking::after { animation: none; opacity: 0; }
}

/* The staged wait line, a sibling under the empty bubble and coming down the
   moment the first words arrive. It is page voice, not answer text, which is
   the other reason it is not in the bubble: the bubble's text is what Copy
   takes, and a progress note in it would be copied along with the answer. */
.statusline { margin: 0; font-size: .95rem; color: var(--muted); }

/* The island (ui-motion-kit lift, 2026-09-17): the pill at the top of the
   app that mirrors what #srStatus just announced, for sighted readers. It
   carries no words of its own in the markup and stays [hidden] except for
   the moment after a Copy; app.js shows it and takes it down. It is
   aria-hidden because srStatus already speaks the line: two live regions
   would say everything twice. */
.island {
  position: fixed; top: 12px; left: 50%;
  translate: -50% 0;
  z-index: 25;
  padding: 8px 18px;
  border-radius: 999px;
  background: var(--accent);
  color: var(--accent-ink);
  font-size: .95rem; font-weight: 600;
  box-shadow: var(--drawer-shadow);
  animation: island-in .28s cubic-bezier(0.22, 1, 0.36, 1);
}
.island.is-out { animation: island-out .18s forwards; }
@keyframes island-in {
  from { opacity: 0; translate: -50% -16px; }
}
@keyframes island-out {
  to { opacity: 0; translate: -50% -10px; }
}
/* Keyframes rather than transitions because the island is toggled through
   display:none (the [hidden] attribute); a display:none element runs no
   transition at all, where animations restart from the keyframe when the
   element becomes rendered again. */
@media (prefers-reduced-motion: reduce) {
  .island, .island.is-out { animation: none; }
}

/* The signal strip. This is the line that says what an answer was drawn from,
   and it is the most load-bearing small element in the app: under an answer it
   is the one thing on the page that a model talking to itself cannot produce.
   It was set as ordinary body text at caption weight, so it read as a footnote
   and not as a record.

   It is an instrument readout now: mono and small, held off the answer by a
   signal tick in the margin. That tick is the same margin mark the bench log
   puts against the entry you are reading and the same signal the preset bank
   spends on its numerals, so signal means one thing here. It is the colour of
   a real measurement, and this line is the realest thing under an answer.

   The words are untouched. The strip is a treatment and not a rewrite: the
   string is built in one place in app.js, the behavior suite asserts it
   character for character, and the styling is therefore not allowed to add,
   drop or reorder a word of it. Anything this line ends up saying about a
   model or a corpus has to be carried by the string itself, in that one
   place, and not conjured here. */
.sources {
  position: relative;
  margin: 12px 0 0; padding-left: 15px;
  font-size: .82rem; line-height: 1.5;
  color: var(--muted);
  max-width: 40rem;
}
.sources::before {
  content: "";
  position: absolute; left: 0; top: 2px; bottom: 2px;
  width: 3px; background: var(--signal);
}

/* The speaker line over an answer. The question does not carry its name on
   screen, because the right alignment already says it; but an answer with no
   name over it reads as anonymous text, which is exactly the templated look
   this pass is here to retire. Small, muted, a label and not a signature.
   Scoped to the AI message so the rule cannot reach the question's own
   speaker line, which stays screen-reader-only. */
.msg.ai .who {
  font-size: .85rem; font-weight: 600; color: var(--muted);
  margin: 0 0 6px;
}

/* The caret that marks an answer still arriving. Drawn off the last block
   rather than appended into it, so it costs the render no node at all and
   cannot be left sitting in a bubble whose stream has ended: it exists only
   while `.streaming` does, and that class comes off at the end of the answer
   on every path, including the one where the visitor pressed Stop.

   Generated content is not read out, and the element carries nothing anyway,
   so a screen reader hears the answer and not the mark on it. `steps(1, end)`
   because a cursor blinks rather than fades; `prefers-reduced-motion` holds it
   solid, which is the same compromise the dots above make. */
.bubble.streaming > :last-child::after,
.bubble.streaming > ul:last-child > li:last-child::after,
.bubble.streaming > ol:last-child > li:last-child::after {
  content: "";
  display: inline-block;
  width: .46em; height: 1.02em;
  margin-left: .14em; vertical-align: -.16em;
  background: var(--signal);
  animation: caret 1.1s steps(1, end) infinite;
}
/* A list that is still being written puts the mark at the end of the item it
   is writing, not on a line of its own under the list. The two rules above
   both match an open list, so this one takes the list's own copy back off it
   and leaves the item's. Same specificity, later, so it wins. */
.bubble.streaming > ul:last-child::after,
.bubble.streaming > ol:last-child::after { content: none; }
@keyframes caret { 0%, 50% { opacity: 1; } 50.01%, 100% { opacity: 0; } }
/* All three selectors, and the list-item ones are the reason this rule is not
   written once. Generated content is not reached by the blanket
   `animation-duration` above, which matches elements and not their
   pseudo-elements, and animation-duration is not inherited. So a reader who
   asked for less motion got a still caret on a paragraph and a blinking one on
   the last step of a list. */
@media (prefers-reduced-motion: reduce) {
  .bubble.streaming > :last-child::after,
  .bubble.streaming > ul:last-child > li:last-child::after,
  .bubble.streaming > ol:last-child > li:last-child::after {
    animation: none; opacity: 1;
  }
}

.threadbar {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 4px 0 12px; border-bottom: 1px solid var(--line); margin-bottom: 6px;
}
.threadbar h2 {
  font-family: var(--display);
  font-size: 1.12rem; font-weight: 600; flex: 1; min-width: 12rem;
  letter-spacing: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
/* The rename field that swaps in over the title. Same type, same box on the
   row, so the swap reads as the title becoming editable and not as a dialog
   arriving. It takes the flex: 1 the title gave up. */
.threadbar .renameinput {
  flex: 1; min-width: 12rem;
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--accent); border-radius: var(--radius-xs);
  font: inherit; font-size: 1.05rem; font-weight: 600;
  padding: 6px 10px;
}
.threadbar .renameinput:focus { box-shadow: 0 0 0 3px var(--accent-soft); }
/* The three thread tools beside Delete. Quieter than the destructive
   control beside them on purpose -- they are conveniences, and the bar's
   job is to name the thread first -- so they are text buttons at the
   footer's size, borderless at rest, with the 48px floor kept. */
.threadtools { display: flex; gap: 2px; flex: 0 0 auto; }
.threadtools .tool {
  min-height: var(--tap); padding: 6px 10px; cursor: pointer; font: inherit;
  font-size: .9rem; font-weight: 600; border-radius: var(--radius-xs);
  background: transparent; color: var(--muted); border: 1px solid transparent;
}
.threadtools .tool:hover { color: var(--link); background: var(--accent-soft); }
/* The one destructive control on the page, and deliberately the quietest thing
   in the bar it sits in. It was a bordered box in the danger colour, on the
   same row as the conversation's own title and shouting over it, with the
   words "Delete this conversation" spelled out on a wide button that dominated
   the top of every thread (Tj, 2026-09-14, on the redesign).

   What it says now is "Delete", which is unambiguous on a row whose only other
   content is the title of the thing being deleted, and the full sentence is
   still the button's accessible name so nothing is lost to a screen reader.
   The colour arrives on hover and on keyboard focus, which is where a warning
   is useful; at rest the confirmation dialog is what protects the visitor, and
   it names the conversation and counts the questions. */
.danger {
  min-height: var(--tap); font: inherit; font-size: .95rem; cursor: pointer;
  flex: 0 0 auto;
  background: transparent; color: var(--muted); font-weight: 600;
  border: 1px solid transparent; border-radius: var(--radius-xs);
  padding: 10px 12px;
}
.danger:hover, .danger:focus-visible {
  color: var(--danger);
  background: var(--danger-wash);
  border-color: var(--danger-line);
}

/* The count line, in the spec-sheet register (2026-10-01): the shell's
   numerals are instrument readouts, and this pill is the one live figure on
   every page view. Mono face, same muted ink -- the measured contrast pairs
   are about color, not face, so the 7:1 floors carry over unchanged. */
.quota {
  font-family: var(--mono);
  font-size: .8rem; color: var(--muted); padding: 8px 3px 0;
  margin-top: 8px;
}
.quota.low { color: var(--warn); font-weight: 600; }
.quota b { color: var(--ink); }

.plans {
  border: 1px solid var(--line); background: var(--surface);
  border-radius: var(--radius); padding: 24px 26px; margin: 12px 0 20px;
}
.plans h2 {
  font-family: var(--display);
  font-size: 1.45rem; font-weight: 600; letter-spacing: -.005em;
}
.plans p { font-size: 1.05rem; line-height: 1.7; color: var(--muted); margin-top: 12px; }
.plans p b { color: var(--ink); font-weight: 700; }
.plans a.cta {
  display: inline-block; margin-top: 18px; background: var(--accent);
  color: var(--accent-ink); padding: 14px 22px; border-radius: var(--radius-sm);
  font-size: 1.05rem; font-weight: 600; text-decoration: none;
  min-height: var(--tap);
}
.plans a.cta:hover { background: var(--accent-dark); color: var(--accent-ink); }
.plannote { margin-top: 14px; font-size: .9rem; color: var(--muted); }
/* The `a.cta` rule above carries `margin-top: 18px`, which is right on the
   post-checkout page where the link follows a paragraph. Inside a wall row it
   is a flex item against `align-items: center`, so the margin split the
   difference and sat the button 9px below the row's centre line: the "Paid"
   text block measured mid 534, the button mid 543. A button that is visibly
   low in its own row is the kind of thing nobody names and everybody sees.
   The row already spaces its own children.

   Three classes deep on purpose: the rule it overrides is `.plans a.cta` at
   (0,2,1), so `.planrow .cta` at (0,2,0) loses and the 9px came straight back
   when this was first written that way. */
.plans .planrow .cta { margin-top: 0; }

/* Wall rows: the two ways forward, stacked, separated by hairlines. */
.planrows { margin-top: 18px; }
/* The interval switch (2026-10-16): two choices, one pressed. Quiet pills on
   the row above the plans, because the decision is the plan and the period is
   a modifier on it. */
.intervalpick {
  display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
  margin: 14px 0 4px;
}
.ipick {
  min-height: var(--tap); cursor: pointer; font: inherit; font-size: .95rem;
  font-weight: 600; color: var(--muted);
  background: var(--surface); border: 1px solid var(--line-strong);
  border-radius: 999px; padding: 9px 16px;
}
.ipick:hover { color: var(--ink); border-color: var(--accent); }
.ipick.on {
  color: var(--accent-ink); background: var(--accent);
  border-color: var(--accent);
}
.ipicknote { font-weight: 500; font-size: .82rem; opacity: .85; }
.planprice { color: var(--ink); font-weight: 600; white-space: nowrap; }
.planrow {
  display: flex; gap: 16px; align-items: center; justify-content: space-between;
  flex-wrap: wrap; padding: 16px 0;
  border-top: 1px solid var(--line);
}
.planrow:last-child { border-bottom: 1px solid var(--line); }
.planwhat { flex: 1; min-width: 14rem; display: flex; flex-direction: column; gap: 4px; }
.planwhat b { color: var(--ink); font-size: 1.05rem; font-weight: 700; }
.planwhat span { color: var(--muted); font-size: .95rem; line-height: 1.6; }
.ctabtn {
  min-height: var(--tap); cursor: pointer; font: inherit; font-weight: 600;
  background: var(--accent); color: var(--accent-ink);
  border: 1px solid var(--accent); border-radius: var(--radius-sm);
  padding: 12px 18px; font-size: 1rem;
}
.ctabtn:hover { background: var(--accent-dark); border-color: var(--accent-dark); }
.ctabtn[disabled] { opacity: .55; cursor: wait; }

/* The wall's two ways forward, weighted. Tj's call 2026-09-14: the paid row is
   the primary and the free account is the secondary.

   Both slots were `--accent` fill, so the wall showed two buttons identical at
   rgb(96, 54, 184) with white labels. Two equal-weight buttons in a row is the
   template tell: nothing tells the visitor which one is the next step, and the
   pair reads as dropped in rather than chosen.

   The secondary reuses the `.acctportal` idiom instead of inventing a second
   outline button for the file to keep in sync: accent-soft ground, accent
   hairline, `--link` for the label. `--link` and not `--accent` because
   `--accent` is a fill only here and measures 7.73:1 on white, which clears
   the 3:1 non-text floor for a border and misses the 7:1 text one.

   Measured: label #4a2791 on the accent-soft tint over white 9.29:1, label
   #4a2791 on the border 7.73:1, both above the 7:1 floor. Hover fills solid,
   where white on `--accent-dark` is 9.97:1. */
.ctabtn.secondary {
  background: var(--accent-soft); color: var(--link);
  border-color: var(--accent);
}
.ctabtn.secondary:hover {
  background: var(--accent-dark); color: var(--accent-ink);
  border-color: var(--accent-dark);
}

/* ── the account block (sidebar) ─────────────────────────────────────── */
/* The account block is one row at the foot of the sidebar, and pressing it
   is what opens the panel beneath -- the disclosure every reference places
   at the foot of this column (Tj, 2026-09-20). It was a card, and before
   that a ruled band; both put a permanent box around the least interesting
   thing in the column. Flat now: transparent ground, no border of its own,
   the same rounded hover the conversation rows wear, and the chevron --
   drawn from two borders of an empty box, so no screen reader meets a
   glyph -- marking that the row expands.

   margin-top:auto is what anchors it: the sidebar is a flex column, and
   this one auto margin drops the account row and the one-line foot under it
   to the bottom of the column at every height of the list. The forms that
   swap inside the block keep their shapes: a form is an instrument, and
   only the furniture around it changed. */
.acct {
  margin-top: auto;
}
.acctopen {
  min-height: var(--tap); width: 100%; cursor: pointer; font: inherit;
  font-weight: 600; font-size: .98rem; text-align: left;
  background: transparent; color: var(--ink);
  border: 0; border-radius: var(--radius-xs);
  padding: 11px 13px 11px 16px;
  display: flex; align-items: center; gap: 8px;
}
.acctopen::after {
  content: "";
  width: 7px; height: 7px; flex: 0 0 auto;
  margin-left: auto;
  border-right: 1.5px solid var(--muted);
  border-bottom: 1.5px solid var(--muted);
  transform: rotate(45deg) translateY(-2px);
  transition: border-color .15s;
}
.acctopen:hover { background: var(--surface-hover); color: var(--link); }
.acctopen:hover::after { border-color: var(--link); }
.acctform { display: flex; flex-direction: column; gap: 8px; padding: 10px 13px; }
.acctform label {
  font-size: .92rem; font-weight: 600; color: var(--ink); padding-left: 2px;
}
.acctform input {
  background: var(--surface); border: 1px solid var(--line-strong); color: var(--ink);
  border-radius: var(--radius-sm); padding: 12px 14px;
  font: inherit; font-size: max(1rem, 16px); outline: none; width: 100%;
  min-height: var(--tap);
}
.acctform input:focus { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); }
.accterr { color: var(--danger); font-size: .92rem; line-height: 1.5; }
.acctbtns { display: flex; gap: 8px; }
.acctbtns button {
  flex: 1; min-height: var(--tap); cursor: pointer; font: inherit;
  font-weight: 600; font-size: .95rem; border-radius: var(--radius-sm);
  padding: 10px 12px;
}
.acctbtns #acctIn { background: var(--accent); color: var(--accent-ink); border: 1px solid var(--accent); }
.acctbtns #acctIn:hover { background: var(--accent-dark); border-color: var(--accent-dark); }
.acctbtns #acctUp { background: transparent; color: var(--link); border: 1px solid var(--line-strong); }
.acctbtns #acctUp:hover { border-color: var(--accent); background: var(--accent-soft); }
.acctbtns button[disabled] { opacity: .55; cursor: wait; }
.acctnote { font-size: .88rem; color: var(--muted); line-height: 1.55; }
.acctwho { display: flex; flex-direction: column; gap: 2px; padding: 10px 13px; }
.acctmail { font-weight: 600; font-size: .95rem; overflow-wrap: anywhere; }
.accttier { font-size: .8rem; color: var(--muted); }
/* The one upgrade sentence (index.html #acctPro). Sized to read as part of
   the account block, not as a banner: the wall does the selling, this line
   only has to be findable. */
.acctup {
  font-size: .85rem; line-height: 1.45; color: var(--link);
  text-decoration: underline; text-underline-offset: 2px;
}
.acctup:hover { color: var(--accent); }
.acctout {
  margin-top: 8px; min-height: var(--tap); cursor: pointer; font: inherit;
  font-size: .9rem; font-weight: 600;
  background: transparent; color: var(--muted);
  border: 1px solid var(--line); border-radius: var(--radius-sm);
  padding: 9px 14px;
}
.acctout:hover { color: var(--ink); border-color: var(--line-strong); }
/* Same shape as Sign out, in outline accent. It is the only button in this
   block that leads somewhere rather than ending something, so it reads as the
   primary action of the two without being a second filled button. */
.acctportal {
  margin-top: 10px; min-height: var(--tap); cursor: pointer; font: inherit;
  font-size: .9rem; font-weight: 600;
  background: var(--accent-soft); color: var(--link);
  border: 1px solid var(--accent); border-radius: var(--radius-sm);
  padding: 9px 14px;
}
.acctportal:hover { background: var(--accent); color: var(--accent-ink); }
.acctportal[disabled] { opacity: .55; cursor: wait; }

/* A quiet text-only control inside the account block (Forgot password?,
   Back to sign in). It reads as navigation rather than action, which is the
   point: it moves between forms of the same block instead of committing
   anything. Still a full tap target, per the house 48px floor. */
.linkbtn {
  align-self: flex-start; min-height: var(--tap); cursor: pointer;
  font: inherit; font-size: .9rem; font-weight: 600;
  background: none; color: var(--link);
  border: 0; padding: 10px 2px;
}
.linkbtn:hover { color: var(--accent-dark); text-decoration: underline; }

/* The one button of the signed-in block that leads outside the page: it
   offers the browser's own install chooser. A ruled row like the rest of
   the band it sits in, with the band's hover; the accent-soft fill it wore
   was panel furniture in a column this sidebar no longer speaks. */
.installbtn {
  min-height: var(--tap); width: 100%; cursor: pointer;
  font: inherit; font-size: .9rem; font-weight: 600;
  background: transparent; color: var(--link);
  border: 0; border-radius: var(--radius-xs);
  padding: 11px 13px 11px 16px; text-align: left;
}
.installbtn:hover { background: var(--surface-hover); }

/* ── the account card (top of main) ──────────────────────────────────── */
/* The visible voice of the account moments: what a new account gets, the
   outcome of a confirm link, the outcome of a reset. Sits above the welcome
   copy so it is the first thing read; the syscard shape is the house surface
   for "the service itself is speaking", and one accent edge is what marks it
   as addressed to this visitor rather than as page furniture. */
.syscard {
  max-width: var(--col); margin: 0 auto 22px;
  background: var(--surface);
  border: 1px solid var(--line-strong); border-left: 3px solid var(--signal);
  border-radius: var(--radius-sm);
  padding: 18px 20px 16px;
  display: flex; flex-direction: column; gap: 8px;
}
.syscard h2 { margin: 0; font-size: 1.15rem; }
.syscard p { margin: 0; color: var(--ink); font-size: .95rem; line-height: 1.6; }
.syscard button {
  align-self: flex-start; margin-top: 4px; min-height: var(--tap);
  cursor: pointer; font: inherit; font-weight: 600; font-size: .95rem;
  background: var(--accent); color: var(--accent-ink);
  border: 1px solid var(--accent); border-radius: var(--radius-sm);
  padding: 9px 18px;
}
.syscard button:hover { background: var(--accent-dark); border-color: var(--accent-dark); }
.syscard[hidden] { display: none; }

/* Full width, unlike `main`. The composer is sticky, so it has to paint across
   the whole column: capped at the column width in a wider window, answers
   would scroll past in the uncovered strip beside it. The asymmetric padding
   is what re-centres the contents on the same `--col` box `main` uses, so the
   ask box lines up with the answers above it.

   The hairline came back 2026-09-16, after the review that caught the drawer
   and the confirms looked at this corner too. The ask box does carry its own
   border and the ground here is opaque, but the ask box is inset from the
   composer's edges: an answer scrolling up behind the sticky bar shows in the
   bare canvas strip above it with nothing marking where the page ends, and
   the review was right that this reads as the answer colliding with the
   controls. One hairline across the top is the fix, the same --line every
   other plane boundary in the file uses. */
/* The composer band. It carries the opaque ground and the top hairline the
   2026-09-16 review asked back to keep: the answer scrolling up behind the
   sticky bar needs a boundary, or the thread visibly collides with the
   controls. What changed 2026-09-20 is what sits on the band. The Listening
   Room shell drew the field directly on the band under a single rule -- a
   "console front panel" conceit, Ask bolted to the end of the rule -- and the
   ruled field read as an unfinished form field, which is the exact complaint
   ("very vibecoded and basic") that opened this redesign. The band now holds
   one surfaced pill: the field inside it, the round send key inside it at the
   right end. */
.composer {
  position: sticky; bottom: 0; z-index: 4;
  width: 100%;
  background: var(--canvas-deep);
  border-top: 1px solid var(--line);
  padding: 14px max(22px, calc((100% - var(--col)) / 2 + 22px)) 18px;
}

/* The landing strips the band. On the welcome screen the composer is the
   hero object, not a toolbar: in every reference (Tj, 2026-09-20) it sits
   centred in the page on the same ground as everything around it, directly
   under the greeting. So while #welcome is on screen the composer
   un-sticks, loses its ground and its top rule, and the ask box stands on
   the canvas with the starters and the fine print under it -- one centred
   composition.

   The centring's lower half is this rule's `margin-bottom: auto`; the
   upper half is the main rule beside it. The pair used to be described as
   "one auto above the greeting, one below this form", but the upper auto
   sat on .welcome, INSIDE main, and main is `flex: 1`: it consumed every
   pixel of .col's free space before any margin saw any, the composer's
   auto resolved to 0, and the whole group sank to the foot of the window
   under a pile of dead space. The fix puts both autos where the free
   space actually is, on .col's own items -- main stops growing and takes
   `margin-top: auto`, the composer keeps `margin-bottom: auto`, and the
   two split the column evenly so the greeting-plus-form group centres as
   one unit. Auto margins on a flex item only see the space the grow
   factors left behind; that order is the whole trap.

   A thread takes the band back: the boundary under a scrolling answer is
   load-bearing, and the note on the rule above says what happens without
   it. CSS only, no state for app.js to carry; a browser without :has()
   (baseline 2023) keeps the sticky band on the landing too, which is the
   shape this shell wore for years. */
body:has(#welcome:not([hidden])) main {
  flex: 0 0 auto;
  margin-top: auto;
}
body:has(#welcome:not([hidden])) .composer {
  position: static;
  background: transparent;
  border-top: 0;
  padding-bottom: 22px;
  margin-bottom: auto;
}

/* Sits on the composer's top edge rather than at a height of its own, so it
   needs no number that goes stale when the ask box grows. `bottom: 100%` on an
   absolutely positioned child of a sticky parent is the composer's top edge.
   Centred, because both a left and a right corner already hold something. */
.jump {
  position: absolute; bottom: 100%; left: 50%; transform: translateX(-50%);
  margin-bottom: 10px;
  font: inherit; font-size: .9rem; font-weight: 600;
  color: var(--link); background: var(--surface);
  border: 1px solid var(--accent); border-radius: var(--radius-xs);
  min-height: var(--tap); padding: 0 18px;
  cursor: pointer;
}
.jump:hover { background: var(--accent-soft); }

/* The box. The composer's hero treatment (Tj, 2026-09-20: in every
   reference the field is a wide pill with the key inside it at the right
   end, not a flat field with a button beside it). --radius-hero is 28px:
   on a 60px-tall box that is a capsule, and the moment the field grows to
   two lines the same radius reads as a soft rounded rectangle, which is
   exactly how the references behave. The quiet ambient shadow is what
   lifts it off the canvas; the references all have it and the old flat box
   is why the landing read as basic. It holds the field and the key side by
   side and bottom-aligns them, so that the key stays put while the field
   grows from one line to six. The field is borderless because the box is
   the border -- one shape, not a shape drawn around another one. */
.askbox {
  display: flex; align-items: flex-end; gap: 12px;
  background: var(--surface);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-hero);
  padding: 6px 6px 6px 20px;
  box-shadow: 0 1px 2px rgba(20, 22, 24, .05), 0 8px 28px rgba(20, 22, 24, .07);
  transition: border-color .15s, box-shadow .15s;
}

/* The field. Sized so one empty line of it is exactly as tall as the key
   beside it, which is what keeps the box 60px at rest instead of 60px plus
   whatever the two disagree by. */
.askbox textarea {
  flex: 1; min-width: 0; width: auto;
  resize: none; min-height: var(--tap); max-height: 14rem;
  background: none; border: 0; outline: none; color: var(--ink);
  border-radius: 0; padding: 9px 8px 9px 0;
  font: inherit; font-size: max(1.1rem, 16px); line-height: 1.55; overflow-y: auto;
}
/* The focus affordance lives on the box, not the field: the border turns
   accent and a zero-offset, zero-shift shadow draws the second pixel, so the
   box reads at 2px without anything moving. The ambient shadow stays on
   through the state change -- box-shadow is one property, so the focus rule
   must restate it or the pill visibly drops to the canvas the instant
   somebody clicks in. Forced-colors deletes the box-shadow, so the block
   below hands the box a real outline instead. */
.askbox:focus-within {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent), 0 8px 28px rgba(20, 22, 24, .07);
}
.askbox textarea::placeholder { color: var(--muted); opacity: 1; }

/* The composer's own refusal line: the same left-stripe danger block the
   thread's notices wear, but mounted under the box it talks about, because a
   refused paste never became a turn in the thread. */
.composer > .notice {
  margin: 8px 0 0; padding: 9px 13px;
  border-left: 3px solid var(--danger); border-radius: var(--radius-xs);
  background: var(--danger-wash);
  color: var(--danger);
  font-size: 1rem; line-height: 1.6;
}

.askacts { display: flex; align-items: center; gap: 8px; flex: 0 0 auto; }

/* The key. The one saturated accent moment on the landing (Tj, 2026-09-20:
   every reference spends its colour budget here, on a round filled send
   button, and the rest of the page stays quiet -- this is where the signal
   red goes). A circle, not a pill: the references all draw it round, and
   the roundness is what makes it read as "send" before anybody reads a
   word. The glyph is an arrow, the accessible name is still Ask (a
   visually-hidden span carries it), so the suite's aria-disabled waits and
   every screen reader keep the same button they knew. White on --signal is
   5.50:1 in light and about 3.7:1 in dark, past the 3:1 glyph floor in
   both; the hover darkens the fill rather than washing it toward
   --signal-ink, because a pale-pink fill under white text would fall
   under 2:1 in the dark theme. */
button.send {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--tap); height: var(--tap); padding: 0;
  background: var(--signal); color: var(--accent-ink);
  border: 1px solid var(--signal); border-radius: 50%;
  font: inherit; cursor: pointer;
  flex: 0 0 auto;
}
button.send:hover {
  background: color-mix(in srgb, var(--signal) 86%, #000);
  border-color: color-mix(in srgb, var(--signal) 86%, #000);
}
button.send[aria-disabled="true"] { opacity: .55; cursor: not-allowed; }
.sendglyph { width: 22px; height: 22px; display: block; }

/* Beside Ask, never in place of it. The suite waits on `#send` going
   aria-disabled and back to know a reply has finished, so a Stop that
   swapped itself in for the send button would leave every one of those
   waits watching an element that is not on the page. Outlined rather than
   filled: the filled button is the one that starts a question, and while a
   reply is streaming the only thing left to do is end it. */
button.stop {
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line-strong); border-radius: var(--radius-sm);
  min-height: var(--tap); padding: 0 18px;
  font: inherit; font-size: 1.05rem; font-weight: 600; cursor: pointer;
  flex: 0 0 auto;
}
button.stop:hover { border-color: var(--danger); color: var(--danger); }

/* The dictate key, left of Ask. Same circle as the send button, hollow where
   send is filled: the filled accent circle is the page's one saturated
   moment and it stays Ask's until dictation is the live state, which is
   when this button takes the fill and the visitor can see at a glance which
   control is holding the microphone. The pulse is state, not decoration --
   it answers "is it listening?" -- and it stands down under
   prefers-reduced-motion like every other moving thing on the page. */
button.micbtn,
button.photobtn {
  display: inline-flex; align-items: center; justify-content: center;
  width: var(--tap); height: var(--tap); padding: 0;
  background: var(--surface); color: var(--ink);
  border: 1px solid var(--line-strong); border-radius: 50%;
  font: inherit; cursor: pointer;
  flex: 0 0 auto;
}
button.micbtn:hover,
button.photobtn:hover { border-color: var(--accent); color: var(--accent); }
button.micbtn svg,
button.photobtn svg { width: 21px; height: 21px; display: block; }
button.micbtn.listening {
  background: var(--signal); border-color: var(--signal); color: var(--accent-ink);
  animation: micpulse 1.6s ease-in-out infinite;
}
@keyframes micpulse {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--signal) 40%, transparent); }
  50% { box-shadow: 0 0 0 7px color-mix(in srgb, var(--signal) 0%, transparent); }
}
@media (prefers-reduced-motion: reduce) {
  button.micbtn.listening { animation: none; }
}

/* The photo key rides the same rules as the dictate key above; only its
   state differs, and it has none: a photo either waits or it does not, and
   the pressed state on the button says which. The waiting thumbnail sits
   above the box it will travel with, thumbnail and one word. */
.photoprev { display: flex; align-items: center; gap: 10px; margin: 8px 2px 0; }
.photoprev[hidden] { display: none; }
.photoprev img {
  width: 56px; height: 56px; object-fit: cover; display: block;
  border: 1px solid var(--line-strong); border-radius: var(--radius-xs);
}
.photoremove {
  min-height: var(--tap); padding: 4px 10px; cursor: pointer; font: inherit;
  font-size: .9rem; font-weight: 600; border-radius: var(--radius-xs);
  background: transparent; color: var(--muted); border: 1px solid transparent;
}
.photoremove:hover { color: var(--danger); background: var(--danger-wash); }

/* The photo in the thread: inside the visitor's own bubble, above their
   words, sized to read panel labels without becoming the whole column. The
   replay note stands in for the pixels themselves, which are visit-only. */
.bubble .photoattach {
  display: block; max-width: 220px; width: 100%;
  border: 1px solid var(--line-strong); border-radius: var(--radius-xs);
  margin: 0 0 6px; height: auto;
}
.bubble .photo-note {
  display: block; margin-top: 4px;
  font-size: .85rem; font-weight: 600; color: var(--muted);
}

/* The can-make-mistakes line (Tj, 2026-09-20: all three references close
   the landing with one, and it is the honest sentence for this product
   too). Muted and centred under the composer, with the two legal links
   inline at the same size -- fine print that shouts is its own kind of
   tell. The links are underlined so they read as links without needing a
   different colour; hover brings them to full ink. */
.fineprint {
  margin: 12px 0 0; text-align: center;
  font-size: .85rem; color: var(--muted); line-height: 1.6;
}
.fineprint a {
  color: var(--muted);
  text-decoration: underline; text-underline-offset: 2px;
}
.fineprint a:hover { color: var(--ink); }

.backdrop { display: none; }

/* ── narrow ────────────────────────────────────────────────────────── */
@media (max-width: 60rem) {
  .app { grid-template-columns: 1fr; }
  .sidebar {
    position: fixed; inset: 0 auto 0 0; width: min(21rem, 88vw); z-index: 30;
    transform: translateX(-102%);
    /* .2s on the shared curve rather than .18s `ease`. A drawer that
       decelerates into place reads as a panel being set down; the default
       curve read as a value sliding from -102% to 0. This is the single
       piece of motion a phone visitor sees most. */
    transition: transform .2s var(--ease);
    box-shadow: var(--drawer-shadow);
  }
  body[data-drawer="open"] .sidebar { transform: none; }
  body[data-drawer="open"] .backdrop {
    display: block; position: fixed; inset: 0; z-index: 20;
    background: var(--backdrop-wash); border: 0;
  }
  .menubtn { display: block; padding: 10px 14px; }
  .whenwide { display: none; }
  .whennarrow { display: inline; }
  /* `.msg { max-width: 100% }` stood here, cutting the answer's 90% cap on a
     phone. The answer has no cap any more: it is the column. The visitor's
     bubble keeps its own `min(88%, 34rem)`, which is the same 88% this rule
     used to hand it, so the phone is unchanged and the desktop keeps a
     question from running the full width of a wide window. */
  .welcome h2 { font-size: 1.6rem; }
  /* The Rack and the room used to be overridden here: as a bordered card in
     the middle column they needed their side padding cut on a phone. The
     panel is a view of this column now, so it takes the column's padding and
     only the bottom differs, because nothing sits under the pane. */
  main { padding: 16px 16px 8px; }
  .system { padding: 16px 16px 32px; }
  .composer { padding: 14px 16px 18px; }

  /* The pills stop wrapping on a phone. Nine full-width stacked rows read as
     a wall and carry the fine print past two viewports of scrolling; the
     shape the references keep on a phone is one swipeable line under the
     composer. The row bleeds to the screen edges (the negative margins match
     the composer's 16px gutters) so pills ride to the edge instead of dying
     mid-air, snap settles a swipe on a pill edge, and the scrollbar stays
     silent -- the cut pill at the viewport edge is what says scrollable. */
  .chips {
    flex-wrap: nowrap; justify-content: flex-start; max-width: none;
    margin: 12px -16px 10px; padding: 0 16px;
    overflow-x: auto; scroll-snap-type: x proximity;
    scrollbar-width: none;
  }
  .chips::-webkit-scrollbar { display: none; }
  .chip { flex: 0 0 auto; scroll-snap-align: start; white-space: nowrap; }

  /* Two deliberate rows, not the accidental three the desktop bar fell into.
     Left alone it broke after the brand and stranded the text-size control at
     the right edge of a second row with 213px of nothing beside it: 177px of
     an 845px screen, above a page whose whole point is the answer.

     One row is reachable only by dropping the size buttons under the 48px
     floor, which is the wrong trade on the control that exists for people who
     need larger text. So row one is the menu button and the wordmark, and row
     two is the size control, left-aligned under it where it reads as a toolbar
     rather than an orphan. The subtitle goes: the hero two lines down says the
     same thing at 1.9rem. */
  header { padding: 10px 14px; gap: 10px; }
  .brand { flex: 1 1 auto; min-width: 0; }
  .brand .sub { display: none; }
  .brand h1 { font-size: 1.2rem; white-space: nowrap; }
  .headright { margin-left: 0; }
  /* The size control's phone overrides went with the fieldset they were
     written for. There is nothing left on it to shrink: three 48px squares
     are the target floor, and the floor is the point of the control. */
}
@media (prefers-reduced-motion: reduce) {
  * { animation-duration: .01ms !important; transition-duration: .01ms !important; }
}
/* Windows High Contrast mode (forced-colors). The browser throws this whole
   palette out and repaints text and grounds in the reader's own four system
   colors, which is the mode working as intended, and almost nothing here
   needs saving from it: text stays text, buttons stay buttons. What breaks is
   exactly the things this file says without ink. Focus rings drawn as outline
   in --accent get recolored but survive; rings drawn as box-shadow, which the
   mode deletes, do not, so the ask box gets a real outline. The notice
   stripes, which carry "this is about the danger" and "this is the page
   talking" in a color the mode no longer has, move to system colors: LinkText
   for the alarm family, plain CanvasText for the quiet one. Button fills that
   mean it (the confirm row) fall back to underline on hover, because a
   white-on-accent hover the mode cannot paint would otherwise read as a dead
   control. Anything not named here is deliberately left to the mode's own
   rewrite. */
@media (forced-colors: active) {
  a:focus-visible, button:focus-visible, textarea:focus-visible,
  input:focus-visible, [tabindex]:focus-visible {
    outline-color: Highlight;
  }
  .askbox:focus-within {
    box-shadow: none;
    outline: 3px solid Highlight; outline-offset: 2px;
  }
  .msg .notice, .confirm, .admin .note {
    border-left-color: LinkText;
  }
  .msg .notice.quiet, .confirm.quiet {
    border-left-color: CanvasText;
  }
  .confirm .confirmrow button:hover { text-decoration: underline; }
}
/* Print is the one place both halves of the middle column are wanted at once,
   which is the arrangement the screen never shows. What is worth printing is
   the answer and the context it was written for: the Rack and the room are
   the half of the answer that was never typed into the question, so a printed
   answer without them is a transcript of something the reader cannot
   reconstruct. The context goes first, where the sidebar used to put it,
   because a reader who meets "your amp" on line one should already know which
   one. The form, the Remove buttons and the keeper line are all controls, and
   the keeper line is a sentence about what a browser will do on a later
   visit, which on paper is nothing.

   The whole sidebar goes with them. It is a conversation list and an account
   block, and both are the paper's antithesis: links that go nowhere in ink,
   and a sign-in form. The screen rule that hides #main behind the system view
   is body-qualified, so re-showing it here takes the same qualifier. */
@media print {
  .app { display: flex; flex-direction: column; }
  .sidebar { display: none; }
  .system {
    order: -1; flex: none; display: block;
    max-width: none; padding: 0 0 14px;
  }
  body[data-view="system"] #main { display: block; }
  .systemhead, .systemsub, .rackform, .rackremove, .rackkeep { display: none; }
  .composer, .headright, .threadbar, .acts { display: none; }
  /* The pieces the screen passes added, judged for paper: the wait line and
     the first-run card are session chrome that means nothing in ink, and
     the search box and theme button are sidebar controls whose sidebar is
     already gone. The provenance line stays -- it is part of the answer's
     record -- but it prints grey, because the muted token is a light-mode
     violet tuned for screens and turns faint on white paper. */
  .statusline, .convsearch, .themebtn { display: none; }
  .sources { color: #333; }
  body { background: #fff; color: #111; }
  /* Only the visitor's bubble is a box in print, because it is the only one
     on screen: the answer prints as the prose it is, on the page's own white.
     A copy button prints as nothing, so it is with the other controls. */
  .msg.user .bubble { background: #fff; border-color: #ccc; }
  /* The fenced block gives up its sideways scroll for paper: a clipped
     measurement is worse than a wrapped one, and the printout cannot scroll.
     The quoted line keeps its left rule -- it is the manual speaking, and on
     paper the rule is what says so. */
  .bubble pre { white-space: pre-wrap; overflow: visible; }
  .welcome p, .plans p { color: #333; }
}

/* ── legal pages ─────────────────────────────────────────────────────
   Markup: static/privacy.html, static/terms.html, static/refunds.html.
   The app shell is a two-column grid whose left column is a conversation
   list; none of that belongs on a contract, so these pages are one measured
   column instead. Same tokens, same 18.4px base, same AAA contrast floor:
   the older-reader rules do not stop at the app page. Everything is scoped
   under .legal or .legalfoot so nothing here can reach the app markup. */
.legal {
  max-width: 46rem;
  margin: 0 auto;
  padding: 40px 24px 72px;
}
.legal .back { font-size: 1rem; margin-bottom: 24px; }
.legal h1 {
  font-family: var(--display);
  font-size: 2.2rem;
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: -.025em;
}
.legal .updated {
  color: var(--muted);
  font-size: 1rem;
  margin-top: 12px;
  padding-bottom: 22px;
  border-bottom: 1px solid var(--line);
}
.legal h2 {
  font-family: var(--display);
  font-size: 1.35rem;
  font-weight: 700;
  line-height: 1.3;
  letter-spacing: -.02em;
  margin: 40px 0 0;
}
.legal h3 { font-size: 1.08rem; font-weight: 700; margin: 28px 0 0; }
.legal p { margin-top: 14px; }
.legal ul, .legal ol { margin: 14px 0 0 1.4em; }
.legal li { margin-top: 9px; }
.legal strong, .legal b { color: var(--ink); font-weight: 700; }
.legal code {
  font-family: var(--mono);
  font-size: .92em;
  background: var(--canvas-deep);
  border: 1px solid var(--line);
  border-radius: 0;
  padding: 1px 5px;
}
/* Three columns, never four: at 46rem a fourth column squeezes the text
   into a strip, and these pages are read on phones by people who are not
   looking for a horizontal scrollbar. */
.legal table { width: 100%; border-collapse: collapse; margin-top: 18px; }
.legal th, .legal td {
  text-align: left;
  vertical-align: top;
  padding: 12px 12px 12px 0;
  border-bottom: 1px solid var(--line);
}
.legal th {
  font-weight: 700;
  color: var(--ink);
  border-bottom-color: var(--line-strong);
}
.legal td { color: var(--muted); }
.legal td strong { color: var(--ink); }
/* The one box on the contract pages. It carries the DeepSeek transfer on the
   privacy policy and the voltage warning on the terms, because a fact that
   changes what a visitor should type deserves to be impossible to skim past.
   The 3px left rule is the house emphasis pattern, the same one the error and
   off-topic answers use in the transcript above. The signup page borrows the
   box for its no-script notice, so the selector covers both shells. */
.legal .callout, .sell .callout {
  margin-top: 22px;
  padding: 20px 22px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: 3px solid var(--signal);
  border-radius: var(--radius);
}
.legal .callout p, .sell .callout p { margin-top: 12px; }
.legal .callout .label, .sell .callout .label {
  font-family: var(--mono);
  font-size: .85rem;
  letter-spacing: .04em;
  color: var(--link);
}
.legalfoot {
  max-width: 46rem;
  margin: 0 auto;
  padding: 28px 24px 56px;
  border-top: 1px solid var(--line);
  display: flex;
  flex-wrap: wrap;
  gap: 14px 24px;
  font-size: 1rem;
}
.legalfoot .fine { width: 100%; color: var(--muted); font-size: .95rem; }
@media (max-width: 34rem) {
  .legal { padding: 28px 18px 56px; }
  .legal h1 { font-size: 1.75rem; }
  .legal th, .legal td { font-size: .95rem; padding-right: 8px; }
}

/* ── marketing pages (/features, /signup) ────────────────────────────
   Markup: static/features.html, static/signup.html. The two selling
   pages, in the sitemap, both in the .sell shell below. The signup form
   is the drawer's .acctform, so the page shows the same surface the
   product itself shows; the rules here are only what a sidebar form
   needs to sit in a page column. The inputs are sized for a 19rem
   sidebar, so the form gets its own measure, and a hairline above puts
   it in the same ruled language as .sellnums and .tiers. The submit
   button takes the filled accent: the base .acctbtns styles carry no
   fill (the drawer's two buttons are filled and outlined by id), and an
   unstyled default button as the page's one action was the one thing the
   borrowed shell never fixed. Scoped under .sell so nothing here can
   reach the app markup. */
.sell .acctform {
  max-width: 26rem;
  margin-top: 30px;
  padding-top: 26px;
  border-top: 1px solid var(--line);
  gap: 12px;
}
.sell .acctform .acctbtns { margin-top: 8px; }
.sell .acctform .acctbtns button {
  background: var(--accent);
  color: var(--accent-ink);
  border: 1px solid var(--accent);
  font-size: 1.05rem;
  padding: 12px 12px;
}
.sell .acctform .acctbtns button:hover {
  background: var(--accent-dark);
  border-color: var(--accent-dark);
}

/* ── the selling pages (/features, /signup) ──────────────────────────
   Markup: static/features.html, static/signup.html. This shell exists
   because both pages wore the legal shell for as long as they existed,
   and that is what was wrong with them: a contract page and a selling
   page want opposite things from the same 46rem. A contract is read top
   to bottom and never looked at twice, so one column and a steady rhythm
   is correct. A selling page is looked at before it is read, and the
   first screen has to show something. Borrowed clothes meant features
   had nowhere to put a picture and no scale above 2.2rem, so it
   described the bench in eight paragraphs and showed nothing, and
   signup rendered as a contract with a form squeezed in after the prose.
   signup.html moved over 2026-09-17 with billing-welcome.html; see the
   marketing block above for what the form borrows.

   What is new here is a shell, not a palette. Same tokens as everything
   else, same 18.4px base, same AAA contrast floor, square corners. The
   measure is 56rem against the legal pages' 46rem, and the h1 goes to
   2.6rem because this is the one page whose heading has to hold a room
   from across a desk. */
.sell { max-width: 56rem; margin: 0 auto; padding: 40px 24px 72px; }
.sell .back { font-size: 1rem; margin-bottom: 22px; }
.sell h1 {
  font-family: var(--display);
  font-size: 2.6rem;
  font-weight: 700;
  line-height: 1.12;
  letter-spacing: -.03em;
}
.sell h2 {
  font-family: var(--display);
  font-size: 1.35rem;
  font-weight: 700;
  line-height: 1.3;
  letter-spacing: -.02em;
  margin: 46px 0 0;
}
.sell h3 { font-size: 1.08rem; font-weight: 700; margin: 28px 0 0; }
.sell p { margin-top: 14px; max-width: 40rem; }
.sell ul, .sell ol { margin: 14px 0 0 1.4em; }
.sell li { margin-top: 9px; }
.sell strong, .sell b { color: var(--ink); font-weight: 700; }

/* The hero: the claim on the left, the drawing on the right, and the
   drawing comes first on a phone because it is the part that is worth
   seeing before the part that is worth reading. */
.sellhero { display: grid; gap: 26px; align-items: start; }
.sellhero h1 { margin: 0; max-width: 32rem; }
.sellsub {
  font-size: 1.12rem;
  line-height: 1.7;
  color: var(--muted);
  max-width: 34rem;
}
/* The honesty line under the comparison rows on the features page. Muted on
   purpose: it is a qualification, and the section's credibility rests on it
   reading as one rather than as a fifth selling point. */
.versusfine { color: var(--muted); font-size: .98rem; }
.sellacts {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 12px 20px;
  margin-top: 26px;
}

/* The filled action. `.plans a.cta` is what used to dress this link and it
   is scoped to the app's plans panel, which this page no longer uses, so
   the fill has to be stated here or the primary action renders as a bare
   underlined link. */
.sell a.cta {
  display: inline-flex;
  align-items: center;
  margin-top: 0;
  background: var(--accent);
  color: var(--accent-ink);
  padding: 14px 22px;
  border-radius: var(--radius-sm);
  font-size: 1.05rem;
  font-weight: 600;
  text-decoration: none;
  min-height: var(--tap);
}
.sell a.cta:hover { background: var(--accent-dark); color: var(--accent-ink); }
.sellalt { font-size: 1.05rem; font-weight: 600; }

/* Three facts under the claim, set as a spec list rather than a row of
   tiles. This is the house emphasis pattern (mono label, ink value) that
   the callout boxes use, and it is the honest shape for three numbers
   that are not comparable to each other: a trial count, a corpus size
   and a price do not belong in equal boxes. */
.sellnums {
  display: grid;
  gap: 10px;
  margin-top: 28px;
  padding-top: 20px;
  border-top: 1px solid var(--line);
  max-width: 34rem;
}
.sellnums > div {
  display: grid;
  grid-template-columns: 7rem minmax(0, 1fr);
  gap: 2px 16px;
  align-items: baseline;
}
.sellnums dt {
  font-family: var(--mono);
  font-size: .85rem;
  letter-spacing: .04em;
  color: var(--signal-ink);
}
.sellnums dd { color: var(--muted); font-size: 1rem; line-height: 1.55; }

/* The drawing. The global .roomplan rules centre it and cap it at 22rem
   for the app's welcome row; as a hero it fills its column instead. */
.sellfig { display: grid; gap: 14px; margin: 0; }
.sellfig .roomplan { max-width: 100%; margin: 0; }
.sellfig figcaption { font-size: .95rem; line-height: 1.6; color: var(--muted); }

/* ── the hero demo (/features, /signup) ──────────────────────────────
   Markup: the <figure class="demo"> both selling pages carry, one block
   held identical by test_marketing_pages.py. A framed run of the box
   itself: the question, the wait, the answer, and the line under the
   answer that says what it stood on. The furniture inside is the app's
   own (.msg, .bubble, .sources are global rules, so a demo on a selling
   page renders exactly as it does on the box), which is the point: the
   frame shows the product, not an artist's idea of it.

   The frame itself is one warm surface: hairline, the panel fill, and a
   single soft shadow to lift it off the paper. The bar above the body is
   browser chrome, drawn but not pretended: three unlit dots and the
   address the box actually lives at. */
.demo { margin: 8px auto 0; max-width: var(--col); }
.demoframe {
  background: var(--surface);
  border: 1px solid var(--line);
  box-shadow: 0 26px 60px -42px rgba(0, 0, 0, .5);
}
.demobar {
  display: flex; align-items: center; gap: 6px;
  padding: 10px 14px;
  border-bottom: 1px solid var(--line);
}
.demobar .dot {
  width: 9px; height: 9px; border-radius: 50%;
  border: 1px solid var(--line-strong);
}
.demobar .demourl {
  margin-left: 8px;
  font-family: var(--mono); font-size: .8rem; letter-spacing: .04em;
  color: var(--muted);
}
.demobody { display: flex; flex-direction: column; padding: 22px 22px 24px; }
.democap {
  margin: 14px 0 0;
  font-family: var(--mono); font-size: .85rem; letter-spacing: .02em;
  color: var(--muted); text-align: center;
}

/* The play gate. The demo is written finished on the page, and every rule
   below hides part of it only under .mk-js, a class the page's script
   adds to <html> when two things are true: script ran, and the reader did
   not ask for reduced motion. No script or reduced motion means the class
   never lands, so the fallback is the finished answer sitting there.
   Opacity and transform only, so the finished layout is reserved before
   the first frame plays and nothing below the demo reflows while it
   runs. */
.mk-js .demo .msg {
  opacity: 0; transform: translateY(8px);
  transition: opacity .5s var(--ease), transform .5s var(--ease);
}
.mk-js .demo .msg.is-in { opacity: 1; transform: none; }
/* The answer arrives the way the app's answers arrive, one line at a
   time. The waits between lines are transition delays rather than
   timers, so one class on the message carries all four lines and the
   sources line in and nothing has to keep count. */
.mk-js .demo .d-ans li {
  opacity: 0; transform: translateY(6px);
  transition: opacity .45s var(--ease), transform .45s var(--ease);
}
.mk-js .demo .d-ans.is-in li { opacity: 1; transform: none; }
.mk-js .demo .d-ans li:nth-child(1) { transition-delay: 0ms; }
.mk-js .demo .d-ans li:nth-child(2) { transition-delay: 220ms; }
.mk-js .demo .d-ans li:nth-child(3) { transition-delay: 440ms; }
.mk-js .demo .d-ans li:nth-child(4) { transition-delay: 660ms; }
.mk-js .demo .d-ans .sources {
  opacity: 0;
  transition: opacity .6s var(--ease) 940ms;
}
.mk-js .demo .d-ans.is-in .sources { opacity: 1; }
/* The wait bubble is transient furniture: it exists only while the demo
   is playing it, so the script toggles it with .d-on rather than fading
   it, which is also how the app itself swaps it for the answer. Outside
   a play it is not in the page at all, which is why this hiding is
   allowed to sit outside the gate. */
.demo .d-think { display: none; }
.demo .d-think.d-on { display: flex; }
/* The typing mark is the app's own caret, the signal bar that
   .bubble.streaming draws, borrowed for the question. It can only exist
   mid-play, and the play only exists when the reader asked for motion,
   so it needs no reduced-motion line of its own. */
.demo .d-typing::after {
  content: "";
  display: inline-block;
  width: .46em; height: 1.02em;
  margin-left: .14em; vertical-align: -.16em;
  background: var(--signal);
  animation: caret 1.1s steps(1, end) infinite;
}

@media (max-width: 34rem) {
  .demobar { padding: 8px 12px; }
  .demobody { padding: 16px 14px 18px; }
}

/* The hero's drawing column is gone: the room drawing moved down into the
   "It answers for the system you own" section, next to the argument it
   belongs to, and the hero stacks claim, facts, and demo in one column. */

/* The tiers, with the panel taken off. They were rows inside a bordered
   card that the app also draws for its own upgrade panel, and the card
   was doing the work a hairline already does: three prices stacked in a
   box read as one object and invite the eye to compare them as a set,
   while three hairline-ruled rows read as three separate offers, which
   is what they are. Each row carries its own way in. */
.tiers { list-style: none; margin: 24px 0 0; padding: 0; }
.tier { display: grid; gap: 6px; padding: 22px 0; border-top: 1px solid var(--line); }
.tier:last-child { border-bottom: 1px solid var(--line); }
.tierhead {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 4px 18px;
}
.tierhead h3 { margin: 0; font-size: 1.12rem; }
.tierprice {
  margin: 0;
  font-family: var(--mono);
  font-size: .95rem;
  letter-spacing: .02em;
  color: var(--signal-ink);
}
.tier p { margin: 0; max-width: 38rem; color: var(--muted); line-height: 1.65; }
.tier a.cta { justify-self: start; margin-top: 10px; }

/* The spec sheet: the same facts the page already states, arranged so a
   reader can find one without reading all of them. Two columns wide, one
   column on a phone, hairline between every pair. */
.spec { margin-top: 22px; }
.spec > div { display: grid; gap: 2px 20px; padding: 13px 0; border-top: 1px solid var(--line); }
.spec > div:last-child { border-bottom: 1px solid var(--line); }
.spec dt { color: var(--ink); font-weight: 700; font-size: 1rem; }
.spec dd { color: var(--muted); font-size: 1rem; line-height: 1.6; }

@media (min-width: 40rem) {
  .spec > div { grid-template-columns: 12rem minmax(0, 1fr); align-items: baseline; }
}

@media (max-width: 34rem) {
  .sell { padding: 28px 18px 56px; }
  .sell h1 { font-size: 1.95rem; }
  .sellnums > div { grid-template-columns: minmax(0, 1fr); gap: 1px; }
  .sellnums dd { margin-bottom: 8px; }
}

/* -- operator screen (/admin) --------------------------------------------
   Markup: admin.html. Reached only through the portal gate, so this is
   styling for one reader rather than for an audience, and the rules below are
   about making a number findable instead of making a paragraph comfortable.

   The measure is 60rem, wider than the legal pages' 46rem, and that is the
   one deliberate disagreement with them. Those pages are prose read line by
   line; this one is tables read by scanning down a column, and three columns
   of addresses and timestamps do not fit in a prose measure. */
.admin { max-width: 60rem; margin: 0 auto; padding: 40px 24px 72px; }
.admin .back { font-size: 1rem; margin-bottom: 24px; }
.admin h1 {
  font-family: var(--display);
  font-size: 2.2rem;
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: -.025em;
}
.admin .updated {
  color: var(--muted);
  font-size: 1rem;
  margin-top: 12px;
  padding-bottom: 22px;
  border-bottom: 1px solid var(--line);
}
.admin h2 {
  font-family: var(--display);
  font-size: 1.35rem;
  font-weight: 700;
  line-height: 1.3;
  letter-spacing: -.02em;
  margin: 40px 0 0;
}
.admin h3 { font-size: 1.08rem; font-weight: 700; margin: 28px 0 0; }

/* The four numbers at the top, sized to be read from across a desk. auto-fit
   rather than four fixed columns: a phone gets two rows of two, and a wide
   window does not stretch a two-digit number over 300px of empty tile. */
.admin .tiles {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(10rem, 1fr));
  gap: 14px;
  margin-top: 24px;
}
.admin .tile {
  padding: 18px 20px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
}
.admin .tile .n {
  font-family: var(--display);
  font-size: 2rem;
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -.03em;
  /* A column of numbers that shifts width as the count changes is read as
     movement. Tabular figures hold the columns still. */
  font-variant-numeric: tabular-nums;
}
.admin .tile .k {
  font-family: var(--mono);
  font-size: .85rem;
  letter-spacing: .04em;
  color: var(--muted);
  margin-top: 4px;
}

.admin table { width: 100%; border-collapse: collapse; margin-top: 18px; }
.admin th, .admin td {
  text-align: left;
  vertical-align: top;
  padding: 12px 12px 12px 0;
  border-bottom: 1px solid var(--line);
}
.admin th {
  font-weight: 700;
  color: var(--ink);
  border-bottom-color: var(--line-strong);
}
.admin td { color: var(--muted); }
/* The second cell is the answer and the first is its label, so the value is
   the one that gets the ink and the figures. */
.admin td + td { color: var(--ink); font-variant-numeric: tabular-nums; }
/* An address is the one value here that can be arbitrarily long, and a table
   that scrolls sideways on a phone hides the column that says when. */
.admin td:first-child { overflow-wrap: anywhere; }

/* The sentence that says what a number is NOT. Same 3px rule the legal pages
   use for the fact that changes what a reader should do, because "these are
   not lifetime totals" is exactly that kind of fact. */
.admin .note {
  margin-top: 18px;
  padding: 14px 18px;
  background: var(--surface);
  border: 1px solid var(--line);
  border-left: 3px solid var(--signal);
  border-radius: var(--radius);
  color: var(--muted);
  font-size: 1rem;
}

/* A zero, said out loud. No rule and no accent: nothing is wrong, the number
   is just zero, and a page that dresses an empty state as a warning trains
   its reader to ignore warnings. */
.admin .empty { margin-top: 14px; color: var(--muted); font-size: 1rem; }

@media (max-width: 34rem) {
  .admin { padding: 28px 18px 56px; }
  .admin h1 { font-size: 1.75rem; }
  .admin th, .admin td { font-size: .95rem; padding-right: 8px; }
  .admin .tile .n { font-size: 1.7rem; }
}

/* ── the features page's second act ──────────────────────────────────
   Four blocks the 2026-09-19 pass added to /features: the ask grid, the
   system section with the room drawing, the answer sequence, and the
   close band. Specificity note that has bitten once already: `.sell p`
   is (0,1,1) and beats a bare class, so every paragraph rule here is
   parent-prefixed to win on purpose rather than by file order. */
.askgrid {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0 34px;
  margin-top: 26px;
}
/* A ruled page, not cards: one hairline above each cell and a single left
   rule between the two columns. The grid's placement is fixed, so the
   right-column cell is exactly child 3: the first child spans, the next
   two share the middle row, the last spans again. */
.askgrid .askcell {
  padding: 18px 0 20px;
  border-top: 1px solid var(--line);
}
.askgrid .askcell:nth-child(3) {
  border-left: 1px solid var(--line);
  padding-left: 34px;
}
/* Wide cells span both columns, which is what gives the rhythm
   wide / narrow narrow / wide, and they never carry the column rule. */
.askgrid .askcell.wide {
  grid-column: 1 / -1;
  border-left: 0;
  padding-left: 0;
}
.askgrid .askcell .n {
  display: block;
  font-family: var(--mono);
  font-size: .85rem;
  letter-spacing: .04em;
  color: var(--signal-ink);
}
.askgrid .askcell p {
  margin: 8px 0 0;
  font-size: 1.08rem;
  line-height: 1.55;
  color: var(--ink);
  max-width: none;
}
@media (max-width: 40rem) {
  .askgrid { grid-template-columns: 1fr; gap: 0; }
  .askgrid .askcell:nth-child(3) { border-left: 0; padding-left: 0; }
}

/* The system section: the argument on the left, the room drawing on the
   right, so the claim and its evidence share one view. Under 48rem the
   drawing drops beneath the list, where the global .roomplan rule already
   caps and centres it. */
.sysrow { display: grid; gap: 30px; margin-top: 26px; align-items: start; }
.sysrow .syschanges { list-style: none; margin: 0; padding: 0; }
.sysrow .syschanges li {
  margin: 0;
  padding: 16px 0 18px;
  border-top: 1px solid var(--line);
  color: var(--muted);
  line-height: 1.6;
  max-width: none;
}
.sysrow .syschanges li:first-child { border-top: 0; padding-top: 4px; }
.sysrow .syschanges li b {
  display: block;
  margin-bottom: 4px;
  color: var(--ink);
}
@media (min-width: 48rem) {
  .sysrow { grid-template-columns: minmax(0, 1fr) 20rem; gap: 38px; }
}

/* The answer sequence. Each step pins under the last one's edge while the
   next arrives, so "retrieval, then grounding, then the answer" is
   something the reader moves through rather than three paragraphs. The
   whole effect is CSS sticky: three offsets make the deck, the rail is
   one pseudo-element, and the page scrolls at its own pace throughout.
   The steps carry an opaque paper background because that is what lets a
   later step slide over an earlier one without either going transparent. */
.seq { position: relative; margin-top: 26px; padding-left: 26px; }
.seq::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 1px;
  background: var(--line);
}
.seq::after {
  content: "";
  position: absolute;
  left: -1px;
  top: 0;
  bottom: 0;
  width: 3px;
  background: var(--signal);
  transform-origin: top;
  transform: scaleY(0);
}
.seqstep {
  position: sticky;
  background: var(--canvas);
  padding: 22px 0 26px;
  border-top: 1px solid var(--line);
  min-height: 42vh;
}
/* The increasing offsets are the deck: each step comes to rest slightly
   lower than the one before it, so all three ends stay legible. */
.seqstep:nth-child(1) { top: 1.5rem; }
.seqstep:nth-child(2) { top: 3rem; }
.seqstep:nth-child(3) { top: 4.5rem; }
.seq .seqno {
  margin: 0;
  font-family: var(--mono);
  font-size: .9rem;
  letter-spacing: .04em;
  color: var(--signal-ink);
}
.seq .seqtext {
  margin: 10px 0 0;
  font-size: 1.12rem;
  line-height: 1.65;
  color: var(--ink);
  max-width: 36rem;
}
/* The rail fills as the section crosses the viewport, where the browser
   can do it for free, and stays a plain hairline everywhere else. Scroll-
   linked, not autonomous: nothing moves that the reader did not move. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .seq::after {
      animation: seqfill linear both;
      animation-timeline: view();
      animation-range: cover 12% cover 88%;
    }
  }
}
@keyframes seqfill {
  from { transform: scaleY(0); }
  to { transform: scaleY(1); }
}

/* The close. One band after the proof: the offer in full, then the two
   doors. The strong rule is the furniture the hero opens with, so the
   page ends on the same weight it began on. */
.closeband {
  margin-top: 64px;
  padding-top: 40px;
  border-top: 1px solid var(--line-strong);
}
.closeband h2 { margin: 0; }
.closeband p { max-width: 40rem; }
.closeband .sellacts { margin-top: 24px; }

/* ── the signup page's second act ────────────────────────────────────
   Markup: static/signup.html. The page keeps the demo, the drawer's
   own form, and the ruled fact rows; what the pass added around them
   is the signature (Your system, carrying the room drawing), the bench
   list, and the trust rows. The one new voice is the mono section
   label: it is the .sellnums dt treatment promoted to a section
   opener, so "Your system" and "On the bench" read as labels on the
   argument rather than as two more headings competing with the
   content they introduce. The sysrow and sellfig rules above are
   reused as-is; only what the signup markup needed beyond them lives
   here. Scoped under .sell so nothing reaches the app markup. */
.sell .selllabel {
  font-family: var(--mono);
  font-size: .85rem;
  letter-spacing: .04em;
  color: var(--signal-ink);
  margin-top: 44px;
}
.sell .selllabel + h2 { margin-top: 6px; }
.sell .fineprint { font-size: .95rem; color: var(--muted); }
.sell .bench {
  list-style: none;
  margin: 22px 0 0;
  padding: 0;
  max-width: 40rem;
}
.sell .bench li {
  margin: 0;
  padding: 18px 0 20px;
  border-top: 1px solid var(--line);
}
.sell .bench li:first-child { border-top: 0; padding-top: 6px; }
.sell .bench h3 { margin: 0; }
.sell .bench li p { margin-top: 6px; color: var(--muted); }

/* The post-payment page (billing-welcome.html) shares the shell, and its
   one odd-sized note is the .plannote the drawer carries. Ruled into the
   page's own language rather than left floating: same hairline the fact
   rows use, same distance the sections keep. The poll script's element
   ids are untouched by any of this; they are contract. */
.sell .plannote {
  margin-top: 44px;
  padding-top: 20px;
  border-top: 1px solid var(--line);
}

/* ── A6: the motion pass ────────────────────────────────────────────────
   Lifted by hand from apps/ui-motion-kit (css/kit.css, js/kit.js) and
   retinted to the current palette. The kit is internal and is
   never linked from a deployed page; blocks arrive as copies, under the
   kit's house rules: transform and opacity only, every loop gated on
   no-preference (the global reduce block earlier in this file is the
   belt; these gates are the suspenders), and a finished, still page
   whenever script or motion is absent -- every script-fed rule is
   keyed on .mk-js, which the selling pages' own script only adds after
   both the motion and the script checks have passed. */

:root {
  --spring: cubic-bezier(.34, 1.56, .64, 1);
  --calm: cubic-bezier(.22, .61, .36, 1);
}

/* The ember ambience. Three blurred radial gradients drifting on
   transform, and the grain tile over them: banding is what gives a
   gradient away, and the grain is what makes it read as paper instead.
   The layer is fixed, behind the page, pointer-transparent, and costs
   nothing at LCP -- the largest paint is still the words. */
.sellamb { position: fixed; inset: 0; z-index: 0; overflow: hidden; pointer-events: none; }
body:has(.sellamb) > main, body:has(.sellamb) > footer { position: relative; z-index: 1; }
.sellamb i { position: absolute; display: block; border-radius: 50%; filter: blur(70px); will-change: transform; }
.sellamb .b1 {
  width: 44vw; height: 44vw; top: -16vw; left: -10vw;
  background: radial-gradient(circle, rgba(198, 141, 61, .22), transparent 65%);
}
.sellamb .b2 {
  width: 38vw; height: 38vw; bottom: -14vw; right: -8vw;
  background: radial-gradient(circle, rgba(140, 109, 63, .18), transparent 65%);
}
.sellamb .b3 {
  width: 30vw; height: 30vw; top: 30%; right: 18%;
  background: radial-gradient(circle, rgba(176, 84, 31, .12), transparent 65%);
}
.sellamb::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
  opacity: .07; mix-blend-mode: overlay;
}
@media (prefers-reduced-motion: no-preference) {
  .sellamb .b1 { animation: mk-drift1 34s var(--calm) infinite alternate; }
  .sellamb .b2 { animation: mk-drift2 44s var(--calm) infinite alternate; }
  .sellamb .b3 { animation: mk-drift3 28s var(--calm) infinite alternate; }
}
@keyframes mk-drift1 { to { transform: translate(14%, 22%) scale(1.15); } }
@keyframes mk-drift2 { to { transform: translate(-18%, -14%) scale(.9); } }
@keyframes mk-drift3 { to { transform: translate(-24%, 18%) scale(1.2); } }

/* The glass, deliberately once: the demo frame. Blur plus saturate over
   the ambience behind it, a hairline, and one inset highlight along the
   top edge -- transparency alone reads as a gray box, the highlight is
   what makes it read as glass. Everything the demo holds keeps its own
   grounding; only the chrome goes translucent. */
.demoframe {
  background: rgba(255, 253, 250, .74);
  border: 1px solid rgba(38, 29, 21, .22);
  backdrop-filter: blur(14px) saturate(150%);
  -webkit-backdrop-filter: blur(14px) saturate(150%);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .65), 0 26px 60px -42px rgba(0, 0, 0, .5);
}

/* The blur reveal: the headline's words start 8px blurred and 6px low
   and arrive on a 35ms ladder. The spans are wrapped at runtime by the
   pages' own script, so the markup keeps the plain sentence and a
   no-script visit reads exactly what a scripted one ends at. */
.mk-js [data-reveal="blur"]:not(.is-in) .w {
  opacity: 0; filter: blur(8px); transform: translateY(6px);
}
[data-reveal="blur"] .w {
  display: inline-block;
  transition: opacity .7s var(--calm), filter .7s var(--calm), transform .7s var(--calm);
  transition-delay: calc(var(--i) * 35ms);
}

/* The sheen, one pass, after the words have landed. It is class-fed (the
   script adds .sheen when the reveal is done) because a background-clip
   over transformed children does not track them -- the clip only goes on
   once every span is back at rest. Wrapped in @supports, because a
   browser without clip:text and a transparent color would be an
   invisible headline, and that trade is never worth a shimmer. */
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
  @media (prefers-reduced-motion: no-preference) {
    .mk-js h1.sheen {
      background: linear-gradient(100deg, var(--ink) 42%, #8a6a35 50%, var(--ink) 58%);
      background-size: 220% 100%;
      /* 2026-11-06 visual pass: holding the END position (-100%) left some
         words resting on the bronze stop - the background repeats, and the
         wrapped window parks mid-gradient on narrow elements. Resting at
         the START (120%) lands the window on pure ink at both ends of the
         wrap, so the pass ends as ink everywhere. */
      background-position: 120% 0;
      -webkit-background-clip: text;
      background-clip: text;
      color: transparent;
      animation: mk-shimmer 3s linear 1 backwards;
    }
  }
}
@keyframes mk-shimmer { from { background-position: 120% 0; } to { background-position: -100% 0; } }

/* The highlighter on the mono labels: one warm band sweeping in from the
   left behind the words, the kit's mark. The label shrinkwraps so the
   band hugs the text instead of ruling the whole column. */
.sell .selllabel { position: relative; z-index: 0; width: fit-content; }
.mk-js .selllabel::after {
  content: ""; position: absolute; z-index: -1;
  left: -0.15em; right: -0.15em; top: 10%; bottom: 4%;
  background: rgba(236, 192, 107, .5);
  transform: scaleX(0); transform-origin: left;
  transition: transform .8s var(--calm) .25s;
}
.mk-js .selllabel.is-in::after { transform: scaleX(1); }

/* The room drawing draws itself. The script masks every stroke to its
   own length and hands the unmasking back one stroke at a time; the
   labels arrive last, once the geometry is down. Without script the
   drawing is simply finished, which is also what the tests hold. */
.mk-js .roomplan :is(path, line, circle) { transition: stroke-dashoffset .9s var(--calm); }
.mk-js .roomplan .rp-label { opacity: 0; transition: opacity .5s var(--calm) 1.3s; }
.mk-js .roomplan.rp-go .rp-label { opacity: 1; }

/* The brands band: one row from the knowledge base's own brand index,
   twice, so the loop is seamless (the padding-right is the gap the
   -50% translation lands in). Edges fade with the mask; a hover, a
   focus inside, or the pause button holds it still, because a reader
   who stops to look is not interrupted. The strip only ever moves
   behind the .mk-js gate: without script, or under reduced motion, the
   list is simply still and readable. The pause button is the keyboard's
   copy of the hover (WCAG 2.2.2: stopping the strip may not require a
   mouse). It lives out of the way until the band is hovered or the
   button tabbed to, then reads as ordinary band furniture; the 13% left
   keeps it inside the mask's fully-opaque middle so it never ghosts.
   The reveal hangs on the band's hover, not the button's: a hidden
   control that ignores pointers can never be hovered in the first
   place. */
.brandband {
  position: relative;
  overflow: hidden;
  border-block: 1px solid var(--line);
  padding: .8rem 0;
  margin: 4px 0 6px;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
  mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}
.brandband .track { display: flex; width: max-content; }
.brandband .mqrow { display: flex; gap: 2.6rem; padding-right: 2.6rem; }
.brandband span {
  white-space: nowrap;
  font-family: var(--mono); font-size: .85rem; letter-spacing: .04em;
  color: var(--muted);
}
.brandband .mqpause {
  position: absolute; left: 13%; top: 50%; transform: translateY(-50%);
  z-index: 1;
  display: none;
  min-height: 24px;
  font: 600 .8rem/1 var(--mono); letter-spacing: .04em;
  color: var(--muted);
  background: var(--canvas);
  border: 1px solid var(--line);
  border-radius: 999px;
  padding: .4rem .8rem;
  cursor: pointer;
}
.mk-js .brandband .mqpause {
  display: inline-flex; align-items: center;
  opacity: 0; pointer-events: none;
}
.mk-js .brandband .mqpause:focus,
.mk-js .brandband:hover .mqpause {
  opacity: 1; pointer-events: auto;
}
@media (prefers-reduced-motion: no-preference) {
  .mk-js .brandband .track { animation: mk-marquee 30s linear infinite; }
  .brandband:hover .track,
  .brandband:focus-within .track,
  .brandband.is-paused .track { animation-play-state: paused; }
}
@keyframes mk-marquee { to { transform: translateX(-50%); } }

/* The split-flap digits, on features only: small mono cards with the
   midline, ticked by the script as the count runs. The plain number
   stays in the markup; this is what it turns into when script and
   motion are both on the table. */
.flaps { display: inline-flex; gap: 2px; vertical-align: -0.12em; margin: 0 1px; }
.flaps .flap {
  display: inline-grid; place-items: center;
  width: 1.15em; height: 1.5em;
  background: var(--accent); color: var(--accent-ink); border-radius: 0;
  font-family: var(--mono); font-size: .82em; font-weight: 700; line-height: 1;
  box-shadow: inset 0 calc(50% - 1px) 0 rgba(0, 0, 0, .9), inset 0 calc(50% + 1px) 0 rgba(255, 255, 255, .14);
}
.flaps .flap::after {
  content: ""; position: absolute; left: 0; right: 0; top: 50%; height: 1px;
  background: rgba(0, 0, 0, .65);
}
.mk-js .flaps .flap { position: relative; }
.mk-js .flaps .flap.tick { animation: mk-flap .28s var(--calm); }
@keyframes mk-flap { 0% { transform: rotateX(90deg); } 100% { transform: rotateX(0); } }

/* The orbit: a slow signal arc behind the two buttons a decision lives on
   (the signup submit and the wall's Pay). Fixed-px spread, because a
   percentage inset resolves x against width and a wide button flings
   the arc. Outside no-preference the buttons are simply their plain
   accent fill, which is the fallback the plan asks for. */
@media (prefers-reduced-motion: no-preference) {
  .sell .acctform .acctbtns button[type="submit"],
  #wallPay { position: relative; }
  .sell .acctform .acctbtns button[type="submit"]::before,
  #wallPay::before {
    content: ""; position: absolute; z-index: -1;
    inset: -14px -22px;
    background: conic-gradient(transparent 0 70%, var(--accent) 85%, transparent 100%);
    border-radius: 50%; filter: blur(9px); opacity: .45;
    animation: mk-orbit 5s linear infinite;
  }
}
@keyframes mk-orbit { to { transform: rotate(1turn); } }

/* The skeleton, on the quota counter only: while the first /v1/quota is
   in flight the row wears a sized placeholder instead of popping in
   from nothing. Token-colored, so the dark room gets the same sweep in
   its own woods. */
.quota.skel {
  color: transparent;
  background: linear-gradient(100deg, var(--surface-hover) 40%, var(--surface) 50%, var(--surface-hover) 60%);
  background-size: 220% 100%;
  animation: mk-skel 1.4s linear infinite;
}
@keyframes mk-skel { from { background-position: 120% 0; } to { background-position: -100% 0; } }
@media (prefers-reduced-motion: reduce) {
  .quota.skel { animation: none; }
}

/* The pointer spotlight: a warm circle that rides the cursor on
   transform only. The class is fed by the pages' script, which only
   turns it on for fine pointers, so touch never pays for it, and never
   lands at all under reduced motion because the same script has
   already returned by then. */
html.ptr-on body::before {
  content: "";
  position: fixed; top: 0; left: 0;
  width: 640px; height: 640px; margin: -320px 0 0 -320px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(198, 141, 61, .09), rgba(198, 141, 61, 0) 70%);
  transform: translate3d(var(--mx, 50vw), var(--my, 30vh), 0);
  pointer-events: none; z-index: 0;
}


/* ── the modern face lift (2026-09-21, Tj: "very low budget at its face") ──

   The ask this section answers: the landing reads cheap next to the modern
   AI chats it copied its anatomy from. The diagnosis behind every rule
   here: what read as cheap was not the anatomy (the greeting, the pill
   composer, the chips are the references' own shape) but the FINISH --
   underlined blue links on the greeting, an orphan quota line, a ragged
   nine-chip cloud, a flat black ground with no depth, and sidebar
   controls that render as utility clutter. The fix is finish, not layout:
   one warm ambience behind the centred group, a glass surface on the
   composer, softened edges and quieter links everywhere, and sidebar
   controls that read as controls.

   Plasma UI (the vault's liquid-glass find) is the vocabulary source, not
   a dependency: it is React + WebGL, GPU-heavy, its own README says it
   does not run well on mobile, and this face must stay mobile-clean on
   the mixed hardware this audience actually owns. Its look -- light
   bending through a translucent surface over a warm ground -- is achieved
   here with backdrop-filter and color-mix over the existing tokens, so
   both themes come free and no new dependency ships.

   Nothing in the markup moves: every string, id and aria-attribute the
   suite holds is exactly where it was. Rules live at the end of the file
   so they win the cascade without !important, and each restates rather
   than relies on that luck. */

/* The ambience. One warm ember ground behind the centred group, blurred
   past banding, static (zero motion cost, so reduced-motion needs no
   answer), painted with the signal red at whisper strength and a brass
   neighbour so the ground reads as a lit room rather than a void. It is
   anchored on .col behind the whole centred group -- greeting AND composer
   -- because the glow that stops at the greeting's bottom edge reads as a
   smudge, not a light. Gated on the welcome screen: a thread is a reading
   surface, and a glow behind scrolling text is noise. */
/* The composer as glass. The ask box already wore the capsule radius and
   an ambient shadow; what it lacked was the surface the light sits ON.
   Translucent ground over the ambience, a blur that gives the glow depth
   behind the field, an inner top highlight for the light source, and a
   focus ring that lifts instead of thickening. The backdrop-filter pair
   is inside @supports: without it the color-mix ground still reads as a
   raised surface, just a opaque one. */
}
.askbox {
  background: var(--surface);
  box-shadow: 0 2px 6px rgba(0, 0, 0, .06), 0 14px 36px rgba(0, 0, 0, .09);
}
.askbox:focus-within {
  box-shadow: 0 0 0 1px var(--accent), 0 14px 36px rgba(0, 0, 0, .09);
}

/* The greeting teasers. "Why not just ask ChatGPT?" carried a bare blue
   underlined link -- the single loudest old-web tell on the face. The
   questions stay (they are the copy Tj wrote), but the links now read as
   quiet emphasis in the ink colour with a hairline underline that warms
   to signal on hover. */
.greetteaser a {
  color: var(--ink);
  text-decoration: underline;
  text-decoration-color: color-mix(in srgb, var(--ink) 34%, transparent);
  text-underline-offset: 3px;
  transition: color .15s, text-decoration-color .15s;
}
.greetteaser a:hover {
  color: var(--signal-ink);
  text-decoration-color: var(--signal-ink);
}
.greetteaser { color: var(--muted); font-size: .98rem; }

/* The quota line gets a home: a small centred badge over the composer, not
   an orphan sentence floating in the column. */
.quota {
  text-align: center; padding: 0;
  margin: 0 auto 10px; width: fit-content;
  background: var(--accent-soft); border: 1px solid var(--line);
  border-radius: 999px; padding: 4px 14px;
  color: var(--muted);
}
.quota b { color: var(--ink); }

/* The starters. Fewer pixels of border, a hover that lifts one pixel and
   warms, and a tighter row so nine pills read as one considered group
   rather than a scatter. */
.chips { gap: 9px 9px; }
.chip {
  border-color: color-mix(in srgb, var(--ink) 14%, transparent);
  background: var(--surface);
  color: var(--muted);
  transition: background .15s, color .15s, border-color .15s, transform .12s;
}
.chip:hover {
  border-color: color-mix(in srgb, var(--ink) 30%, transparent);
  background: var(--surface-hover);
  color: var(--ink);
  transform: translateY(-1px);
}

/* The sidebar. Restated here explicitly because the landing's verdict is
   decided at first glance and the column sets it: quiet surface rows, one
   radius scale, and no element brighter than the send key. The new-thread
   button keeps its surface ground (the rule further up already demoted it
   from the accent slab); what changes is the edge -- a softer radius that
   matches the composer's world instead of the square one. */
.newchat, .convsearch { border-radius: 12px; }
.convsearch {
  background: color-mix(in srgb, var(--surface) 55%, transparent);
  border-color: color-mix(in srgb, var(--ink) 12%, transparent);
}
.convsearch:focus {
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

/* The theme control rendered as a stray glyph; a circle with a hairline
   makes it a button you can see. */
.themebtn {
  background: var(--surface);
  border-color: var(--line);
  border-radius: 50%;
}
.themebtn:hover { background: var(--surface-hover); border-color: var(--line-strong); }

/* The pressed text-size block was a full paper fill -- the brightest thing
   in the column after the send key. A pressed control wants presence, not
   a shout: soft fill, accent edge, ink text. */
.sizebtns button[aria-pressed="true"] {
  background: var(--accent-soft);
  color: var(--ink);
  border-color: var(--accent);
}
/* The install link wore the plain link blue; in the sidebar's quiet column
   a muted row with the band's hover is the register every other row uses. */
.installbtn { color: var(--muted); }
.installbtn:hover { color: var(--ink); }

/* ── face lift pass 2 (2026-09-21, Tj: "looks the exact same cheapness") ──

   Pass 1 changed finish details and Tj's eyes called it the same page,
   which is the same verdict the palette passes earned and the same
   lesson: visible or it did not happen. Pass 2 changes STRUCTURE, and the
   biggest structural gap between this face and the references is the
   composer's shape: the references all build the field as a PANEL -- text
   area on top, an action row under it, one rounded container -- while
   this page drew a single-line capsule with a button glued to the end.
   Same ids, same order, one layout change; app.js touches nothing here
   but the values of the elements it already owns. */

/* The panel. Column layout: the field gets the full width and its own
   line, the action row hangs under it right-aligned, and the whole thing
   reads as the object the references draw. Radius up to match the panel
   proportion (a 26px radius on this height is the references' corner),
   the glass ground and layered shadow from pass 1 stay. */
.askbox {
  flex-direction: column; align-items: stretch; gap: 2px;
  padding: 13px 13px 11px 18px;
  border-radius: 26px;
}
.askbox textarea {
  width: 100%;
  min-height: 54px;
  font-size: max(1.08rem, 16px);
  padding: 6px 2px 10px;
}
.askacts { justify-content: flex-end; gap: 10px; padding: 0 2px 2px 0; }

/* The starters tighten into the references' compact pills: smaller type,
   smaller hit pad -- still above 44px of real target with the padding --
   and a quieter border, so nine suggestions read as one calm row set. */
.chip {
  font-size: .93rem;
  padding: 7px 14px;
  min-height: 0;
  min-width: 0;
  color: var(--muted);
}
.chip:hover { color: var(--ink); }

/* The two teasers drop a size each and pull in: they are footnotes to the
   claim, and footnotes do not set the same type size as the claim. */
.greetteaser {
  display: inline; margin: 0 10px 0 0;
  font-size: .9rem; color: var(--muted);
}
.greetteaser a {
  color: var(--ink); text-decoration: none; font-weight: 500;
}
.greetteaser a:hover { color: var(--signal-ink); text-decoration: underline; }
.greet p { font-size: 1.02rem; margin-top: 10px; }

/* The sidebar's search becomes a ghost row: no box until focus, which is
   how every reference draws a filter field inside a quiet column. */
.convsearch {
  background: transparent;
  border-color: transparent;
}
.convsearch:hover { background: var(--surface); }
.convsearch:focus {
  background: var(--surface);
  border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}

/* The panel needs the band under it quiet on a thread: nothing changes
   for the sticky composer, this comment is the boundary marker. */

/* ── the floating composer (2026-09-21, Tj: "the chatbox should float, so
   when i scroll up i can look at what the AI said and type back") ──────

   The sticky was already in the code and had silently died under pass 1's
   layering rule (fixed above); what remained was the SHAPE: a full-width
   band with a top rule reads as a footer glued to the window, not a box
   you can type into while you read. So the band goes away in a thread and
   the panel floats: the band's ground and rule are gone, the ask box
   carries its own glass + shadow + radius, and a fade fades the answer
   out behind the panel the way the references do -- text dissolves as it
   passes under instead of hitting a hard edge. */

.composer {
  background: transparent;
  border-top: 0;
}
body:has(#welcome:not([hidden])) .composer::before { content: none; }
.composer::before {
  content: ""; position: absolute; left: 0; right: 0; bottom: 100%;
  height: 64px; pointer-events: none;
  background: linear-gradient(to top, var(--canvas) 12%, transparent);
}
.askbox { margin-bottom: 12px; }

/* The wall keeps its ground: when the quota runs out the wall replaces
   the composer, and its own band styles are untouched by this pass. */

/* ── pass 6: the ChatGPT-anatomy restructure (2026-09-21, Tj's reference
   screenshot: "make audiophilegpt look like this, but with our flair and
   accessibility features and audio focused") ─────────────────────────────

   The reference's whole trick is subtraction again, one level deeper than
   pass 5: the sidebar is flat rows (no bordered boxes anywhere), the
   composer is a single-line capsule with the key at the right end, the
   suggestions are quiet text rows instead of pills, and the greeting is
   LIGHT weight -- hierarchy carried by size and air, not by boxes and
   bold. Our flair stays: warm tokens, the signal-red key, the knowledge
   base chips, the roomplan in the sidebar's system row. Every
   accessibility feature survives untouched: --tap targets, focus rings,
   the text-size control, aria labels, reduced motion. */

/* Sidebar: no more boxes. The new-thread row loses its border and ground
   entirely -- it is a hover row like every other row, with the + still in
   markup. The search stays ghost. Nothing in this column draws a box. */
.newchat {
  background: transparent;
  border-color: transparent;
  font-weight: 500;
}
.newchat:hover { background: var(--surface-hover); border-color: transparent; }

/* Greeting: light weight, bigger, plain. The reference greets at a size
   you'd expect a headline to shout at, in the weight a sentence uses. */
.greet h2 {
  font-weight: 400;
  font-size: clamp(1.9rem, 1.1rem + 2.1vw, 2.4rem);
  letter-spacing: -.01em;
}

/* The composer back to a single-line capsule: field on the left, key at
   the right end, fully rounded ends while it is one line tall. It still
   grows for multiline questions; the capsule opening just gets taller. */
.askbox {
  flex-direction: row; align-items: center; gap: 10px;
  padding: 8px 8px 8px 22px;
  border-radius: 999px;
  margin-bottom: 14px;
}
.askbox textarea {
  min-height: var(--tap);
  max-height: 9rem;
  font-size: max(1.05rem, 16px);
  padding: 6px 2px;
}

/* The starters become rows, not pills: full-width quiet lines inside a
   centred column, text left, a small signal-coloured marker out front,
   hover ground like the sidebar's rows. The marker is a CSS glyph, so the
   buttons keep their words-only accessible names. */
.chips {
  flex-direction: column; flex-wrap: nowrap; align-items: stretch;
  gap: 2px;
  width: min(100%, 30rem);
  margin-left: auto; margin-right: auto;
}
.chip {
  width: 100%; justify-content: flex-start;
  display: flex; align-items: center; gap: 10px;
  background: transparent; border-color: transparent;
  border-radius: 12px;
  padding: 10px 14px; min-height: var(--tap);
  color: var(--muted);
  font-size: 1rem;
}
.chip::before {
  content: "›"; flex: 0 0 auto;
  color: var(--signal); font-weight: 600;
  transition: transform .12s;
}
.chip:hover {
  background: var(--surface-hover);
  border-color: transparent;
  color: var(--ink);
}
.chip:hover::before { transform: translateX(2px); }

/* Air: the reference is mostly empty space, so the centred group gets a
   wider breath between greeting and box than any previous pass dared. */
.greet { margin-bottom: 34px; }
.greet p { max-width: 34rem; }

/* The upgrade affordances (2026-09-21, Tj: map the reference's Upgrade
   entries). The header link sits top right; the sidebar row sits above the
   account block. Both are words with a signal-coloured spark, both hidden
   for paid accounts by app.js on the quota refresh. */
.upgradelink {
  display: inline-flex; align-items: center; gap: 7px;
  min-height: var(--tap); padding: 6px 14px;
  color: var(--signal-ink); font-weight: 600; font-size: .95rem;
  text-decoration: none; border-radius: 999px;
  transition: background .15s;
}
header { display: flex; align-items: center; }
.upgradelink:hover { background: var(--accent-soft); }
.ugspark { color: var(--signal); font-size: .95rem; }
header .upgradelink { margin-left: auto; margin-right: 18px; }
.sideupgrade { width: 100%; margin: 0 0 2px; }
/* 2026-11-06 visual pass: with a two-line conversation title the button's
   top edge sat 6px over the list's last border - a collision that reads as
   a rendering bug. The list owns the gap. */
.convlist { margin-bottom: 10px; }

/* ── The repair guides ──────────────────────────────────────────────────── */
/* Public, indexable, light-only pages built from the knowledge base's
   documented failure rows. Same paper shell the legal pages wear, with a
   measure of column width borrowed from the selling pages' prose measure.
   The share page carries its own inline copy of this shell; these are the
   real styles, and the share page's inline block is the duplicate. */
.guidemain { max-width: 46rem; margin: 0 auto; padding: 2.5rem 1.25rem 3rem; }
.guidemain h1 { margin: 0 0 1.25rem; }
.guidemain h2 { margin: 2.25rem 0 0.75rem; }
.guidemain h3 { margin: 1.5rem 0 0.5rem; }
.guidemain p { line-height: 1.65; }
.guidebrand { font-weight: 700; margin: 0 0 1.5rem; }
.guidebrand a { text-decoration: none; color: inherit; }
.guidemain .sellacts { margin: 1rem 0 0; }

/* ================== THE VIOLET AURORA (2026-10-16) ==================
   "The real app" pass. Tj set the bar by reference -- "make audiophilegpt
   feel like a real app.. how does google make gemini look so cool? that's
   what I mean" -- and picked the flavor by name from the mock: house violet
   aurora. Three slow light fields sit behind the app page; the shell above
   them (sidebar, sticky composer band) goes translucent so the light reads
   through; one signature gradient (--grad / --grad-text, tokens above)
   paints the tube, the greeting, the send key, the Upgrade buttons and the
   quota pill. Everything is scoped to html[data-app], because this one
   stylesheet also dresses the legal, signup and marketing pages and none of
   them get the aurora; the element itself lives only in index.html.

   Cost note: three blurred fields animating transform only, plus two
   backdrop-filter panels, is the whole GPU bill. No JS, no canvas, no
   second paint on scroll. Reduced motion freezes the fields in place (the
   light stays, the drift stops); print removes them and puts the canvas
   back, so a printed thread stays paper. */
html[data-app] body { background: transparent; }
.aurora {
  position: fixed; inset: 0; z-index: 0; overflow: hidden;
  pointer-events: none;
  background: var(--canvas);
}
/* PAINTED, NOT LOOPING -- and both of those words were paid for this day.
   Three findings, measured on this service's own browser suite (65 rack-page
   tests, HEAD baseline 46.7s):
     1. filter: blur(64px) on the fields: 743s and 5 streaming-test timeouts.
        A radial gradient is already a soft field; the blur bought almost
        nothing visual. Removed.
     2. The same with the blur gone but the drift running: 141s, 2 timeouts.
     3. Drift off: 46.6s, all green, identical to baseline.
   So the whole bill was the three infinite animations -- about 1.5s of CPU
   per page load in software rendering, which on a phone is battery spent on
   a 30-second cycle nobody sits and watches. The light is PAINTED: the
   composition below is the whole effect, and the page keeps its motion
   budget for things a visitor actually sees move (the greeting's sweep
   below, hover lifts, streaming). To restore the drift, re-add the three
   animations and the --aurora drift keyframes from commit history (this
   file, 2026-10-16) and re-run test_rack_page.py before shipping -- the
   suite is the instrument that caught it. */
.aurora span {
  position: absolute; display: block; border-radius: 50%;
}
.aurora .a-violet  { width: 46%; aspect-ratio: 1; top: -14%; left: 26%; background: radial-gradient(circle, var(--aurora-a) 0%, transparent 72%); }
.aurora .a-magenta { width: 40%; aspect-ratio: 1; top: 0; right: -10%; background: radial-gradient(circle, var(--aurora-b) 0%, transparent 72%); }
.aurora .a-amber   { width: 34%; aspect-ratio: 1; bottom: -20%; left: 10%; background: radial-gradient(circle, var(--aurora-c) 0%, transparent 72%); }
@media (max-width: 700px) {
  /* Wider fields on a phone, where the viewport is tall and narrow. */
  .aurora .a-violet { width: 72%; left: 6%; }
  .aurora .a-magenta { width: 64%; }
  .aurora .a-amber { width: 58%; }
}
@media (prefers-reduced-motion: reduce) {
  .greet h2 .sig { animation: none; }
}
@media print {
  .aurora { display: none; }
  html[data-app] body { background: var(--canvas); }
}

/* The shell rides above the light. .app is the page's one wrapper, so a
   single rule puts every plane over the aurora; the sticky composer keeps
   its own z-index inside it. */
html[data-app] .app { position: relative; z-index: 1; }

/* The sidebar and the composer's in-thread band go translucent so the light
   reads through, which is most of what makes the aurora feel like a room
   instead of wallpaper. The text pairs were measured against these grounds
   (worst 7.6:1 muted) and a 90% mix over low-opacity light moves them by
   well under a point. The landing composer is already transparent (the
   :has(#welcome) rule above outranks this one), so only the thread band
   changes.

   NO backdrop-filter here, deliberately (2026-10-16): a backdrop-blurred
   panel whose backdrop is a permanently animating layer makes the browser
   re-blur the panel every frame forever, and it bought nothing visually
   because the thing behind it is already a soft light field. The translucent
   mix alone carries the effect. (The bulk of the paint cost measured that
   day was the aurora's own blur() filter, removed above; this pairing was
   the second, smaller term, and it stays out.) */
html[data-app] .sidebar {
  background: color-mix(in srgb, var(--canvas-deep) 90%, transparent);
}
html[data-app] .composer {
  background: color-mix(in srgb, var(--canvas-deep) 90%, transparent);
}
/* 2026-11-06 visual pass: in LIGHT mode the 90% ground let answer text
   ghost through the sticky composer (a clipped line read as a typo:
   "shorted" arriving as "shoted"). Dark hides it behind the dim aurora, so
   only light goes opaque - and opaque beats blur here on purpose: the
   aurora lessons already paid for what filter costs on this page. */
html[data-theme="light"] .composer { background: var(--canvas-deep); }
@media (prefers-color-scheme: light) {
  html:not([data-theme]) .composer { background: var(--canvas-deep); }
}

/* The header mark's wave, painted by the theme's own gradient. The defs
   live inside the h1's SVG in index.html and the wave path carries
   stroke="url(#agTube)" inline, so every page that inlines the lockup
   paints it; the tube and base stay currentColor and only the wave
   graduates (2026-10-17 mark). The stop classes follow the theme, which is
   the whole reason they are classes and not hexes. */
#agTube .s1 { stop-color: var(--stop-a); }
#agTube .s2 { stop-color: var(--stop-b); }

/* The greeting's turn of phrase wears the gradient, alive: a nine-second
   sweep, slower than anything else on the page. This is the one place the
   app spends colour on type, and the ramp is the readable cut. */
.greet h2 .sig {
  background: var(--grad-text);
  background-size: 220% 100%;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: sigsweep 9s ease-in-out infinite alternate;
}
@keyframes sigsweep { from { background-position: 0% 50%; } to { background-position: 100% 50%; } }
/* Two guards, because transparent text with no clip support is invisible
   text: an old engine gets the link colour, forced colours get the system
   colour and no animation. */
@supports not ((-webkit-background-clip: text) or (background-clip: text)) {
  .greet h2 .sig { color: var(--link); background: none; }
}
@media (forced-colors: active) {
  .greet h2 .sig { color: CanvasText; background: none; animation: none; }
}

/* The send key spends the gradient: the one saturated moment on the landing
   (the note above button.send says why the colour budget lands here), now
   the brand's full ramp with a slow drift on hover. */
html[data-app] button.send {
  background: var(--grad);
  background-size: 180% 100%;
  border-color: transparent;
}
html[data-app] button.send:hover {
  background: var(--grad);
  background-size: 180% 100%;
  background-position: 100% 50%;
  border-color: transparent;
  box-shadow: 0 10px 26px color-mix(in srgb, var(--stop-b) 40%, transparent);
}

/* The Upgrade buttons are shoelace (sl-button variant="primary"), so the
   gradient goes through the kit's own part rather than fighting it. */
html[data-app] .upgradelink::part(base) {
  background: var(--grad);
  border: 0;
}
html[data-app] .upgradelink:hover::part(base) { filter: brightness(1.06); }

/* The quota pill keeps its shape and gets the gradient edge: the double
   background is the standard two-layer trick (padding-box fill over a
   border-box ramp). The fill has to be OPAQUE -- the first cut reused the
   pill's own --accent-soft, which is 10% alpha, so the bright ramp showed
   straight through the middle and the text sat on it (caught on the live
   page, same hour). color-mix against --surface gives an opaque tint that
   lands within a shade of the old ground, and muted-on-surface is a pair
   this palette already measures. The .low and .skel states are left alone:
   a warning that wears brand colour is a warning nobody reads. */
html[data-app] .quota:not(.low):not(.skel) {
  border: 1.5px solid transparent;
  background-image: linear-gradient(color-mix(in srgb, var(--surface) 90%, var(--stop-a)), color-mix(in srgb, var(--surface) 90%, var(--stop-a))), var(--grad);
  background-origin: border-box;
  background-clip: padding-box, border-box;
}

/* The composer's focus state keeps its measured ring and picks up the
   gradient's warmth in the ambient shadow: the box reads as lit, not just
   outlined. Restates both shadows for the reason the original does. */
html[data-app] .askbox:focus-within {
  box-shadow: 0 0 0 1px var(--accent), 0 10px 34px color-mix(in srgb, var(--stop-b) 26%, transparent);
}

/* ---------- the chat view, same pass (2026-10-16) ----------
   Two touches, both small on purpose: a thread is a reading surface and
   should stay one. */
/* The wait sweep wears the signature, in its violet-and-magenta half. Same
   sheen geometry the 09-17 lift shipped (a transform-only pseudo inside the
   bubble, so the suite's `.bubble.thinking` box and the Copy text are
   untouched), now in brand colours, so the pause and the front door agree
   about where the colour lives. Small area, unchanged animation: measured
   flat in the rack suite.

   NO amber leg here, and that is a correction caught on screen (2026-10-16):
   amber at sweep alpha over a dark ground reads MUDDY BROWN -- the exact
   register the 09-20 palette pass was rejected for ("ugly brown/green
   shit"), and on the light ground it reads as a peach smear. The amber keeps
   its jobs where it is at full chroma (the byline dot, the send key, the
   gradient itself) and sits this one out. */
html[data-app] .msg.ai .bubble.thinking::after {
  background: linear-gradient(100deg, transparent 30%,
    color-mix(in srgb, var(--stop-a) 10%, transparent) 44%,
    color-mix(in srgb, var(--stop-b) 14%, transparent) 50%,
    color-mix(in srgb, var(--stop-a) 8%, transparent) 57%,
    transparent 70%);
}
/* The answer's byline gets the mark's own dot: it is the one place in a
   thread where the product names itself, so the one place the signature
   earns a pixel. Purely decorative, no motion. */
html[data-app] .msg.ai .who::before {
  content: ""; display: inline-block;
  width: .52em; height: .52em; border-radius: 50%;
  margin-right: .45em; vertical-align: .02em;
  background: var(--grad);
}

/* The Pro seat's photo note (2026-10-16): said under the photo preview while
   Pro is selected, because the Pro model has no vision and that question
   answers at Deep Thinking. Quiet, the statusline's register rather than the
   refusal block's: this is information, not an error. */
.prophotonote { margin: 8px 2px 0; font-size: .9rem; color: var(--muted); }
.prophotonote[hidden] { display: none; }
