/* ============================================================================
   Components. Every value here comes from tokens.css.

   Written with CSS logical properties throughout (inline-start rather than
   left, padding-inline rather than padding-left). The page is Arabic and runs
   right to left, but the bot answers in the language it was asked in, so an
   English answer flips direction on its own with no separate stylesheet.
   ========================================================================= */

/* --- Reset ------------------------------------------------------------- */

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

html {
  -webkit-text-size-adjust: 100%;
  /* No global scroll-behavior. It applied to every programmatic scroll in the
     app, including the two that need to be instant: following a stream, which
     restarted a smooth scroll toward a moving target on every chunk and
     produced a constant drift, and reopening a saved conversation, which
     animated a long scroll through content built one frame earlier. The three
     places that do want smoothness ask for it explicitly. */

  /* Reserved always, so that locking the page under the drawer does not take
     the scrollbar's width away with it. Without this the whole layout jumps
     sideways at the moment the drawer opens, and jumps back as it closes. */
  scrollbar-gutter: stable;
}

@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* --- Motion primitives --------------------------------------------------

   Three keyframes carry every entrance in the interface. Sharing them is what
   keeps a page of separately-authored sections feeling like one object that
   arrived, rather than a dozen elements each doing their own thing.

   Components name the keyframes themselves; there is no `.enter` utility to
   sprinkle on. There was one, and nothing had used it since the entrances
   moved onto the components — a class that means "animate" tells a later
   reader nothing about what is arriving or why.

   `--enter-delay` is the hook that remains: app.js sets it per item to stagger
   a rendered group. Every entrance is `both`-filled, so an element with a
   delay stays invisible until its turn instead of flashing first. */

@keyframes rise {
  from {
    opacity: 0;
    transform: translateY(var(--rise));
  }
}

@keyframes fade {
  from {
    opacity: 0;
  }
}

/* Used only where a surface should appear to grow out of the control that
   opened it — the palette, the sync sheet. */
@keyframes emerge {
  from {
    opacity: 0;
    transform: translateY(var(--rise)) scale(0.985);
  }
}

body {
  font-family: var(--font-body);
  font-size: var(--text-base);
  line-height: var(--leading-body);
  color: var(--text);
  /* Two layers. The linear ramp does the grading; the radial is a pool of
     warmth behind the hero, which is how gold gets onto the landing page as
     *light* rather than as paint. A flat top-to-bottom wash across 15 RGB
     levels — what this was — is below the threshold at which an eye reads a
     gradient at all, so it cost a fixed-attachment repaint to render something
     indistinguishable from a flat fill.

     Fixed attachment keeps both anchored to the viewport, so they do not
     stretch and re-render as the conversation grows. */
  background:
    radial-gradient(
      120% 78% at 50% -8%,
      color-mix(in oklab, var(--gold) 7%, var(--bg-top)) 0%,
      transparent 62%
    ),
    linear-gradient(180deg, var(--bg-top) 0%, var(--bg-mid) 50%, var(--bg-bottom) 100%);
  background-attachment: fixed;
  min-height: 100dvh;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

img,
svg {
  display: block;
  max-width: 100%;
}

/* A fixed grain layer over the whole page. Fractal noise generated inline, so
   it costs no request and no image file.

   The noise is used as a MASK over a flat colour rather than painted directly.
   Painting it directly means tinting the page with whatever colours
   feTurbulence happens to emit — and raw fractal noise is chromatic, so the
   page picked up red and green speckles. As a mask, the noise decides only
   *where* the layer shows; --grain decides what colour and how strongly, and
   its alpha is what keeps the effect at the edge of perception.

   The filter desaturates before it is used, so the mask varies in luminance
   alone and the texture is even rather than blotchy. */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: var(--grain);
  -webkit-mask-image: var(--grain-noise);
  mask-image: var(--grain-noise);
  -webkit-mask-size: 160px 160px;
  mask-size: 160px 160px;
}

/* Everything real sits above the grain. */
.shell,
.palette {
  position: relative;
  z-index: 1;
}

/* Numbers that sit in columns — counts, prices, ranks — need equal widths or
   they shimmer as they change. */
.category__count,
.pick__price,
.pick__rank,
.section__meta,
.composer__note,
kbd {
  font-variant-numeric: tabular-nums;
}

::selection {
  background: var(--gold-dim);
  color: var(--text);
}

/* The default scrollbar is a piece of the operating system sitting inside a
   carefully made page. Worth restyling where the browser allows it. */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--border-strong) transparent;
}

button,
input,
textarea {
  font: inherit;
  color: inherit;
}

button {
  background: none;
  border: none;
  cursor: pointer;
}

a {
  color: inherit;
}

/* Drawn as an outline rather than a box-shadow, and that is the whole point.

   A box-shadow ring is a single property, so any component that declares its
   own box-shadow silently replaces it — same specificity, later rule wins. That
   is not hypothetical: the primary call to action and all eight category cards
   were focusable with no visible ring at all, because `.cta` and `.category`
   each set a shadow of their own further down this file. An outline occupies a
   property nothing else on the page uses, so it cannot be clobbered by a
   surface effect, and it follows the border radius on its own.

   No border-radius here either. The rule used to force one on focus, which
   squared off the corners of anything that had not restated its own radius
   later in the file — a shape change is not focus feedback. */
:focus-visible {
  outline: 2px solid var(--gold-deep);
  outline-offset: 2px;
}

/* Screen-reader-only: present to assistive technology, invisible on screen. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* Inline icons are drawn, not filled.
   Declared once here rather than repeated as fill/stroke presentation
   attributes on every glyph in the markup — which is how the header's
   magnifier and moon came to render as solid black blobs the moment those
   attributes were dropped from one of them. The one shape that is genuinely
   filled, the stop square in the send button, carries fill on the shape
   itself, which this does not touch. */
svg {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}

/* --- Layout ------------------------------------------------------------ */

.shell {
  display: flex;
  flex-direction: column;
  min-height: 100dvh;
}

.wrap {
  width: 100%;
  max-width: var(--width-wide);
  margin-inline: auto;
  padding-inline: var(--space-4);
}

.wrap--narrow {
  max-width: var(--width-content);
}

@media (min-width: 640px) {
  .wrap {
    padding-inline: var(--space-5);
  }
}

/* --- Header ------------------------------------------------------------ */

.header {
  position: sticky;
  inset-block-start: 0;
  z-index: var(--z-header);
  display: flex;
  align-items: center;
  /* Mobile-first: the nav takes its own line below the brand, so the header
     grows rather than squeezing four things onto one crowded row. It becomes
     a single fixed-height row once there is width for it. */
  min-block-size: var(--header-height);
  padding-block: var(--space-2);
  background: color-mix(in srgb, var(--bg-top) 82%, transparent);
  backdrop-filter: blur(16px) saturate(1.4);
}

@media (min-width: 720px) {
  .header {
    block-size: var(--header-height);
    padding-block: 0;
  }
}

/* Drawn rather than bordered: a rule that fades at both ends sits on the page
   instead of boxing it. */
.header::after {
  content: '';
  position: absolute;
  inset-block-end: 0;
  inset-inline: 0;
  block-size: 1px;
  /* Gold, not grey. This is the only continuous line on the page and it sits
     directly under the wordmark; drawing it in --border-strong at 0.6 put a
     mid-grey rule under a gold brand. */
  background: linear-gradient(
    to right,
    transparent,
    var(--gold-edge) 22%,
    var(--gold-edge) 78%,
    transparent
  );
  opacity: 0.55;
}

.header__inner {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: var(--space-4);
  row-gap: var(--space-1);
  inline-size: 100%;
}

@media (min-width: 720px) {
  .header__inner {
    flex-wrap: nowrap;
  }
}

.brand {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-family: var(--font-display);
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
  text-decoration: none;
  white-space: nowrap;
}

/* A single gold mark, the only piece of pure decoration on the page. */
.brand__mark {
  inline-size: 0.5rem;
  block-size: 0.5rem;
  border-radius: var(--radius-pill);
  background: var(--gold);
  flex-shrink: 0;
}

/* --- Navigation ---------------------------------------------------------

   Typographic, not buttoned. A row of pills in the header would compete with
   the mode tabs directly below it and make the page look like it has two sets
   of controls doing the same job.

   The underline grows from the start edge on hover, which is why it animates
   `inline-size` rather than a transform: `transform-origin` has no logical
   keyword, so a scaleX version needs one rule for RTL and another for LTR that
   have to be kept in sync. On a one-pixel rule the cheaper property is not
   worth two rules that can drift apart.
   ----------------------------------------------------------------------- */

.nav {
  display: none;
  align-items: center;
  gap: var(--space-5);
  white-space: nowrap;
}

/* Below this the hamburger carries exactly the same destinations, so the
   header stays a single row on a phone instead of growing a second one. */
@media (min-width: 60rem) {
  .nav {
    display: flex;
  }
}

/* On the chat screen the drawer carries navigation. A second copy of it in the
   header would be two answers to the same question. */
:root[data-view='chat'] .nav {
  display: none;
}

.nav__link {
  position: relative;
  padding-block: var(--space-2);
  color: var(--text-muted);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  transition: color var(--duration-base) var(--ease);
}

.nav__link::after {
  content: '';
  position: absolute;
  inset-block-end: 0;
  inset-inline-start: 0;
  block-size: 1px;
  inline-size: 0;
  background: var(--gold);
  transition: inline-size var(--duration-base) var(--ease);
}

.nav__link[aria-current='true'] {
  color: var(--text);
}

.nav__link[aria-current='true']::after {
  inline-size: 100%;
}

.header__spacer {
  flex: 1;
}

.header__actions {
  display: flex;
  align-items: center;
  gap: var(--space-1);
}

.icon-button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: var(--control-height);
  block-size: var(--control-height);
  border-radius: var(--radius-md);
  color: var(--text-muted);
  transition:
    color var(--duration-fast) var(--ease),
    background-color var(--duration-fast) var(--ease);
}

@media (hover: hover) {
  .icon-button:hover {
    color: var(--text);
    background: var(--surface);
  }
}

