/*
 * The visual language: the dials, the scale, and what every page sits on.
 *
 * Nothing here knows what the app is. The colour tokens come from the theme
 * sheet an app generates (`theme-build.ts`), the faces come from an app's own
 * `@font-face` imports, and everything below is the arithmetic between them --
 * the type and spacing scales, the radius tiers the `--round` dial multiplies,
 * the leading a measure implies, the shell's own geometry, and the one focus
 * ring. `test/zkit-design/scale.test.ts` refuses a size or a length that is
 * not on one of these scales.
 */

/*
 * Every corner in the app, on one dial.
 *
 * Three tiers and a multiplier, rather than the five ad-hoc pixel values that
 * were here before (4, 6, 8, 10, 14 -- the 4 and the 8 each used once or twice
 * and indistinguishable from their neighbours). `--round` scales all of them
 * together, so "a bit sharper" is one number and not a sweep through forty
 * class names. 1 is the shape the app was designed at.
 *
 *   --r-1  small controls: icon buttons, chips, kbd, menu items
 *   --r-2  rows and fields: chat rows, inputs, code blocks, the composer
 *   --r-3  panels: the sidebar, dialogs, cards
 *
 * `rounded-full` is deliberately NOT on the dial. A pill and an avatar are
 * circles by construction, and scaling them turns them into lozenges.
 */
:root {
  --round: 1;
  --r-1: calc(6px * var(--round));
  --r-2: calc(10px * var(--round));
  --r-3: calc(14px * var(--round));
}

/* ---------------------------------------------------------------------------
 * The scale: four dials, and everything else derived from them.
 *
 * `--round` above is the pattern this generalises. One unitless number, three
 * radii, and you can drag it. What was missing was the same treatment for the
 * two things every component actually reaches for -- space and type -- which
 * is why they were being spelled as magic numbers instead: twenty distinct
 * spacing steps borrowed from a utility framework's scale, and ten font sizes
 * written as literal pixels, six of them fractional.
 *
 * Two rules hold this together and both are load-bearing.
 *
 * **The dials are unitless, and lengths are derived from them.** A number can
 * be multiplied; a length cannot be multiplied by another length. That is what
 * makes `--density` possible at all: one number, and the whole app goes from a
 * reading surface to a dense tool, which is impossible when the padding is
 * spelled out three hundred times.
 *
 * **The type scale is computed and then snapped to whole pixels** with
 * `round()`. A modular scale lands on values like 12.64, and a fractional
 * font-size gives a fractional line box -- which is exactly the bug that had
 * the message editor at 52.046875px against a 52px textarea, shoving the page
 * up by a pixel every time you typed. So the scale stays derived AND the
 * output stays integral, rather than choosing between them.
 *
 * `--prose-size` is deliberately NOT on this scale. A look tunes it optically
 * against the x-height of the typeface it chose -- 15.5px of Geist reads as
 * 17px of Newsreader -- and that is a judgement about a face, not a step in a
 * system. The chrome is systematic; the reading surface is tuned. See
 * `lib/looks.ts`.
 * ------------------------------------------------------------------------ */
:root {
  --space-base: 2;
  --density: 1;
  --type-base: 16;
  --type-ratio: 1.125;

  /* 2 4 6 8 12 16 24 32 48, from a base of 2 at 1 2 3 4 6 8 12 16 24.
     
     The steps are not a guess. Counted over the components before this
     landed, the single commonest spacing value in the app was 6px, 38 uses of
     it, because that is the padding that turns a 14px icon into a target you
     can hit -- and a scale built on a 4px base cannot say 6 at all. 2 is the
     gap between icons in a row, which is a real size rather than a rounding
     error. Above 16 the steps double, because at that size a reader cannot
     see a 4px difference anyway and offering one only invites hesitation. */
  --space-1: calc(round(var(--space-base) * 1px * var(--density), 1px));
  --space-2: calc(round(var(--space-base) * 2px * var(--density), 1px));
  --space-3: calc(round(var(--space-base) * 3px * var(--density), 1px));
  --space-4: calc(round(var(--space-base) * 4px * var(--density), 1px));
  --space-5: calc(round(var(--space-base) * 6px * var(--density), 1px));
  --space-6: calc(round(var(--space-base) * 8px * var(--density), 1px));
  --space-7: calc(round(var(--space-base) * 12px * var(--density), 1px));
  --space-8: calc(round(var(--space-base) * 16px * var(--density), 1px));
  --space-9: calc(round(var(--space-base) * 24px * var(--density), 1px));

  /* 10 11 13 14 16 18 20, from 16 at a major second. Named by their distance
     from the base rather than by t-shirt sizes, because `--text--2` says where
     it sits and `--text-sm` says only that somebody thought it was small. */
  --text--3: calc(
    round(
      var(--type-base) * 1px / var(--type-ratio) / var(--type-ratio) /
        var(--type-ratio),
      1px
    )
  );
  --text--2: calc(
    round(var(--type-base) * 1px / var(--type-ratio) / var(--type-ratio), 1px)
  );
  --text--1: calc(round(var(--type-base) * 1px / var(--type-ratio), 1px));
  --text-0: calc(var(--type-base) * 1px);
  --text-1: calc(round(var(--type-base) * var(--type-ratio) * 1px, 1px));
  --text-2: calc(
    round(var(--type-base) * var(--type-ratio) * var(--type-ratio) * 1px, 1px)
  );

  /* The wordmark, and nothing else. A single ratio is right for text and too
     slow for display -- reaching 38px from 16 at a major second takes seven
     steps -- so rather than bend the text scale or invent a display scale for
     one element, this is a judgement that says it is one, exactly like
     `--prose-size`. A second element wanting a display size is the signal to
     give display its own ratio. */
  --text-display: calc(round(var(--type-base) * 2.4 * 1px, 1px));

  /* Unitless, always. A leading with a unit computes once on the parent and
     inherits the RESULT, so a child at a different size gets the parent's
     leading -- which is how `text-[12.5px] leading-[18.75px]` came to be
     written out by hand in the sidebar, ratio already multiplied in. */
  --leading-tight: 1.35;
  --leading-body: 1.5;
  --leading-loose: 1.7;
}

