/* Design tokens and cascade-layer order.
   Two tiers: raw --lch-* triplets, left unwrapped so alpha composes at the
   point of use, under semantic --color-* names that are the only thing the
   rest of the app is allowed to reference. Dark mode re-authors the raw tier;
   the semantic tier is written once and never touched, so it re-resolves.
   The palette is Hanshi — see docs/design/HANSHI.md. */
@layer reset, base, components, modules, utilities;

:root {
  /* Raw color tier: oklch triplets (lightness chroma hue), kept unwrapped so
     alpha can be composed at the point of use: oklch(var(--lch-ink) / 50%).
     Most of the tier is re-authored for dark mode below — semantic names stay
     valid. The Fixed block is the exception and the reason the split exists. */

  /* Fixed — never re-authored for dark, the way Fizzy keeps its own two.
     Shadows and scrims compose from these; if they flipped, a shadow would
     become a white glow on dark paper. */
  --lch-black: 0% 0 0;
  --lch-shadow: 24% 0.012 75; /* warm, per HANSHI's single shadow */

  --lch-paper: 98.8% 0.004 88; /* warm near-white, never pure white */

  /* Ink — warm gray (hue 75–88), darkest (text) to lightest (surface tint) */
  --lch-ink-darkest: 24% 0.012 75;
  --lch-ink-darker: 32% 0.013 75;
  --lch-ink-dark: 46% 0.016 75;
  --lch-ink: 58% 0.016 78;
  --lch-ink-light: 72% 0.014 80;
  --lch-ink-lighter: 91% 0.008 86;
  --lch-ink-lightest: 97.2% 0.006 88;

  /* Accent — shu red; also serves "negative" (deliberate: one hue budget).
     Every tint (~94.5% L, ≤.03 C) has a paired -ink (~40% L, same hue) for
     text set on that tint. Never put default ink on a tint. */
  --lch-red-dark: 46% 0.14 33;
  --lch-red: 56% 0.15 33;
  --lch-red-tint: 94.5% 0.03 33;
  --lch-red-ink: 40% 0.09 33;

  /* Semantic states — moss (done), straw (waiting), mizu (selected/you) */
  --lch-green: 50% 0.08 145;
  --lch-green-tint: 94.5% 0.028 145;
  --lch-green-ink: 38% 0.06 145;
  --lch-amber: 60% 0.09 85;
  --lch-amber-tint: 95% 0.032 85;
  --lch-amber-ink: 43% 0.07 85;
  --lch-blue: 52% 0.07 250;
  --lch-blue-tint: 94.5% 0.024 250;
  --lch-blue-ink: 40% 0.05 250;

  /* Semantic color tier — style against these, never the raw tier */
  --color-canvas: oklch(var(--lch-paper));

  /* The wash is an ALPHA of the ink, not an opaque near-white. It therefore
     composes over whatever is beneath it — paper, a card, a tint, dark paper —
     so one token is the hover state everywhere instead of a colour that only
     works on one background. This is the whole reason the raw tier is left
     unwrapped, and in Basecamp the equivalent token is the single most-used
     background in the product. The ladder extends by adding a step, not by
     adding a colour. */
  --color-surface: oklch(var(--lch-ink-darkest) / 3%);

  /* The scrim behind a dialog. Warm rather than neutral, and composed from the
     fixed tier so it darkens the page in both themes. */
  --color-scrim: oklch(var(--lch-shadow) / 25%);

  --color-ink: oklch(var(--lch-ink-darkest));
  --color-ink-subtle: oklch(var(--lch-ink-dark));
  --color-ink-faint: oklch(var(--lch-ink));
  --color-line: oklch(var(--lch-ink-lighter));
  --color-accent: oklch(var(--lch-red));
  --color-accent-strong: oklch(var(--lch-red-dark));
  --color-accent-tint: oklch(var(--lch-red-tint));
  --color-accent-ink: oklch(var(--lch-red-ink));
  --color-link: var(--color-ink); /* links are ink + underline; shu is spent on focus only */
  --color-positive: oklch(var(--lch-green));
  --color-positive-tint: oklch(var(--lch-green-tint));
  --color-positive-ink: oklch(var(--lch-green-ink));
  --color-warning: oklch(var(--lch-amber));
  --color-warning-tint: oklch(var(--lch-amber-tint));
  --color-warning-ink: oklch(var(--lch-amber-ink));
  --color-negative: oklch(var(--lch-red));
  --color-negative-tint: oklch(var(--lch-red-tint));
  --color-negative-ink: oklch(var(--lch-red-ink));
  --color-selected: oklch(var(--lch-blue-tint)); /* mizu — selection and "you" in chat */
  --color-selected-ink: oklch(var(--lch-blue-ink));
  --color-highlight: oklch(var(--lch-amber-tint)); /* straw — also internal notes */

  color-scheme: light;

  /* Type — 16px root, six steps, fluid between a narrow floor and a wide
     ceiling. The steps themselves are declared on every element below, not
     here; see the note there for why. */
  --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    "Noto Sans", "Helvetica Neue", Arial, sans-serif;
  --font-mono: ui-monospace, "SF Mono", "Cascadia Mono", Menlo, Consolas, monospace;

  /* Space — text-relative horizontally, rhythm-relative vertically.
     Surfaces retune density by redefining these on a container. */
  --inline-space: 1ch;
  --block-space: 1rem;

  /* Hairlines, corners, elevation.
     Two radii: 4px on flat surfaces (cards, inputs, chips-as-rects),
     6px reserved for things that float (dialogs, popovers, slideovers). */
  --border-size: 1px;
  --border: var(--border-size) solid var(--color-line);
  --border-radius: 0.25rem;
  --border-radius-float: 0.375rem;
  --shadow: 0 8px 28px oklch(var(--lch-shadow) / 9%); /* one warm shadow, floats only */

  /* Layers */
  --z-raised: 1;
  --z-popup: 10;
  --z-panel: 30;
  --z-chrome: 50;
  --z-overlay: 100;

  /* Motion — the sheet-push and the side-panel slides are the only
     transitions in the app */
  --speed-quick: 120ms;
  --speed: 200ms;
  --ease-out: cubic-bezier(0.22, 1, 0.36, 1);
  --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1);

  /* Focus — the one place shu appears outside chips and destructive rows.
     The size is a token because the same 2px shu mark is also the written
     line's focus baseline, the lean composer's margin line, and the ring the
     Lexxy editor draws on its own controls. */
  --focus-ring-size: 2px;
  --focus-ring: var(--focus-ring-size) solid var(--color-accent);
  --focus-offset: 2px;

  /* The notch and the home indicator. The layout ships viewport-fit=cover, so
     iOS hands us the whole screen and expects us to say which parts of it are
     safe — without these, the fixed header sits under the notch and the fixed
     footer under the home indicator. Named rather than used raw (fizzy), so a
     native shell can inject its own values over the env() defaults. */
  --safe-inset-top: env(safe-area-inset-top, 0px);
  --safe-inset-right: env(safe-area-inset-right, 0px);
  --safe-inset-bottom: env(safe-area-inset-bottom, 0px);
  --safe-inset-left: env(safe-area-inset-left, 0px);

  /* What a finger needs. HANSHI rules the disc at 26–28px and that stays the
     drawn size; on a touch screen the button grows an invisible halo to this
     instead (buttons.css) — fizzy's .btn--ensure-tap-target-size, applied to
     every circle rather than remembered per call site. */
  --tap-target-size: 2.75rem;
}