/* The theme control's two glyphs. Dark is the default and needs no attribute,
   so the sun is the resting state and the moon is the exception — the same
   asymmetry tokens.css declares for the palette itself. Each glyph shows the
   theme the click leads to, not the one you are in. */
.icon-moon {
  display: none;
}

:root[data-theme='light'] .icon-sun {
  display: none;
}

:root[data-theme='light'] .icon-moon {
  display: block;
}

/* The keyboard hint is meaningless without a keyboard. */
.header__hint {
  display: none;
  align-items: center;
  gap: var(--space-2);
  padding-inline: var(--space-3);
  block-size: var(--control-height);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-md);
  color: var(--text-faint);
  font-size: var(--text-xs);
}

@media (min-width: 900px) {
  .header__hint {
    display: inline-flex;
  }
}

kbd {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--text-muted);
}

/* --- Main -------------------------------------------------------------- */

.main {
  flex: 1;
  padding-block: var(--space-6) var(--space-7);
}

@media (min-width: 640px) {
  .main {
    padding-block: var(--space-8) var(--space-9);
  }
}

/* --- Hero -------------------------------------------------------------- */

.hero {
  text-align: center;
  margin-block-end: var(--space-7);
}

.hero__title {
  font-family: var(--font-display);
  font-size: var(--text-xl);
  line-height: 1.15;
  font-weight: var(--weight-semibold);
  letter-spacing: -0.02em;
  text-wrap: balance;
}

@media (min-width: 640px) {
  .hero__title {
    font-size: var(--text-2xl);
  }
}

.hero__lead {
  margin-block-start: var(--space-3);
  margin-inline: auto;
  max-inline-size: 34rem;
  color: var(--text-muted);
  font-size: var(--text-md);
  line-height: var(--leading-snug);
  text-wrap: pretty;
}

/* --- Mode tabs --------------------------------------------------------- */

.modes {
  display: flex;
  gap: var(--space-1);
  margin-block-end: var(--space-3);
  padding: var(--space-1);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface-sunken);
  /* Four tabs do not fit on a narrow phone; scrolling beats wrapping into two
     rows that shift the composer down. */
  overflow-x: auto;
  /* Contained, so running out of tabs does not pass the gesture to the browser,
     where a horizontal overscroll is the back navigation. */
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  position: relative;
}

/* One indicator that slides, rather than a background that disappears from one
   tab and reappears on another. The selected state used to be painted on the
   tab itself, so switching had no continuity — which is why a strip that did
   technically have a transition still felt dead.

   Positioned with physical `left` and translateX on purpose: the offsets come
   from offsetLeft, which is measured from the left edge in both writing
   directions. Anchoring logically and translating physically would flip one of
   the two and send the indicator off the wrong end in RTL. */
.modes__thumb {
  position: absolute;
  top: var(--space-1);
  bottom: var(--space-1);
  left: 0;
  inline-size: var(--thumb-w, 0);
  transform: translateX(var(--thumb-x, 0));
  border-radius: var(--radius-md);
  background: var(--surface-raised);
  box-shadow:
    inset 0 0 0 var(--border-width) var(--gold-edge),
    var(--rim);
  transition:
    transform var(--duration-base) var(--ease),
    inline-size var(--duration-base) var(--ease);
  pointer-events: none;
}

/* Until it has been measured it has no width, and a zero-width box with a
   border still draws a sliver. */
.modes__thumb:not([style*='--thumb-w']) {
  opacity: 0;
}

.modes::-webkit-scrollbar {
  display: none;
}

.mode {
  flex: 1 0 auto;
  /* Above the sliding indicator, which is painted behind the whole strip. */
  position: relative;
  padding: var(--space-2) var(--space-4);
  border-radius: var(--radius-md);
  color: var(--text-muted);
  font-size: var(--text-sm);
  white-space: nowrap;
  transition:
    color var(--duration-fast) var(--ease),
    background-color var(--duration-fast) var(--ease);
}

@media (hover: hover) {
  .mode:hover {
    color: var(--text);
  }
}

/* The fill and the ring now belong to .modes__thumb; the tab keeps only the
   colour change, which is the part that should not slide. */
.mode[aria-selected='true'] {
  color: var(--text);
}

/* --- Composer ---------------------------------------------------------- */

.composer {
  box-shadow: var(--rim), var(--shadow-raised);
  position: relative;
  display: flex;
  align-items: flex-end;
  gap: var(--space-2);
  padding: var(--space-2);
  border: var(--border-width) solid var(--border-strong);
  border-radius: var(--radius-lg);
  background: var(--surface);
  transition:
    border-color var(--duration-base) var(--ease),
    box-shadow var(--duration-base) var(--ease);
}

/* Focus lights the edge from inside rather than drawing a ring around it —
   the control gains attention without gaining a second outline. */
.composer:focus-within {
  border-color: var(--gold-edge);
  box-shadow:
    inset 0 1px 0 var(--edge-highlight),
    0 0 0 3px var(--gold-dim);
}

.composer__input {
  flex: 1;
  resize: none;
  border: none;
  background: none;
  padding: var(--space-2) var(--space-2);
  font-size: var(--text-base);
  line-height: var(--leading-snug);
  max-block-size: 12rem;
  field-sizing: content;
}

/* An empty field is one row, whatever its placeholder happens to say.

   `field-sizing: content` sizes an empty textarea from its placeholder, and
   the typed placeholder rewrites that string twenty times a second. At 390px
   one of the cycled lines wraps to two rows, so the composer — and with it the
   hint and the whole mode tablist above it — rose and fell 22.5px twice a
   minute with nobody touching anything, moving live tap targets under a thumb
   already reaching for them.

   `:placeholder-shown` matches only while the value is empty, so this pins the
   resting height and nothing else: the first typed character stops it matching
   and the field goes back to growing with what is actually in it. */
.composer__input:placeholder-shown {
  block-size: calc(var(--leading-snug) * var(--text-base) + 2 * var(--space-2));
}

.composer__input:focus {
  outline: none;
}

.composer__input::placeholder {
  color: var(--text-faint);
}

.send {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: var(--control-height);
  block-size: var(--control-height);
  flex-shrink: 0;
  border-radius: var(--radius-md);
  background: var(--gold);
  color: var(--on-gold);
  transition:
    opacity var(--duration-base) var(--ease),
    transform var(--duration-base) var(--ease),
    background-color var(--duration-base) var(--ease),
    color var(--duration-base) var(--ease),
    box-shadow var(--duration-base) var(--ease);
}

@media (hover: hover) {
  .send:hover:not(:disabled) {
    opacity: 0.88;
  }
}

/* Press. The transform was already here; what was missing was any of the four
   controls declaring `transform` in its transition list, so every press on the
   page was a 1px teleport down and a 1px teleport back. */
.send:active:not(:disabled),
.chip:active,
.category:active,
.pick__action:active {
  transform: translateY(1px);
  transition-duration: var(--duration-instant);
}

/* Dimmed by mixing toward the surface, not by fading toward it — the same
   correction --gold-dim exists to carry, applied here too late.

   `opacity: 0.35` over the composer fill rendered rgb(96, 86, 70): olive mud,
   the exact colour the token comment warns about. Worse, opacity fades the
   glyph along with the fill, so the arrow sank into its own button — in light
   mode it was white on near-white and effectively gone. Mixing moves lightness
   and leaves hue alone, and the two colours stay independent, so the arrow
   keeps its contrast. */
.send:disabled {
  background: var(--gold-dim);
  color: var(--gold-deep);
  cursor: not-allowed;
}

/* Both icons occupy the same box, so swapping one for the other cannot shift
   the button's size or the layout around it. */
.send__icon {
  inline-size: 18px;
  block-size: 18px;
}

/* The arrow points along the reading direction, whichever way that runs. The
   stop square has no direction, so only the arrow flips. */
.send__icon--send {
  transform: scaleX(-1);
}

:root:dir(ltr) .send__icon--send {
  transform: none;
}

/* Exactly one icon is present at a time; the button is a fixed size either
   way, so nothing shifts as it swaps. */
.send__icon--stop,
.send[data-streaming] .send__icon--send {
  display: none;
}

.send[data-streaming] .send__icon--stop {
  display: block;
}

/* Stopping is not the primary action, so it does not get the gold. Keeping it
   in the same position matters more than making it stand out — the control
   under your finger should not move when its meaning changes. */
.send[data-streaming] {
  background: var(--surface-sunken);
  color: var(--text-muted);
  box-shadow: inset 0 0 0 var(--border-width) var(--border-strong);
}

@media (hover: hover) {
  .send[data-streaming]:hover {
    color: var(--text);
    opacity: 1;
  }
}

.composer__note {
  display: flex;
  justify-content: center;
  gap: var(--space-3);
  margin-block-start: var(--space-2);
  color: var(--text-faint);
  font-size: var(--text-xs);
  text-align: center;
}

/* --- Composer dock -----------------------------------------------------

   Sticky rather than fixed: it sits in the flow under the hero on a fresh
   page, and pins to the bottom once the conversation is long enough to scroll.
   Fixed would float it over an empty page from the first paint.
   ----------------------------------------------------------------------- */

/* Pinned to the viewport floor at all times, so the composer is reachable from
   anywhere on the page without scrolling back to find it.

   Fixed rather than sticky, and that distinction is the whole reason this
   works. A bottom-sticky box on a page shorter than the viewport gets pushed
   *down* from its flow position — which left a composer-shaped hole in the
   middle of the landing page and painted an opaque band across the category
   grid. Fixed leaves the flow entirely, so there is no slot to vacate; the
   space it would have occupied is given back deliberately by --dock-height
   below, which is measured rather than guessed because the textarea grows. */
.dock {
  position: fixed;
  inset-inline: 0;
  inset-block-end: 0;
  z-index: var(--z-dock);
  padding-block: var(--space-3);
  /* Clear of the home indicator on a phone, and clear of the on-screen
     keyboard on platforms that overlay it rather than resizing the page. Both
     are zero on a desktop. */
  padding-block-end: calc(var(--space-3) + env(safe-area-inset-bottom, 0px));
  transform: translateY(calc(-1 * var(--keyboard-inset)));
  /* Fading to the page colour rather than a hard edge keeps a long answer from
     appearing to be cut off by a bar. */
  background: linear-gradient(to top, var(--bg-bottom) 0%, var(--bg-bottom) 62%, transparent 100%);
}