/*
 * The dark neutrals sit on ONE hue at half the saturation they used to.
 *
 * They drifted: ground was hue 20, bg 34, and raised/hover 42-43 -- the
 * surfaces got yellower as they got lighter, which is backwards for a warm
 * neutral and put the most yellow value in the app on `--raised`, the sidebar,
 * the largest bright surface on screen. Everything neutral is hue 30 now, the
 * light theme's own tan family, at 10-12% saturation rather than 16-23%,
 * because the eye reads hue far more readily in a dark mid-grey than in a
 * light one and 20% at 12% lightness is a colour rather than a warmth.
 *
 * Lightness is a monotonic ramp, which it was not: `--raised` and `--hover`
 * were both L12, so hovering a row in the sidebar changed nothing at all.
 * Hover and active are translucent for the same reason -- one opaque value
 * cannot be correct against both the pane at L7 and the sidebar at L14, and
 * an overlay is right against whatever it lands on.
 */

/* The defaults of the first look, so the page is right before a line of script
 * has run. `applyLook` overwrites these as inline styles on the root. */
:root {
  --font-ui: "Geist Variable", sans-serif;
  --font-prose: "Newsreader Variable", Georgia, serif;
  --font-mono: "Geist Mono Variable", monospace;
  /* The chrome sits on the scale, with one unitless dial for the optical
     correction a look needs. Space Grotesk at 13px reads a size larger than
     Geist at 13px, and a look has to be able to say so -- but it says it as a
     RATIO and the result is snapped back to whole pixels, rather than writing
     `12.5px` and reintroducing the fractional line box the scale exists to
     avoid. Unset is 1, which is most looks. */
  --ui-adjust: 1;
  --code-adjust: 1;
  --ui-size: calc(round(var(--text--2) * var(--ui-adjust), 1px));
  --prose-size: 17px;
  /* Leading follows the MEASURE, not just the face. A look states the ratio
     at a 720px measure and a slope per 100px, both fitted to Golden Ratio
     Typography's own answers for that face (`lib/leading.ts`), and
     `--measure-n` is the column's rendered content width as a unitless
     number, written by `lib/measure.ts`. Unset it reads 720, the desktop
     default, until the observer's first frame. Same line on a phone's 340px
     and a 1200px column used to be the same leading, and was wrong at both.

     The calc that spends these two sits on the elements whose text they set,
     in `prose.css`: a `var()` inside a custom property is substituted on the
     element carrying the declaration, so on `:root` it read root's own
     `--measure-n` and a measure set further down could not reach it. */
  --leading-720: 1.75;
  --leading-slope: 0.044;
  --prose-tracking: 0.002em;
  --prose-weight: 400;
  --code-size: calc(round(var(--text--2) * var(--code-adjust), 1px));

  /* GRT's scale: six sizes at half-integer powers of φ from the prose size,
     `--prose-sN` being φ^(N/2). Snapped to whole pixels like the chrome
     scale, and for the same reason. The chrome stays on its own major-second
     scale: a φ^½ step is 27% and the chrome needs 12%. */
  --prose-s4: calc(round(var(--prose-size) * 2.618, 1px));
  --prose-s3: calc(round(var(--prose-size) * 2.058, 1px));
  --prose-s2: calc(round(var(--prose-size) * 1.618, 1px));
  --prose-s1: calc(round(var(--prose-size) * 1.272, 1px));
  --prose-s0: var(--prose-size);
  --prose-s-1: calc(round(var(--prose-size) * 0.786, 1px));

  /* Two steps below prose, and only two. The message pane had five sizes
     inside two and a half pixels of each other -- 13.5, 13, 12.5, 12, 11 --
     which is not a hierarchy, it is five things that all look slightly wrong
     next to each other. `--aside-size` is text you read but that is not the
     answer: your own message, the reasoning. `--meta-size` is text you look
     up: counts, models, toggles, hints. Prose, aside, meta.

     Your own turn is NOT an aside and does not use this. In an interactive
     fiction app the two speakers are writing the same kind of thing, so the
     turn carries the same type as the answer and the band is what says who
     wrote it. See `.turn-text`. */
  --aside-size: var(--text--1);
  --meta-size: var(--text--3);

  /* One column, one left edge. Everything in a turn hangs off this measure,
   * and anything carrying padding of its own bleeds outward by `--bleed` so
   * that its text still starts exactly here. */
  /* The column, and now the measure with it. This used to be two numbers --
     a 48rem column with text capped at 66ch inside it -- which left prose
     stopping 200px short of the code blocks beside it, and the left and right
     margins therefore nothing like each other. One number instead, on a
     slider, so the line length is set by the thing you can actually see. */
  --column: 48rem;
  --measure: 100%;
  --bleed: var(--space-6);
}