/* The derived space steps, declared on every element and not on :root: a
   custom property's var() resolves where the property is DECLARED, so a
   :root-declared half would bake in :root's --block-space once and ignore
   every container retune — which silently broke the density mechanism the
   comment above promises. Declared universally, each element derives its
   halves from the --block-space it actually inherits. A surface that sets
   its own -half still wins on specificity. */
*,
::before,
::after {
  --inline-space-half: calc(var(--inline-space) / 2);
  --inline-space-double: calc(var(--inline-space) * 2);
  --block-space-half: calc(var(--block-space) / 2);
  --block-space-double: calc(var(--block-space) * 2);

  /* ——— Fluid type ———
     Basecamp's mechanism, not Fizzy's (user, 2026-08-11, after reading all
     three). Fizzy bumps the ROOT to 19px past 100ch, which drags every rem in
     the app with it — spacing, bar heights, the panel — and is why it reads
     large. Basecamp instead keeps a fixed root and makes the type itself
     fluid: `clamp(min, Ncqi, max)` per component (docs/research/bc-design —
     `clamp(1.6rem, 6cqi, 2rem)` is its body, 16px→20px). Writebook does the
     same with four reusable tokens. We take that mechanism at BC's scale but
     a calmer ceiling: +12.5% rather than their +25%.

     ONE fluid value, five ratios. --text-normal is the fluid unit and every
     other step is the ratio it already had, so the scale HANSHI ruled is
     untouched and there is exactly one number to retune. The ramp runs from a
     400px sheet (16px, the floor a phone sits on) to a 900px one (17px), flat
     outside that — no breakpoint, so nothing jumps.

     The ceiling was 18px for an afternoon and read too big on a desktop
     (user, 2026-08-11). 17px is the whole change: +6.25% at the top rather
     than +12.5%, enough that a wide sheet is not set at phone size and little
     enough that the calm register survives. To retune again, move the third
     clamp argument and the middle term with it — the middle is
     `ceiling - 4 * slope`, and the slope is `(ceiling - floor) / 5` per cqi,
     which is what puts the floor at a 400px sheet and the ceiling at 900px.

     Declared here rather than on :root for the reason the spacing steps above
     are: a custom property's var() resolves where it is DECLARED, and `cqi`
     is resolved the same way. On :root it would bake in the root's context
     once — and the root is not inside any container, so every sheet would get
     the same size forever. Per element, each resolves against the container it
     actually sits in, which is #main — and #main narrows when the panel opens,
     which is the width the text should answer to (AGENTS.md).

     Outside a container — the header and footer are — `cqi` falls back to the
     small viewport by spec, so the chrome tracks the window instead. That is
     the right answer there and needs no second rule. */
  --text-normal: clamp(1rem, 0.95rem + 0.2cqi, 1.0625rem);
  --text-x-small: calc(0.7 * var(--text-normal));
  --text-small: calc(0.85 * var(--text-normal));
  --text-medium: calc(1.15 * var(--text-normal));
  --text-large: calc(1.4 * var(--text-normal));
  --text-x-large: calc(1.8 * var(--text-normal));
}