/* The composer belongs to the conversation and to nothing else. A landing page
   with a chat box welded to the bottom of it is two products wearing one page,
   and it was also the reason the landing page had a composer-shaped band of
   reserved space across the middle of it. */
.dock {
  display: none;
}

:root[data-view='chat'] .dock {
  display: block;
}

/* No footer under a conversation. The composer is pinned to the floor, so a
   footer below it meant scrolling to the end of an answer and finding a band
   of reserved space and a pricing disclaimer between the last sentence and the
   input. A conversation ends where the composer begins. */
:root[data-view='chat'] .footer {
  display: none;
}

/* With the footer gone, the clearance for the pinned composer has to come from
   the thread instead, or the last message hides underneath it. */
:root[data-view='chat'] .main {
  padding-block-end: calc(var(--dock-height, 7rem) + var(--space-5));
}

/* One panel at a time, chosen by the router. Which one is visible is the
   `hidden` attribute's job — see setView() in app.js — so all these do is
   declare that a panel is a block when it is not hidden. */
.view,
.landing,
.chat,
.tools,
.news {
  display: block;
}

/* --- Icon buttons with a label ------------------------------------------ */

.icon-button--text {
  inline-size: auto;
  padding-inline: var(--space-2);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  letter-spacing: 0.02em;
}

/* --- Saved conversations ------------------------------------------------ */

.convos {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  list-style: none;
}

.convo {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  border: var(--border-width) solid transparent;
  border-radius: var(--radius-md);
  transition:
    border-color var(--duration-fast) var(--ease),
    background-color var(--duration-fast) var(--ease);
}

@media (hover: hover) {
  .convo:hover {
    background: var(--surface);
    border-color: var(--border);
  }
}

.convo--active {
  background: var(--surface);
  border-color: var(--gold-edge);
}

.convo__open {
  flex: 1;
  min-inline-size: 0;
  padding: var(--space-2) var(--space-3);
  color: var(--text-muted);
  font-size: var(--text-sm);
  text-align: start;
  /* One line: the title is the opening question, and letting it wrap turns a
     tidy list into a wall. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.convo--active .convo__open {
  color: var(--text);
}

.convo__delete {
  flex-shrink: 0;
  inline-size: 1.75rem;
  block-size: 1.75rem;
  margin-inline-end: var(--space-1);
  border-radius: var(--radius-sm);
  color: var(--text-faint);
  font-size: var(--text-md);
  line-height: 1;
  /* Hidden until the row is hovered or focused, so a list of chats is a list
     of chats rather than a row of delete buttons. */
  opacity: 0;
  transition:
    opacity var(--duration-fast) var(--ease),
    color var(--duration-fast) var(--ease);
}

.convo__delete:focus-visible {
  opacity: 1;
}

@media (hover: hover) {
  .convo__delete:hover {
    color: var(--danger);
  }
}

/* Hover is not available on a touch screen, so there the control is always
   visible rather than unreachable. */
@media (hover: none) {
  .convo__delete {
    opacity: 1;
  }
}

/* --- Suggestion chips -------------------------------------------------- */

.suggestions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: var(--space-2);
  margin-block-start: var(--space-4);
}

.chip {
  padding: var(--space-2) var(--space-4);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--surface);
  color: var(--text-muted);
  font-size: var(--text-sm);
  transition:
    color var(--duration-fast) var(--ease),
    transform var(--duration-base) var(--ease),
    background-color var(--duration-fast) var(--ease),
    border-color var(--duration-fast) var(--ease);
}

@media (hover: hover) {
  .chip:hover {
    color: var(--text);
    border-color: var(--border-strong);
  }
}

/* --- Category directory ------------------------------------------------ */

/* Section separation is one step down from where it was. Every break on the
   page used --space-8 regardless of how related the two sections were, so a
   group of chips that belongs to the composer sat as far from it as the
   directory did. Relatedness is what the gap is supposed to encode. */
.section {
  margin-block-start: var(--space-7);
}

.section__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-4);
  margin-block-end: var(--space-4);
}

/* A short gold lead-in rule on the start edge, the same idiom the bot's answers
   already use. It marks where a section begins without a heavier heading, and
   it flips sides with the writing direction on its own. */
.section__title {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
}

.section__title::before {
  content: '';
  inline-size: var(--space-5);
  block-size: 2px;
  background: var(--gold);
  /* Baseline alignment would drop a 2px bar onto the text baseline; this lifts
     it to the optical centre of the line instead. */
  align-self: center;
  flex-shrink: 0;
}

.section__meta {
  color: var(--text-faint);
  font-size: var(--text-sm);
}

/* Explicit column counts rather than auto-fill.

   auto-fill picks whatever number of tracks the width allows, and with eight
   cards that produced a ragged final row at most widths — three cards in a
   four-track row, and at one breakpoint a row two-thirds empty. The counts
   below are the ones that divide eight exactly, and the three-column case is
   skipped for that reason. A directory whose last row is a hole reads as
   unfinished no matter how good the cards are. */
.grid {
  display: grid;
  gap: var(--space-3);
  grid-template-columns: 1fr;
}

@media (min-width: 40rem) {
  .grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (min-width: 64rem) {
  .grid {
    grid-template-columns: repeat(4, 1fr);
  }
}

/* The way into the full list, so it reads as a destination rather than as an
   eighth category: gold text, and a dashed edge that says "not a category". */
.category--all {
  color: var(--gold);
  border-style: dashed;
  border-color: var(--gold-edge);
  background: none;
  box-shadow: none;
}

.category--all .category__name {
  color: inherit;
}

@media (hover: hover) {
  .category--all:hover {
    background: var(--gold-dim);
    border-style: solid;
  }
}

.category {
  box-shadow: var(--rim), var(--shadow-raised);
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  padding: var(--space-3) var(--space-4);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
  text-align: start;
  /* The body line-height is tuned for running Arabic prose and is far too
     loose for a two-line card, which would otherwise be mostly empty space. */
  line-height: var(--leading-tight);
  position: relative;
  overflow: hidden;
  /* A variable rather than a literal, so the hover and press states each set
     one value instead of restating the whole transform. */
  --lift: 0px;
  transform: translateY(var(--lift));
  transition:
    border-color var(--duration-base) var(--ease),
    background-color var(--duration-base) var(--ease),
    transform var(--duration-base) var(--ease),
    box-shadow var(--duration-base) var(--ease);
}

/* A gold hairline that draws up the leading edge on hover. Same idiom the
   bot's answers already use for their own rule, so it reads as house style
   rather than as a second vocabulary. */
.category::after {
  content: '';
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  inline-size: 2px;
  background: var(--gold);
  transform: scaleY(0);
  transform-origin: block-end;
  transition: transform var(--duration-base) var(--ease);
}

@media (hover: hover) {
  .category:hover {
    --lift: -2px;
    border-color: var(--gold-edge);
    background: var(--surface-raised);
    box-shadow:
      var(--rim),
      0 8px 24px -8px light-dark(rgba(60, 50, 30, 0.18), rgba(0, 0, 0, 0.6));
  }

  .category:hover::after {
    transform: scaleY(1);
  }
}

/* Pressing cancels the lift rather than adding to it, so the card meets the
   finger instead of running away from it. */
.category:active {
  --lift: 0px;
}

.category__name {
  font-size: var(--text-base);
  font-weight: var(--weight-medium);
}

.category__count {
  color: var(--text-faint);
  font-size: var(--text-xs);
}

/* --- Conversation ------------------------------------------------------

   Motion here has one job: make an answer feel like it is arriving rather than
   appearing. Everything is a short fade with a few pixels of rise — no bounce,
   no scale, no glow. A message that springs is a message you notice instead of
   read, and the global reduced-motion rule turns all of it off.
   ----------------------------------------------------------------------- */

.thread {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
}

.turn {
  animation: rise var(--duration-base) var(--ease) both;
}

/* --- The visitor's turn ------------------------------------------------ */

.turn--user {
  display: flex;
  justify-content: flex-end;
}

.turn--user .turn__body {
  box-shadow: var(--rim), var(--shadow-raised);
  max-inline-size: 85%;
  padding: var(--space-3) var(--space-4);
  border: var(--border-width) solid var(--border);
  /* The corner nearest the edge it came from stays tight, which is what makes
     a bubble read as anchored rather than floating. */
  border-radius: var(--radius-lg) var(--radius-lg) var(--radius-lg) var(--radius-sm);
  background: var(--surface);
  line-height: var(--leading-snug);
}

:root:dir(ltr) .turn--user .turn__body {
  border-radius: var(--radius-lg) var(--radius-lg) var(--radius-sm) var(--radius-lg);
}

/* --- The answer -------------------------------------------------------- */

.turn--bot .turn__body {
  position: relative;
  padding-inline-start: var(--space-5);
}

/* A hairline on the leading edge marks the answer without boxing it in, so a
   long reply reads as a document rather than a chat bubble. It draws itself
   downward as the answer starts, which reads as the answer taking shape. */
.turn--bot .turn__body::before {
  content: '';
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  inline-size: 2px;
  border-radius: var(--radius-pill);
  background: linear-gradient(to bottom, var(--gold-edge), var(--gold-dim));
  transform-origin: top;
  animation: draw 400ms var(--ease) both;
}

@keyframes draw {
  from {
    transform: scaleY(0);
  }
}

/* --- Thinking ---------------------------------------------------------- */

.turn__status {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  color: var(--text-faint);
  font-size: var(--text-sm);
}

.turn__dots {
  display: inline-flex;
  gap: 3px;
}

.turn__dots i {
  inline-size: 4px;
  block-size: 4px;
  border-radius: var(--radius-pill);
  background: var(--gold);
  animation: blink 1.2s var(--ease) infinite;
}

/* Offsets rather than one pulse: three dots moving together is a throbbing
   blob, three offset is a rhythm. */
.turn__dots i:nth-child(2) {
  animation-delay: 160ms;
}

.turn__dots i:nth-child(3) {
  animation-delay: 320ms;
}

@keyframes blink {
  0%,
  100% {
    opacity: 0.2;
  }
  40% {
    opacity: 1;
  }
}

/* A caret on the last line while tokens are still arriving, so a pause in the
   stream reads as thinking rather than as finished. */
/* The caret blinks from its container, not from itself.

   It is drawn on whichever block is currently last, and that block is replaced
   on every streamed chunk — the whole answer is re-rendered so that a list or
   a table becomes valid once its final line arrives. An infinite animation
   declared on the caret therefore restarted every few dozen milliseconds and
   never reached its own midpoint, so the caret never blinked once.

   Animating an inherited custom property on the stable .prose element instead
   means the value keeps advancing no matter how often the last child is
   swapped: the new child simply reads whatever the alpha is now. @property is
   required — a custom property with no registered type is a string, and
   strings do not interpolate. */
@property --caret-alpha {
  syntax: '<number>';
  inherits: true;
  initial-value: 1;
}

.prose--streaming {
  animation: caret 1.05s var(--ease) infinite alternate;
}

.prose--streaming > *:last-child::after {
  content: '';
  display: inline-block;
  inline-size: 2px;
  block-size: 1em;
  margin-inline-start: 2px;
  vertical-align: text-bottom;
  background: var(--gold);
  opacity: var(--caret-alpha);
}

/* A period that is not a whole number of seconds, so it never phase-locks with
   the thinking dots. */
@keyframes caret {
  from {
    --caret-alpha: 1;
  }

  to {
    --caret-alpha: 0.12;
  }
}

/* --- Answer prose ------------------------------------------------------ */

.prose > * + * {
  margin-block-start: var(--space-4);
}

.prose h2,
.prose h3 {
  font-family: var(--font-display);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-tight);
  margin-block-start: var(--space-6);
}