@theme {
  --font-sans: var(--font-ui);
  --font-serif: var(--font-prose);

  /* The spacing scale, handed to the utilities.
   *
   * The named steps below override the generated ones, so `p-4` is exactly
   * `--space-4` and reads as a quarter of `p-[var(--space-4)]`. That is also
   * the answer to why the app had 265 arbitrary-value utilities: Tailwind was
   * never told what the tokens were, so every use of one went through the
   * escape hatch.
   *
   * The base is deliberately NOT turned off. `--spacing: initial` closes the
   * set -- an off-scale `p-3.5` then generates no class at all -- but the same
   * base feeds `w-*`, `h-*`, `size-*` and `min-w-*`, and a width is not a
   * spacing step: `min-w-60` is a menu at 240px and belongs on no scale that
   * tops out at 48. Closing it took the sizes down with the padding. So the
   * closed set is enforced where it means something, by name, in
   * `test/web/scale.test.ts` -- the same arrangement as the type scale, and
   * for the same reason: the check knows which properties it is talking
   * about and the framework does not. */
  --spacing-0: 0px;
  --spacing-1: var(--space-1);
  --spacing-2: var(--space-2);
  --spacing-3: var(--space-3);
  --spacing-4: var(--space-4);
  --spacing-5: var(--space-5);
  --spacing-6: var(--space-6);
  --spacing-7: var(--space-7);
  --spacing-8: var(--space-8);
  --spacing-9: var(--space-9);

  --radius-sm: 6px;
  --radius-md: 10px;
  --radius-lg: 14px;

  --ease-out: cubic-bezier(0.2, 0.8, 0.2, 1);
  --animate-rise: rise 160ms cubic-bezier(0.2, 0.8, 0.2, 1);
  --animate-fade: fade 130ms cubic-bezier(0.2, 0.8, 0.2, 1);
  /* Entering an inline editor. Fast enough to feel like a response to the
     double-click rather than an animation you wait through: the field is
     usable before the motion finishes. Opacity only -- a scale, however
     slight, drags the glyphs across a few subpixels and reads as the text
     shifting rather than as the field arriving. */
  --animate-edit-in: edit-in 120ms cubic-bezier(0.2, 0.8, 0.2, 1);
  --animate-pulse-soft: pulse-soft 1.6s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}

@keyframes rise {
  from {
    opacity: 0;
    transform: translateY(4px);
  }
  to {
    opacity: 1;
    transform: none;
  }
}

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

@keyframes edit-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes pulse-soft {
  0%,
  100% {
    opacity: 1;
    transform: scale(1);
  }
  50% {
    opacity: 0.4;
    transform: scale(0.7);
  }
}

html,
body {
  height: 100%;
  /* Nothing at the document level scrolls: the conversation and the sidebar
     each scroll inside the shell. Saying so here is what stops a phone from
     scrolling the document to reveal a focused input. */
  overflow: hidden;
  overscroll-behavior: none;
}

/* The shell is pinned to the VISUAL viewport, whose size and offset
   src/zkit-design/viewport.ts writes as a phone keyboard comes and goes. Where
   that API does not exist nothing is written and the shell is the layout
   viewport, which is the desktop case. */
#root {
  position: fixed;
  top: var(--app-top, 0);
  left: 0;
  width: 100%;
  height: var(--app-height, 100%);
}

body {
  margin: 0;
  background: var(--ground);
  color: var(--fg);
  font-family: var(--font-ui);
  font-size: var(--ui-size);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* One ring, everywhere, and it is never removed -- only invisible until the
 * keyboard asks for it. */
@utility focus-ring {
  outline: 2px solid transparent;
  outline-offset: 2px;
  &:focus-visible {
    outline-color: var(--accent);
  }
}

::selection {
  background: var(--accent-tint);
  color: var(--fg);
}

* {
  scrollbar-width: thin;
  scrollbar-color: var(--hairline-strong) transparent;
}

*::-webkit-scrollbar {
  width: 11px;
  height: 11px;
}
*::-webkit-scrollbar-thumb {
  background: var(--hairline-strong);
  border-radius: 999px;
  border: 3.5px solid transparent;
  background-clip: content-box;
}
*::-webkit-scrollbar-thumb:hover {
  background: var(--fg-3);
  background-clip: content-box;
}

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.001ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.001ms !important;
    scroll-behavior: auto !important;
  }
}