/* Dark mode: redefine the raw tier (explicit choice wins over the OS).
   Lightness is compressed, not inverted; hue stays warm so dark paper
   reads as lamplit washi, not slate. */
html[data-theme="dark"] {
  --lch-paper: 15% 0.010 80;

  --lch-ink-darkest: 92% 0.006 85;
  --lch-ink-darker: 83% 0.008 84;
  --lch-ink-dark: 71% 0.010 82;
  --lch-ink: 57% 0.012 80;
  --lch-ink-light: 42% 0.012 78;
  --lch-ink-lighter: 29% 0.010 77;
  --lch-ink-lightest: 20% 0.010 76;

  --lch-red-dark: 72% 0.11 33;
  --lch-red: 64% 0.13 33;
  --lch-red-tint: 27% 0.05 33;
  --lch-red-ink: 82% 0.06 33;

  --lch-green: 66% 0.10 145;
  --lch-green-tint: 26% 0.04 145;
  --lch-green-ink: 80% 0.06 145;
  --lch-amber: 72% 0.11 85;
  --lch-amber-tint: 27% 0.05 85;
  --lch-amber-ink: 82% 0.07 85;
  --lch-blue: 68% 0.08 250;
  --lch-blue-tint: 26% 0.04 250;
  --lch-blue-ink: 80% 0.05 250;

  --shadow: 0 8px 28px oklch(var(--lch-shadow) / 35%);

  color-scheme: dark;
}

@media (prefers-color-scheme: dark) {
  html:not([data-theme]) {
    --lch-paper: 15% 0.010 80;

    --lch-ink-darkest: 92% 0.006 85;
    --lch-ink-darker: 83% 0.008 84;
    --lch-ink-dark: 71% 0.010 82;
    --lch-ink: 57% 0.012 80;
    --lch-ink-light: 42% 0.012 78;
    --lch-ink-lighter: 29% 0.010 77;
    --lch-ink-lightest: 20% 0.010 76;

    --lch-red-dark: 72% 0.11 33;
    --lch-red: 64% 0.13 33;
    --lch-red-tint: 27% 0.05 33;
    --lch-red-ink: 82% 0.06 33;

    --lch-green: 66% 0.10 145;
    --lch-green-tint: 26% 0.04 145;
    --lch-green-ink: 80% 0.06 145;
    --lch-amber: 72% 0.11 85;
    --lch-amber-tint: 27% 0.05 85;
    --lch-amber-ink: 82% 0.07 85;
    --lch-blue: 68% 0.08 250;
    --lch-blue-tint: 26% 0.04 250;
    --lch-blue-ink: 80% 0.05 250;

    --shadow: 0 8px 28px oklch(var(--lch-shadow) / 35%);

    color-scheme: dark;
  }
}