.prose h2 {
  font-size: var(--text-lg);
}

.prose h3 {
  font-size: var(--text-md);
}

.prose ul,
.prose ol {
  padding-inline-start: var(--space-5);
}

.prose li + li {
  margin-block-start: var(--space-2);
}

.prose a {
  color: var(--gold);
  text-decoration: underline;
  text-underline-offset: 0.2em;
  text-decoration-thickness: 1px;
}

.prose strong {
  font-weight: var(--weight-semibold);
}

.prose code {
  font-family: var(--font-mono);
  font-size: 0.9em;
  padding: 0.1em 0.35em;
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
  /* Code is Latin even inside Arabic text. */
  direction: ltr;
  unicode-bidi: embed;
}

.prose pre {
  padding: var(--space-4);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface-sunken);
  overflow-x: auto;
  direction: ltr;
  text-align: start;
}

.prose pre code {
  padding: 0;
  background: none;
}

.prose blockquote {
  border-inline-start: 2px solid var(--border-strong);
  padding-inline-start: var(--space-4);
  color: var(--text-muted);
}

.prose table {
  inline-size: 100%;
  border-collapse: collapse;
  font-size: var(--text-sm);
}

.prose th,
.prose td {
  padding: var(--space-2) var(--space-3);
  border: var(--border-width) solid var(--border);
  text-align: start;
}

.prose th {
  background: var(--surface-sunken);
  font-weight: var(--weight-semibold);
}

/* Wide content must scroll inside its own box, never the page. */
.scroll-x {
  overflow-x: auto;
  max-inline-size: 100%;
}

/* --- Recommendation cards ---------------------------------------------- */

.picks {
  display: grid;
  gap: var(--space-3);
  margin-block-start: var(--space-5);
}

@media (min-width: 900px) {
  .picks {
    grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  }
}

.pick {
  box-shadow: var(--rim), var(--shadow-raised);
  /* Staggered by index, so a set of recommendations resolves in order instead
     of landing as one block. 60ms is under the threshold where it reads as
     waiting, and over the one where it reads as simultaneous. */
  animation: rise var(--duration-base) var(--ease) both;
  animation-delay: calc(var(--index, 0) * 60ms);
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-4);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
}

/* Only the top recommendation is gold. A ranked list where everything is
   highlighted has not ranked anything. */
.pick--top {
  border-color: var(--gold-edge);
}

.pick__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-3);
}

.pick__rank {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: 1.5rem;
  block-size: 1.5rem;
  flex-shrink: 0;
  border-radius: var(--radius-pill);
  background: var(--surface-sunken);
  color: var(--text-faint);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
}

.pick--top .pick__rank {
  background: linear-gradient(160deg, var(--gold), var(--gold-deep));
  color: var(--on-gold);
  box-shadow: inset 0 1px 0 var(--edge-highlight);
  font-weight: var(--weight-semibold);
}

/* The recommended card is lifted, not just outlined. */
.pick--top {
  box-shadow:
    inset 0 1px 0 var(--edge-highlight),
    0 0 0 1px var(--gold-edge),
    var(--shadow-overlay);
}

.pick__name {
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-tight);
}

.pick__badge {
  display: inline-block;
  margin-block-start: var(--space-1);
  padding: 0.1rem var(--space-2);
  border-radius: var(--radius-sm);
  background: var(--gold-dim);
  color: var(--gold);
  font-size: var(--text-xs);
}

.pick__why {
  color: var(--text-muted);
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
}

.pick__lists {
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  font-size: var(--text-sm);
}

.pick__list {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  list-style: none;
}

.pick__list li {
  display: flex;
  gap: var(--space-2);
  line-height: var(--leading-snug);
}

/* Marker glyphs, not emoji: they carry the same meaning and survive both
   themes without importing an icon font. */
.pick__list li::before {
  flex-shrink: 0;
  color: var(--text-faint);
  font-family: var(--font-mono);
}

.pick__list--pros li::before {
  content: '+';
  color: var(--success);
}

.pick__list--cons li::before {
  content: '−';
  color: var(--danger);
}

/* The card's one action. Outlined rather than filled: the gold fill belongs to
   the composer's send button, and two solid gold controls on screen would each
   weaken the other. */
.pick__action {
  inline-size: 100%;
  padding: var(--space-2) var(--space-3);
  border: var(--border-width) solid var(--gold-edge);
  border-radius: var(--radius-md);
  color: var(--gold);
  font-size: var(--text-sm);
  line-height: var(--leading-tight);
  transition: background-color var(--duration-fast) var(--ease);
}

@media (hover: hover) {
  .pick__action:hover {
    background: var(--gold-dim);
  }
}

.pick__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  margin-block-start: auto;
  padding-block-start: var(--space-3);
  border-block-start: var(--border-width) solid var(--border);
}

.pick__price {
  color: var(--text-muted);
  font-size: var(--text-sm);
}

.pick__link {
  color: var(--gold);
  font-size: var(--text-sm);
  text-decoration: none;
  white-space: nowrap;
}

@media (hover: hover) {
  .pick__link:hover {
    text-decoration: underline;
    text-underline-offset: 0.2em;
  }
}

/* --- Sources ----------------------------------------------------------- */

.sources {
  animation: rise var(--duration-base) var(--ease) both;
  margin-block-start: var(--space-5);
  padding-block-start: var(--space-4);
  border-block-start: var(--border-width) solid var(--border);
}

.sources__title {
  color: var(--text-faint);
  font-size: var(--text-xs);
  letter-spacing: 0.04em;
  margin-block-end: var(--space-2);
}

.sources__list {
  display: flex;
  flex-direction: column;
  gap: var(--space-1);
  list-style: none;
}

.sources__list a {
  color: var(--text-muted);
  font-size: var(--text-sm);
  text-decoration: none;
  /* A long URL must not push the layout wider than the viewport. */
  overflow-wrap: anywhere;
}

@media (hover: hover) {
  .sources__list a:hover {
    color: var(--gold);
  }
}

/* --- Command palette --------------------------------------------------- */

.palette {
  border: none;
  padding: 0;
  background: none;
  max-inline-size: min(34rem, calc(100vw - 2rem));
  inline-size: 100%;
  margin-block-start: 12vh;
  margin-inline: auto;
}

.palette::backdrop {
  background: var(--scrim);
  backdrop-filter: blur(3px);
}

.palette__panel {
  border: var(--border-width) solid var(--border-strong);
  border-radius: var(--radius-lg);
  background: var(--surface);
  /* One declaration. There were two, and the second silently discarded the
     rim — a duplicate property is not merged, it is replaced. */
  box-shadow: var(--rim), var(--shadow-overlay);
  overflow: hidden;
}

.palette__input {
  inline-size: 100%;
  padding: var(--space-4);
  border: none;
  border-block-end: var(--border-width) solid var(--border);
  background: none;
  font-size: var(--text-md);
}

.palette__input:focus {
  outline: none;
}

.palette__results {
  max-block-size: 22rem;
  overflow-y: auto;
  /* Reaching the end of the results must not hand the wheel to the page behind
     the dialog. Neither this nor the conversation list contained its own
     scroll, so both of them chained. */
  overscroll-behavior: contain;
  list-style: none;
}

.palette__item {
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  inline-size: 100%;
  padding: var(--space-2) var(--space-4);
  /* Prose line-height leaves a list of one-line rows mostly empty. */
  line-height: var(--leading-snug);
  text-align: start;
}

@media (hover: hover) {
  .palette__item:hover {
    background: var(--surface-raised);
  }
}

.palette__item-name {
  font-weight: var(--weight-medium);
}

.palette__item-meta {
  margin-inline-start: auto;
  color: var(--text-faint);
  font-size: var(--text-xs);
  white-space: nowrap;
}

.palette__empty {
  padding: var(--space-5);
  color: var(--text-faint);
  font-size: var(--text-sm);
  text-align: center;
}

/* --- Ghost buttons ------------------------------------------------------
   A bordered, unfilled control. Used wherever an action is available but not
   the point of the view — filling them would compete with the composer and the
   top recommendation, which are the two things gold is reserved for.
   ----------------------------------------------------------------------- */

.ghost-button {
  padding: var(--space-2) var(--space-3);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
  color: var(--text-muted);
  font-size: var(--text-sm);
  white-space: nowrap;
  transition:
    color var(--duration-fast) var(--ease),
    border-color var(--duration-fast) var(--ease);
}

@media (hover: hover) {
  .ghost-button:hover {
    color: var(--text);
    border-color: var(--border-strong);
  }
}

.ghost-button:active {
  transform: translateY(1px);
}

/* --- Sheet (modal) ------------------------------------------------------ */

.sheet {
  border: none;
  padding: 0;
  background: none;
  inline-size: 100%;
  max-inline-size: min(30rem, calc(100vw - 2rem));
  margin: auto;
}

.sheet::backdrop {
  background: var(--scrim);
  backdrop-filter: blur(3px);
}

.sheet__panel {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-5);
  border: var(--border-width) solid var(--border-strong);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow:
    inset 0 1px 0 var(--edge-highlight),
    var(--shadow-overlay);
  text-align: start;
}

.sheet__title {
  font-family: var(--font-display);
  font-size: var(--text-lg);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-tight);
}

.sheet__text {
  color: var(--text-muted);
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
}

.sheet__label {
  margin-block-start: var(--space-2);
  color: var(--text-faint);
  font-size: var(--text-xs);
}

.sheet__row {
  display: flex;
  gap: var(--space-2);
}

.sheet__input {
  flex: 1;
  min-inline-size: 0;
  padding: var(--space-2) var(--space-3);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface-sunken);
  font-family: var(--font-mono);
  font-size: var(--text-sm);
  /* A key is an opaque token, not prose: it reads left-to-right in every
     interface language. */
  direction: ltr;
  text-align: start;
}

.sheet__input:focus {
  outline: none;
  border-color: var(--gold-edge);
}

.sheet__actions {
  display: flex;
  gap: var(--space-2);
}

.sheet__actions--end {
  justify-content: flex-end;
  margin-block-start: var(--space-2);
  padding-block-start: var(--space-3);
  border-block-start: var(--border-width) solid var(--border);
}

.sheet__status {
  min-block-size: 1.25rem;
  color: var(--gold);
  font-size: var(--text-sm);
}

/* --- Notices ----------------------------------------------------------- */

.notice {
  padding: var(--space-3) var(--space-4);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
  color: var(--text-muted);
  font-size: var(--text-sm);
}

.notice--error {
  border-color: color-mix(in srgb, var(--danger) 40%, var(--border));
  color: var(--danger);
}

/* --- Footer ------------------------------------------------------------ */

/* The clearance for the pinned dock belongs on the last element in the page,
   not on <main> — the footer comes after main, so padding on main alone still
   let the footnote slide underneath the composer.

   --dock-height is written by app.js from a ResizeObserver, because the dock
   is not a fixed height: the textarea grows as you type. The fallback covers
   the frame before the first measurement. */
.footer {
  padding-block: var(--space-5);
  border-block-start: var(--border-width) solid var(--border);
  color: var(--text-faint);
  font-size: var(--text-xs);
}

.footer__inner {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
}

/* --- State ------------------------------------------------------------- */

[hidden] {
  display: none !important;
}

/* --- Entrance choreography ----------------------------------------------

   The page assembles itself in reading order: heading, lead, controls,
   composer, then the shelves below. Each step is one --stagger behind the
   last, which is short enough that the whole sequence is over before it could
   feel like a wait, and long enough that the eye reads an order rather than a
   flash.

   Delays are declared here rather than inline in the markup so the timing of
   the page lives in one place — change --stagger and the whole sequence
   re-times together.

   The dock itself is deliberately excluded: it is sticky and already carries a
   transform for the on-screen keyboard, and a second one would fight it. Its
   children animate instead.
   ----------------------------------------------------------------------- */

.hero__title,
.hero__lead,
.modes,
.composer,
.composer__note,
.suggestions,
.section {
  animation: rise var(--duration-slow) var(--ease-entrance) both;
}

.hero__title {
  animation-delay: 0ms;
}

.hero__lead {
  animation-delay: var(--stagger);
}

.modes {
  animation-delay: calc(var(--stagger) * 2);
}

.composer {
  animation-delay: calc(var(--stagger) * 3);
}

.composer__note {
  animation-delay: calc(var(--stagger) * 4);
}

.suggestions {
  animation-delay: calc(var(--stagger) * 4);
}

.section {
  animation-delay: calc(var(--stagger) * 5);
}

/* Items rendered from data stagger against their own index, set by app.js.
   Capped there, not here: an unbounded delay means the last card in a long
   list arrives after the visitor has already scrolled past it.

   Every group app.js calls stagger() on has to be listed. Four of them were
   not — .offer, .step, .update and .tool — so each of those elements carried
   an --enter-delay that nothing read, and eighteen tool cards, three offers,
   three steps and six update rows appeared all at once while the comments
   around them described an entrance that no longer existed. */
.chip,
.category,
.offer,
.step,
.update,
.tool {
  animation: rise var(--duration-base) var(--ease-entrance) both;
  animation-delay: var(--enter-delay, 0ms);
}

/* Second visit onward, nothing arrives — it is simply there.

   app.js stamps [data-entered] on a view panel as it leaves the screen, and on
   the dock as the chat view does. Listing the entrance selectors explicitly
   rather than reaching for a descendant wildcard is deliberate: the thread
   below has a blinking cursor and a drawing checkmark that must keep running
   on a view you have already seen. */
[data-entered]
  :is(
    .hero__title,
    .hero__lead,
    .suggestions,
    .section,
    .chip,
    .category,
    .offer,
    .step,
    .update,
    .tool,
    .modes,
    .composer,
    .composer__note
  ) {
  animation: none;
}

/* --- Hover, guarded ------------------------------------------------------

   A touch screen has no pointer to leave, so an unguarded :hover sticks after
   a tap: the card you pressed stays lit until you press something else. Every
   hover state in this file is therefore behind (hover: hover).

   The rules below were split out of compound selectors where only one half was
   a hover — the aria-current, --active and :focus-visible halves have to keep
   working on a phone, so they stayed where they were.
   ----------------------------------------------------------------------- */

@media (hover: hover) {
  .nav__link:hover {
    color: var(--text);
  }
}

@media (hover: hover) {
  .nav__link:hover::after {
    inline-size: 100%;
  }
}

@media (hover: hover) {
  .convo:hover .convo__open {
    color: var(--text);
  }
}

@media (hover: hover) {
  .convo:hover .convo__delete {
    opacity: 1;
  }
}

/* --- Dialog motion -------------------------------------------------------

   A <dialog> is display:none when closed, and display is not an animatable
   property — which is why both of these appeared and vanished instantly no
   matter what else was declared on them.

   `allow-discrete` is the load-bearing part. It lets display and overlay flip
   at the *end* of the outgoing transition instead of the start, so the close
   animates too. Without it only the open would move, and a panel that fades in
   but snaps out feels worse than one that does neither.

   @starting-style supplies the "before it existed" state; a dialog has no
   previous computed style to transition from without it.
   ----------------------------------------------------------------------- */

.palette,
.sheet {
  opacity: 0;
  transition:
    opacity var(--duration-panel) var(--ease),
    transform var(--duration-panel) var(--ease),
    overlay var(--duration-panel) allow-discrete,
    display var(--duration-panel) allow-discrete;
}

/* The palette drops from the search control that opened it; the sheet rises
   from the bottom edge, the direction a sheet comes from. */
.palette {
  transform: translateY(-10px) scale(0.985);
}

.sheet {
  transform: translateY(14px);
}

.palette[open],
.sheet[open] {
  opacity: 1;
  transform: none;
}

@starting-style {
  .palette[open] {
    opacity: 0;
    transform: translateY(-10px) scale(0.985);
  }

  .sheet[open] {
    opacity: 0;
    transform: translateY(14px);
  }
}

.palette::backdrop,
.sheet::backdrop {
  opacity: 0;
  transition:
    opacity var(--duration-panel) var(--ease),
    overlay var(--duration-panel) allow-discrete,
    display var(--duration-panel) allow-discrete;
}

.palette[open]::backdrop,
.sheet[open]::backdrop {
  opacity: 1;
}

@starting-style {
  .palette[open]::backdrop,
  .sheet[open]::backdrop {
    opacity: 0;
  }
}

/* --- Conversations sidebar ----------------------------------------------

   Off-canvas and fixed, so it never participates in the page's layout and
   cannot disturb the pinned composer by existing.

   The slide direction is the one place a mirrored rule is unavoidable:
   transform has no logical equivalent, so the distance is kept in a variable
   and only its sign is restated for RTL. Everything else — which edge it
   occupies, which side the page shifts toward — is expressed logically and
   flips on its own.
   ----------------------------------------------------------------------- */

.sidebar {
  --slide: -100%;

  position: fixed;
  inset-block: var(--header-height) 0;
  inset-inline-start: 0;
  z-index: var(--z-sidebar);
  inline-size: var(--sidebar-width);
  max-inline-size: 86vw;

  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  padding: var(--space-4);

  background: var(--surface-sunken);
  border-inline-end: var(--border-width) solid var(--border);
  box-shadow: var(--rim);

  transform: translateX(var(--slide));
  visibility: hidden;
  transition:
    transform var(--duration-panel) var(--ease),
    visibility 0s linear var(--duration-panel);
}

:root[dir='rtl'] .sidebar {
  --slide: 100%;
}

:root[data-sidebar='open'] .sidebar {
  transform: translateX(0);
  visibility: visible;
  /* No delay on the way in — only on the way out, so the panel stays
     hit-testable until it has finished leaving. */
  transition-delay: 0s;
}

.sidebar__title {
  font-family: var(--font-display);
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
}

/* Only this scrolls, so the new-chat button above and the sync row below stay
   reachable however long the list grows. */
.sidebar .convos {
  flex: 1;
  min-block-size: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  margin-inline: calc(var(--space-2) * -1);
  padding-inline: var(--space-2);
}

.sidebar__empty {
  color: var(--text-faint);
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
}

.sidebar__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding-block-start: var(--space-3);
  border-block-start: var(--border-width) solid var(--border);
}

.sidebar__note {
  color: var(--text-faint);
  font-size: var(--text-xs);
}

/* Dismisses the panel on the sizes where it overlays the page.

   Interactivity is driven from the same state as the opacity, in CSS, rather
   than from a `hidden` attribute set in JS. Two reasons: an element at opacity
   0 is still hit-testable, so a closed scrim was a full-viewport invisible
   layer swallowing every click — and `hidden` is display:none, which cannot
   fade, so the scrim vanished instantly while the panel was still sliding. */
.scrim {
  position: fixed;
  /* Below the header, which stays interactive: the drawer covers the page, not
     the site's own controls. */
  inset-block: var(--header-height) 0;
  inset-inline: 0;
  z-index: var(--z-scrim);
  background: var(--scrim);
  opacity: 0;
  pointer-events: none;
  visibility: hidden;
  transition:
    opacity var(--duration-panel) var(--ease),
    visibility 0s linear var(--duration-panel);
}

:root[data-sidebar='open'] .scrim {
  opacity: 1;
  pointer-events: auto;
  visibility: visible;
  transition-delay: 0s;
}

/* A covered page must stop moving. The scrim swallows clicks but not wheels or
   drags, so on a long page — the tools directory, most of all — opening the
   drawer and scrolling ran the content along behind it while the drawer sat
   still. The dialogs do not need this; showModal() locks the page itself. */
:root[data-sidebar='open'] {
  overflow: hidden;
}

/* Wide enough to seat the panel beside the page rather than over it.

   80rem, not the 68rem this started at, and the number is derived rather than
   picked: the category grid goes to four columns at 64rem, and each card needs
   roughly 230px to hold a two-line label. Four of those plus gaps is about
   960px of content, and 960 + 272 for the panel + 48 of padding is 1280px. Any
   narrower and opening the panel either crushes the grid or knocks the reading
   column off its measure — so between 64rem and 80rem, which is the common
   laptop, the panel overlays instead and the content never reflows.

   The page and the pinned composer give up the same amount of inline space,
   which is why it is one token and not two measurements that can drift. */
@media (min-width: 80rem) {
  /* Closed does not mean gone here. Narrowing to a rail keeps every
     destination one click away on a screen with room to spare, which is what
     "it stays as icons" means: the labels go, the navigation does not. */
  :root[data-view='chat'] .sidebar {
    transform: none;
    visibility: visible;
    inline-size: var(--rail-width);
    padding-inline: var(--space-2);
    overflow: hidden;
    transition:
      inline-size var(--duration-panel) var(--ease),
      padding-inline var(--duration-panel) var(--ease);
  }

  /* Out of the column, still in the accessibility tree. The same clipping the
     .sr-only helper uses, rather than display:none, so each icon keeps the
     name its label already carries in both languages — no second copy of the
     string in an aria-label for applyLocale to keep in sync. */
  :root[data-view='chat'] .rail__label {
    position: absolute;
    inline-size: 1px;
    block-size: 1px;
    padding: 0;
    overflow: hidden;
    clip-path: inset(50%);
  }

  /* This one is genuinely unavailable while the drawer is a rail, so hiding it
     from assistive technology as well as from the page is correct. */
  :root[data-view='chat'] .sidebar__chats {
    display: none;
  }

  /* Narrowed to a rail, an item is a 20px icon alone in a 52px column, and the
     inline padding a labelled row needs holds it against the start edge rather
     than in the middle — four pixels off centre, every icon, all the way down.
     Four pixels is enough to see and not enough to name, which is the worst
     kind of misalignment to leave in. Centre them and drop the padding they no
     longer have anything to space. */
  :root[data-view='chat']:not([data-sidebar='open']) .rail__item {
    justify-content: center;
    padding-inline: 0;
  }

  :root[data-view='chat'][data-sidebar='open'] .sidebar {
    inline-size: var(--sidebar-width);
    padding-inline: var(--space-4);
  }

  :root[data-view='chat'][data-sidebar='open'] .rail__label {
    position: static;
    inline-size: auto;
    block-size: auto;
    overflow: visible;
    clip-path: none;
  }

  :root[data-view='chat'][data-sidebar='open'] .sidebar__chats {
    display: flex;
  }

  /* Beside the page, nothing is covered, so nothing has to stop moving. The
     lock above is written for the drawer as an overlay and applied to the open
     state at every width, which on a wide chat screen froze the conversation
     the drawer was sitting next to. */
  :root[data-view='chat'][data-sidebar='open'] {
    overflow: visible;
  }

  /* The page and the pinned composer give up the same amount of inline space,
     which is why it is one token and not two measurements that can drift.

     .main, not .shell. Insetting the whole shell pulled the header in with it,
     and the rail only starts below the header — so the corner where the two
     would have met belonged to neither, and sat there as an empty 68×56 notch
     against a header that runs edge to edge on every other screen. The header
     is a bar across the top of the site; the rail is beside the page, not
     beside the site's own chrome. */
  :root[data-view='chat'] .main,
  :root[data-view='chat'] .dock {
    padding-inline-start: var(--rail-width);
    transition: padding-inline-start var(--duration-panel) var(--ease);
  }

  :root[data-view='chat'][data-sidebar='open'] .main,
  :root[data-view='chat'][data-sidebar='open'] .dock {
    padding-inline-start: var(--sidebar-width);
  }

  /* Beside the page, nothing is covered, so there is nothing to dismiss — and
     nothing to lock either: the page is still yours to scroll with the panel
     open, which is the whole point of seating it alongside. */
  :root[data-view='chat'] .scrim {
    display: none;
  }

  :root[data-view='chat'][data-sidebar='open'] {
    overflow: visible;
  }
}

/* --- Tools view ---------------------------------------------------------

   The knowledge base, browsable. Cards deliberately reuse the .category
   treatment — same fill, same border, same lift, same gold hairline drawing up
   the leading edge — because they are the same kind of object and inventing a
   second card style would make the page look like two products.

   Gold is spent on exactly two things here: the active filter chip, and the
   primary action at the end of a tool's article. The composer's send button is
   fixed to the viewport and always on screen, and three solid gold controls at
   once would each weaken the other two.
   ----------------------------------------------------------------------- */

.page__head {
  margin-block-end: var(--space-5);
}

.page__title {
  font-family: var(--font-display);
  font-size: var(--text-xl);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-tight);
}

/* Focused programmatically when the view opens, so assistive technology hears
   where it landed. It is a heading, not a control, so it must not draw a ring
   for a mouse user who never asked for it. */
.page__title:focus {
  outline: none;
}

/* A heading that takes focus on arrival is a landing spot, not a control.

   Both page titles are given tabindex="-1" and focused by the router, so the
   next Tab starts at the top of the view you just opened rather than back at
   the top of the site. That is worth doing and it is not worth a ring: an
   outline drawn around a block-level heading spans the whole reading column,
   which on a 40px title reads as an error box around the page's own name.
   Neither is reachable by Tab — they are not in the tab order at all — so
   nothing keyboard-operable loses its indicator here. */
.page__title:focus-visible,
.article__title:focus-visible {
  outline: none;
}

.page__lead {
  margin-block-start: var(--space-2);
  max-inline-size: var(--width-content);
  color: var(--text-muted);
}

.tools__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  margin-block-end: var(--space-4);
}

.tools__search {
  flex: 1 1 16rem;
  min-inline-size: 0;
  block-size: var(--control-height);
  padding-inline: var(--space-3);
  border: var(--border-width) solid var(--border-strong);
  border-radius: var(--radius-md);
  background: var(--surface);
  color: var(--text);
  font: inherit;
  font-size: var(--text-sm);
}

.tools__search:focus-visible {
  outline: none;
  border-color: var(--gold-edge);
  box-shadow: 0 0 0 3px var(--gold-dim);
}

.filters {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  /* Same idiom the header nav uses on a phone: scroll rather than wrap, so the
     row below never moves. */
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
}

.filters::-webkit-scrollbar {
  display: none;
}

.filter {
  padding: var(--space-1) var(--space-3);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-pill);
  background: var(--surface);
  color: var(--text-muted);
  font-size: var(--text-xs);
  white-space: nowrap;
  transition:
    color var(--duration-fast) var(--ease),
    background-color var(--duration-fast) var(--ease),
    border-color var(--duration-fast) var(--ease);
}

.filter[aria-pressed='true'] {
  border-color: var(--gold-edge);
  background: var(--gold-dim);
  color: var(--gold);
  font-weight: var(--weight-medium);
}

.tools__count {
  margin-inline-start: auto;
  color: var(--text-faint);
  font-size: var(--text-sm);
}

.tools__grid {
  display: grid;
  gap: var(--space-3);
  grid-template-columns: 1fr;
}

@media (min-width: 40rem) {
  .tools__grid {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (min-width: 64rem) {
  .tools__grid {
    grid-template-columns: repeat(3, 1fr);
  }
}

.tool {
  position: relative;
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  overflow: hidden;
  padding: var(--space-4);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
  box-shadow: var(--rim), var(--shadow-raised);
  --lift: 0px;
  transform: translateY(var(--lift));
  transition:
    border-color var(--duration-base) var(--ease),
    transform var(--duration-base) var(--ease),
    box-shadow var(--duration-base) var(--ease);
}

.tool::after {
  content: '';
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  inline-size: 2px;
  background: var(--gold);
  transform: scaleY(0);
  transform-origin: block-end;
  transition: transform var(--duration-base) var(--ease);
}

@media (hover: hover) {
  .tool:hover {
    --lift: -2px;
    border-color: var(--gold-edge);
    box-shadow:
      var(--rim),
      0 8px 24px -8px light-dark(rgba(60, 50, 30, 0.18), rgba(0, 0, 0, 0.6));
  }

  .tool:hover::after {
    transform: scaleY(1);
  }
}

/* The whole card opens the drawer, but the footer actions must stay clickable,
   so the card's hit area is a stretched pseudo-element underneath them rather
   than a wrapper around them — a button cannot contain a button. */
.tool__open {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: var(--space-2);
  font: inherit;
  color: inherit;
  text-align: start;
  /* It is an anchor now, and every anchor on this site says so explicitly:
     .brand, .pick__link and the source list each opt out here, and only .prose
     a keeps the underline, where a link inside running text needs one. A
     heading that draws its own rule is not a heading. */
  text-decoration: none;
}

.tool__open::after {
  content: '';
  position: absolute;
  inset: 0;
}

.tool__name {
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-tight);
}

/* The name in the other script. Never dropped in either language: the Latin
   name is what you type into a search engine and what is printed on the site
   you are about to visit, and several of the Arabic names are transliterations
   nobody writes down. */
.tool__alt {
  color: var(--text-faint);
  font-size: var(--text-xs);
  font-weight: 400;
}

.tool__tag {
  color: var(--text-faint);
  font-size: var(--text-xs);
}

/* The only thing on the card at full text colour, because it is the only thing
   here anyone is actually reading. */
.tool__summary {
  color: var(--text);
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
}

.tool__uses {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1);
  list-style: none;
}

.tool__use {
  padding: 2px var(--space-2);
  border-radius: var(--radius-sm);
  background: var(--surface-sunken);
  color: var(--text-faint);
  font-size: var(--text-xs);
}

.tool__foot {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-2);
  /* Pushes the footer to the bottom however tall the summary above it runs, so
     a row of cards has one baseline rather than three. */
  margin-block-start: auto;
  padding-block-start: var(--space-3);
  border-block-start: var(--border-width) solid var(--border);
}

.tool__price {
  color: var(--text-faint);
  font-size: var(--text-xs);
}

.tools__empty {
  padding-block: var(--space-8);
  text-align: center;
}

.tools__empty-title {
  font-family: var(--font-display);
  font-size: var(--text-lg);
}

.tools__empty-hint {
  margin-block-start: var(--space-2);
  color: var(--text-muted);
}

.tools__empty-actions {
  display: flex;
  justify-content: center;
  gap: var(--space-2);
  margin-block-start: var(--space-4);
}

/* --- The tool article ----------------------------------------------------

   A page in the reading column, laid out like something written rather than
   something displayed. It was a drawer: 32rem of panel sliding over the grid,
   its own scroll container, a sticky footer of actions and a backdrop. All of
   that was scaffolding for covering a page, and none of it is needed by a page
   that simply is the page.
   ----------------------------------------------------------------------- */

.article {
  padding-block: var(--space-6) var(--space-9);
}

/* The way back, and a real link. It sits above the heading rather than beside
   it so that the title is the first thing on the page, as it would be in
   anything else you read. */
.article__back {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  margin-block-end: var(--space-5);
  color: var(--text-muted);
  font-size: var(--text-sm);
  text-decoration: none;
  transition: color var(--duration-base) var(--ease);
}

@media (hover: hover) {
  .article__back:hover {
    color: var(--gold);
  }
}

/* The chevron points the way the reader goes back, which is the way the text
   runs. Mirrored rather than drawn twice — the same idiom as the send arrow. */
.article__back-icon {
  inline-size: 18px;
  block-size: 18px;
  transform: scaleX(-1);
}

:root:dir(ltr) .article__back-icon {
  transform: none;
}

.article__head {
  display: grid;
  gap: var(--space-2);
  padding-block-end: var(--space-5);
  border-block-end: var(--border-width) solid var(--border);
}

/* Category and review date, above the title. A kicker rather than a subtitle:
   it says what kind of thing this is before it says which one. */
.article__kicker {
  color: var(--text-faint);
  font-size: var(--text-xs);
  letter-spacing: 0.04em;
}

.article__title {
  font-family: var(--font-display);
  font-size: var(--text-2xl);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-tight);
}

.article__alt {
  color: var(--text-faint);
  font-size: var(--text-sm);
}

/* The one-line verdict, marked the way the bot's own answers are marked. */
.article__summary {
  margin-block-start: var(--space-2);
  padding-inline-start: var(--space-4);
  border-inline-start: 2px solid var(--gold);
  font-size: var(--text-md);
  line-height: var(--leading-snug);
}

/* Price and uses, side by side where there is room for two columns and stacked
   where there is not. */
.article__facts {
  display: grid;
  gap: var(--space-4);
  padding-block: var(--space-5);
  border-block-end: var(--border-width) solid var(--border);
}

@media (min-width: 40rem) {
  .article__facts {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: var(--space-5);
  }
}

.article__term {
  color: var(--text-faint);
  font-size: var(--text-xs);
  letter-spacing: 0.04em;
}

.article__value {
  margin-block-start: var(--space-1);
  line-height: var(--leading-snug);
}

.article__body {
  padding-block: var(--space-5);
}

/* Not sticky. In the drawer the actions had to float above a scrolling panel;
   on a page they are the end of the article, and arriving at them is what
   finishing the article means. */
.article__foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--space-3);
  padding-block-start: var(--space-5);
  border-block-start: var(--border-width) solid var(--border);
}

.article__foot .pick__action {
  inline-size: auto;
  min-inline-size: 14rem;
}

/* Three bars while the body is in flight. Delayed, because a skeleton that
   shows for eighty milliseconds is worse than no skeleton at all. */
.article__skeleton {
  display: grid;
  gap: var(--space-2);
  animation: fade var(--duration-base) var(--ease) 200ms both;
}

.article__skeleton span {
  block-size: 0.75rem;
  border-radius: var(--radius-sm);
  background: var(--surface-raised);
}

.article__skeleton span:nth-child(2) {
  inline-size: 94%;
}

.article__skeleton span:nth-child(3) {
  inline-size: 72%;
}

/* --- The drawer rail -----------------------------------------------------

   Navigation as a column of icons with labels beside them. It is the same list
   the header nav carries on a wide screen, which is deliberate: one set of
   destinations, described the same way, so the site does not appear to have
   two different maps of itself depending on where you look.
   ----------------------------------------------------------------------- */

.rail {
  display: flex;
  flex-direction: column;
  gap: 2px;
}

.rail__item,
.sidebar__new {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  /* A 44px touch target at the smallest size the rail ever takes. */
  min-block-size: 2.75rem;
  padding-inline: var(--space-3);
  border-radius: var(--radius-md);
  color: var(--text-muted);
  font-size: var(--text-sm);
  text-align: start;
  transition:
    color var(--duration-fast) var(--ease),
    background-color var(--duration-fast) var(--ease);
}

/* The primary action in the panel. Everything it shares with a rail row comes
   from the rule above; this is only the difference.

   It had a second full rule of its own six hundred lines further up, and the
   shared one below it quietly won `gap`, `padding-inline`, `color` and the
   entire transition list. So the gold text that rule asked for was never gold,
   and the press underneath had no `transform` in the list it was animating
   against — the same one-property omission that made every press on this page
   a teleport until it was found on the send button. */
.sidebar__new {
  border: var(--border-width) solid var(--gold-edge);
  /* A pixel of border the rail rows do not carry. With the same padding it put
     this icon a pixel inside their column, which in a column of six icons is
     the one that looks wrong. */
  padding-inline: calc(var(--space-3) - var(--border-width));
  font-weight: var(--weight-medium);
  transition:
    color var(--duration-fast) var(--ease),
    background-color var(--duration-fast) var(--ease),
    transform var(--duration-base) var(--ease);
}

.sidebar__new:active {
  transform: translateY(1px);
  transition-duration: var(--duration-instant);
}

@media (hover: hover) {
  .sidebar__new:hover {
    background: var(--gold-dim);
  }
}

.rail__item[aria-current='true'] {
  color: var(--text);
  background: var(--surface);
}

@media (hover: hover) {
  .rail__item:hover {
    color: var(--text);
    background: var(--surface);
  }
}

.rail__icon {
  inline-size: 1.25rem;
  block-size: 1.25rem;
  flex-shrink: 0;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.6;
}

.rail__label {
  white-space: nowrap;
}

/* The conversation half of the drawer, below the navigation half. */
.sidebar__chats {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
  flex: 1;
  min-block-size: 0;
  padding-block-start: var(--space-3);
  border-block-start: var(--border-width) solid var(--border);
}

/* --- Header, reorganised -------------------------------------------------

   One row at every width. The hamburger appears where the nav cannot fit, and
   on the chat screen at any width, where it is the control that widens the
   rail into the full drawer.
   ----------------------------------------------------------------------- */

.header__menu {
  display: inline-flex;
}

/* From the width where the header carries its own nav, the hamburger has one
   remaining job — widening the chat's rail into the full drawer — so it leaves
   the other three views entirely.

   It was briefly `visibility: hidden` instead, to stop the brand sliding
   sideways on the way into the chat. That traded a movement for a permanent
   fault: the empty seat pushed the brand 52px inside the content column, so it
   no longer stood above the page heading it is supposed to line up with. A
   misalignment you can see on every screen is worse than a shift you see when
   you change screens, and whichever of the two is on show, the header's first
   item has to sit on the same edge as the column beneath it. */
@media (min-width: 60rem) {
  .header__menu {
    display: none;
  }

  :root[data-view='chat'] .header__menu {
    display: inline-flex;
  }
}

/* The magnifier duplicates the labelled hint, so only one of them is ever on
   screen at a time. */
@media (min-width: 900px) {
  .header__search {
    display: none;
  }
}

.brand__name {
  font-family: var(--font-display);
}

.menu-icon {
  inline-size: 1.25rem;
  block-size: 1.25rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
}

/* The middle bar retracts and the outer two meet in the centre and cross. They
   have to rotate as well as translate: translated alone they landed on the
   same line and the control read as a minus sign. */
.menu-icon__top,
.menu-icon__mid,
.menu-icon__low {
  transition:
    transform var(--duration-base) var(--ease),
    opacity var(--duration-fast) var(--ease);
  transform-origin: center;
}

:root[data-sidebar='open'] .menu-icon__mid {
  opacity: 0;
}

:root[data-sidebar='open'] .menu-icon__top {
  transform: translateY(5px) rotate(45deg);
}

:root[data-sidebar='open'] .menu-icon__low {
  transform: translateY(-5px) rotate(-45deg);
}

/* --- Landing -------------------------------------------------------------

   The page that answers "what is this". It gets the only two-button row on the
   site and the only display-size heading, and then hands off: everything below
   the hero is a short explanation of one thing, in the same card treatment the
   rest of the site already uses.
   ----------------------------------------------------------------------- */

.hero__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  margin-block-start: var(--space-5);
}

/* The one solid gold control on this page. The composer's send button is the
   other one in the product, and the two are never on screen together. */
.cta {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-block-size: 3rem;
  padding-inline: var(--space-5);
  border-radius: var(--radius-md);
  background: var(--gold);
  color: var(--on-gold);
  font-size: var(--text-sm);
  font-weight: var(--weight-semibold);
  /* --gold-glow is a colour, not a shadow. `box-shadow: <color>` has no
     offsets, so it was invalid at computed-value time — which does not drop
     the declaration, it computes it to `none`. The glow never drew, and the
     dead declaration still won the cascade against the focus ring, so the
     page's primary button was focusable with nothing to show for it. */
  box-shadow: 0 10px 30px -12px var(--gold-glow);
  transition:
    background-color var(--duration-fast) var(--ease),
    transform var(--duration-base) var(--ease);
}

.cta:active {
  transform: translateY(1px);
  transition-duration: var(--duration-instant);
}

@media (hover: hover) {
  .cta:hover {
    background: var(--gold-deep);
  }
}

.ghost-button--lg {
  min-block-size: 3rem;
  padding-inline: var(--space-4);
  font-size: var(--text-sm);
}

.hero__meta {
  margin-block-start: var(--space-4);
  color: var(--text-faint);
  font-size: var(--text-xs);
}

.section__lead {
  margin-block-end: var(--space-4);
  max-inline-size: var(--width-content);
  color: var(--text-muted);
  font-size: var(--text-sm);
}

.offers {
  display: grid;
  gap: var(--space-3);
}

@media (min-width: 48rem) {
  .offers {
    grid-template-columns: repeat(3, 1fr);
  }
}

.offer {
  padding: var(--space-4);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  box-shadow: var(--shadow-raised), var(--rim);
}

.offer__title {
  font-family: var(--font-display);
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
}

.offer__text {
  margin-block-start: var(--space-2);
  color: var(--text-muted);
  font-size: var(--text-sm);
  line-height: var(--leading-body);
}

/* Numbered rather than carded: three steps in order are a sequence, and a row
   of equal boxes would say they are alternatives. */
/* The list draws no marker of its own: the number is a designed element with
   the section's gold on it, and the native one rendered beside it as "1.1". */
.steps {
  display: grid;
  gap: var(--space-4);
  list-style: none;
}

@media (min-width: 48rem) {
  .steps {
    grid-template-columns: repeat(3, 1fr);
  }
}

.step {
  position: relative;
  padding-block-start: var(--space-3);
  border-block-start: var(--border-width) solid var(--border-strong);
}

.step__number {
  display: block;
  font-family: var(--font-display);
  font-size: var(--text-sm);
  color: var(--gold);
}

.step__title {
  margin-block-start: var(--space-1);
  font-size: var(--text-base);
  font-weight: var(--weight-semibold);
}

.step__text {
  margin-block-start: var(--space-1);
  color: var(--text-muted);
  font-size: var(--text-sm);
  line-height: var(--leading-body);
}

/* --- Category cards, with an explanation ------------------------------- */

.category__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--space-3);
  inline-size: 100%;
}

.category__blurb {
  margin-block-start: var(--space-2);
  color: var(--text-muted);
  font-size: var(--text-xs);
  line-height: var(--leading-snug);
}

/* --- Chat screen -------------------------------------------------------- */

/* What an empty conversation says for itself. Centred in the space above the
   composer rather than pinned to the top, so a fresh chat reads as an
   invitation instead of a page that failed to load. */
.chat__empty {
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* Roughly the space between the header and the composer, so the invitation
     sits in the middle of the screen rather than at the top of a tall blank. */
  min-block-size: 46vh;
  padding-block: var(--space-6) var(--space-5);
  text-align: center;
}

.chat__title {
  font-family: var(--font-display);
  font-size: var(--text-xl);
  font-weight: var(--weight-semibold);
  line-height: var(--leading-tight);
}

.chat__lead {
  margin-block: var(--space-2) var(--space-5);
  color: var(--text-muted);
  font-size: var(--text-sm);
}

.chat__empty .suggestions {
  justify-content: center;
}

/* The hint above the mode tabs.

   Without it the tabs read as decoration: four words in a pill, already one
   of them chosen, with nothing saying the choice is the visitor's to make or
   that it changes what comes back. */
.modes__hint {
  margin-block-end: var(--space-2);
  color: var(--text-faint);
  font-size: var(--text-xs);
  text-align: center;
}

/* --- Copy buttons --------------------------------------------------------

   Placed rather than floated. A control that only appears on hover does not
   exist on a phone, and this one is the point of the whole howto flow: the
   answer is a prompt, and the next thing the reader does is paste it.
   ----------------------------------------------------------------------- */

.copy {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-block-size: 2rem;
  padding-inline: var(--space-3);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-md);
  color: var(--text-muted);
  font-size: var(--text-xs);
  transition:
    color var(--duration-fast) var(--ease),
    border-color var(--duration-fast) var(--ease),
    background-color var(--duration-fast) var(--ease);
}

.copy--compact {
  min-block-size: 1.75rem;
  padding-inline: var(--space-2);
  border-color: transparent;
}

.copy--done {
  color: var(--success);
  border-color: color-mix(in oklab, var(--success) 40%, transparent);
}

/* A clipboard write can be refused, and on any non-secure origin it is not
   there at all. The control has to say so rather than going quietly dead. */
.copy--failed {
  color: var(--danger);
  border-color: color-mix(in oklab, var(--danger) 40%, transparent);
}

.copy__icon {
  inline-size: 0.9rem;
  block-size: 0.9rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
  stroke-linecap: round;
  stroke-linejoin: round;
}

@media (hover: hover) {
  .copy:hover {
    color: var(--text);
    border-color: var(--border-strong);
    background: var(--surface);
  }
}

.turn__actions {
  display: flex;
  gap: var(--space-2);
  margin-block-start: var(--space-3);
}

/* --- Code blocks ---------------------------------------------------------

   A header bar over the block, holding the language the model tagged it with
   and the copy button. The bar shares the block's fill so the two read as one
   object with a lid, rather than a button parked on top of a slab.
   ----------------------------------------------------------------------- */

.code {
  margin-block: var(--space-4);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-md);
  overflow: hidden;
  background: var(--surface-sunken);
}

.code__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-3);
  padding: var(--space-1) var(--space-2) var(--space-1) var(--space-3);
  border-block-end: var(--border-width) solid var(--border);
  /* A prompt is written left to right even when the answer around it is not,
     so its lid is laid out the same way as its contents. */
  direction: ltr;
}

.code__lang {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--text-faint);
}

/* The block already has the box; the pre inside it must not draw a second. */
.code pre {
  margin: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
}

/* --- Sources, collapsed --------------------------------------------------

   Twelve links between the end of an answer and the next question is a wall,
   and almost nobody opens all of them — but the readers who do are exactly the
   ones this site is for. Closed by default, with the count on the summary so
   the decision to open it is an informed one.
   ----------------------------------------------------------------------- */

.sources__title {
  display: flex;
  align-items: center;
  gap: var(--space-2);
  cursor: pointer;
  list-style: none;
}

/* Safari draws its own triangle through ::-webkit-details-marker rather than
   honouring list-style on the summary. */
.sources__title::-webkit-details-marker {
  display: none;
}

.sources__chevron {
  inline-size: 0.9rem;
  block-size: 0.9rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: transform var(--duration-base) var(--ease);
}

.sources[open] .sources__chevron {
  transform: rotate(180deg);
}

/* --- Search palette, grouped -------------------------------------------- */

.palette__group {
  padding: var(--space-3) var(--space-3) var(--space-1);
  color: var(--text-faint);
  font-size: var(--text-xs);
  font-weight: var(--weight-medium);
  letter-spacing: 0.04em;
}

/* Keyboard selection has to be visible without a pointer anywhere near it.

   It was painting --surface, which is the panel's own fill — the selected row
   was the same colour as the thing it sat on, in both themes, so arrowing
   through the results moved an invisible cursor and Enter opened whatever it
   happened to be on. Hover had a real state; the keyboard had none.

   --surface-raised does the lifting, and the gold bar along the leading edge
   separates *selected* from merely *hovered*, which are different claims: one
   says the pointer is here, the other says Enter will open this. */
.palette__item[aria-selected='true'] {
  position: relative;
  background: var(--surface-raised);
  color: var(--text);
}

.palette__item[aria-selected='true']::before {
  content: '';
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  inline-size: 2px;
  background: var(--gold);
}

/* --- What is new ---------------------------------------------------------

   Short on purpose. This site can vouch for when it last reviewed each entry
   and for nothing else; anything about releases that actually happened comes
   from the assistant, which searches rather than remembering.
   ----------------------------------------------------------------------- */

.news__ask {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--space-4);
  margin-block-end: var(--space-6);
  padding: var(--space-4);
  border: var(--border-width) solid var(--gold-edge);
  border-radius: var(--radius-lg);
  background: var(--gold-dim);
}

.news__ask-text {
  flex: 1 1 18rem;
}

.news__ask-title {
  font-family: var(--font-display);
  font-size: var(--text-md);
  font-weight: var(--weight-semibold);
}

.news__ask-note {
  margin-block-start: var(--space-1);
  color: var(--text-muted);
  font-size: var(--text-sm);
}

.updates {
  display: grid;
  gap: var(--space-2);
  list-style: none;
}

.update__open {
  display: grid;
  gap: var(--space-1);
  inline-size: 100%;
  padding: var(--space-3);
  border: var(--border-width) solid var(--border);
  border-radius: var(--radius-md);
  background: var(--surface);
  text-align: start;
  transition:
    border-color var(--duration-fast) var(--ease),
    background-color var(--duration-fast) var(--ease);
}

@media (hover: hover) {
  .update__open:hover {
    border-color: var(--gold-edge);
    background: var(--surface-raised);
  }
}

.update__when {
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  color: var(--text-faint);
}

.update__name {
  font-weight: var(--weight-semibold);
}

.update__text {
  color: var(--text-muted);
  font-size: var(--text-sm);
  line-height: var(--leading-snug);
}

/* The section a filter chip has selected, said in words. A chip and a count
   tell you how many; this tells you of what. */
.tools__blurb {
  margin-block-end: var(--space-4);
  color: var(--text-muted);
  font-size: var(--text-sm);
}

/* The default class on an inline SVG built by icon() in app.js; every current
   caller passes its own, and this is the fallback size. */
.icon {
  inline-size: 1rem;
  block-size: 1rem;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.7;
}
