/*
 * tokens.css
 * Root variable contracts. Themes override any of these.
 * Import after Uno's preflights, before component utilities.
 */

/*
 * ── Breakpoints ─────────────────────────────────────────────────────
 *
 * sm 640px · md 768px · lg 1024px · xl 1280px · 2xl 1536px
 *
 * These are deliberately NOT custom properties. A custom property cannot
 * be used in a media query — `@media (min-width: var(--bp-md))` does not
 * work, and never has — so shipping --bp-* tokens would look themable
 * and silently do nothing. The numbers are written literally wherever
 * the package needs them, and documented here so consumers match.
 *
 * The scale is Tailwind's, which is also UnoCSS's default, so apps that
 * still run Uno alongside this package get one set of breakpoints
 * rather than two that nearly agree.
 */

/*
 * Border-box sizing. UnoCSS's preflight used to supply this; the package
 * depends on it (.field and .table are width:100% with padding and a
 * border), so it ships it now that there is no build step. It sits in the
 * earliest layer, making it the weakest rule here — any consumer reset
 * overrides it.
 */
*,
*::before,
*::after {
  box-sizing: border-box;
}

/*
 * `:host` beside `:root`, because a shadow tree has no document element.
 *
 * `:root` matches `<html>` and nothing else, so a stylesheet folded into a
 * shadow root — which is what a Sierra widget IS, one script carrying its own
 * CSS into a root on somebody else's page — declares these on no element at
 * all, and every `var(--…)` below resolves empty. The failure is partial and
 * therefore confusing: `box-sizing` and the `--space-*` rules are on `*` and do
 * cross, so a widget styled with this kit renders as a transparent box with the
 * spacing right, having shipped the whole kit to do it. Invariant 13 says this
 * is the styling language; it has to work in every surface that has one.
 *
 * `:host` matches nothing in a document, so this is a no-op there — and inside
 * a shadow root it declares the tokens on the host element, from which they
 * inherit into the tree exactly as `:root`'s do in a page.
 */
:root,
:host {
  /*
   * Lets a keyword be interpolated, which is what makes `height: 0` to
   * `height: auto` animatable at all — the thing CSS could not do for
   * twenty years and every accordion measured in JavaScript instead. It
   * inherits, so declaring it once here covers every element; a rule that
   * animates to a keyword without it simply snaps, with nothing to say why.
   */
  interpolate-size: allow-keywords;

  /* ── Semantic colors ──────────────────────────────────────── */
  --color-primary:   #0d83dd;
  --color-secondary: #1f2937;
  --color-muted:     #6b7280;
  --color-info:      #2EA2C9;
  --color-success:   #16a34a;
  --color-warning:   #d4b609;
  --color-danger:    #F4403A;

  /* ── Neutral surfaces ─────────────────────────────────────── */
  --surface:        #ffffff;
  --surface-raised: #ffffff;
  --surface-sunken: #f5f5f5;
  --ink:            #1c1b18;
  --ink-soft:       #5a564e;
  /*
   * --ink-mute is the lightest step of the ink ramp, and it is still body
   * text: placeholders, table headers, field hints, nav labels, .pagination-gap,
   * .empty — all at 11–13px, so AA's 4.5:1 applies, not the 3:1 large-text
   * threshold.
   *
   * It was #8b867b, which is 3.62:1 on --surface and 3.32:1 on
   * --surface-sunken. Every light theme inherited that, so every
   * placeholder and every table header in the package failed AA.
   *
   * The replacement is the same color scaled uniformly in linear RGB —
   * the same operation chip.css uses to cap a fill — so chromaticity is
   * exact and this is still the identical warm gray, only dark enough to
   * read: 4.92:1 on --surface, 4.51:1 on --surface-sunken, and still well
   * clear of --ink-soft so the three-step ramp survives.
   */
  --ink-mute:       #757067;
  --rule:           #e7e3d8;
  --rule-strong:    #d4cfc0;

  /*
   * ── Control height ────────────────────────────────────────
   *
   * The vertical padding a .btn and a .field BOTH read, which is what makes
   * an input and the button beside it the same height. They had two
   * different rungs — a button at --space-xs and a control at --space-sm,
   * everything else about them identical — so a filter bar came out with its
   * controls at 38px and its submit at 34px, and `.cluster`/`.bar`/`.toolbar`
   * all center, which put the button 2px low on the row it belongs to.
   *
   * An app could not fix it: a button's size is expressed as a FONT SIZE, so
   * the only knob that made a button taller made its text bigger too.
   *
   * It resolves to the taller of the two. 38px is no prize against the 44px
   * touch-target guidance, but it is the better of the pair, and the element
   * people tap and type into should not be the one that shrinks.
   *
   * The horizontal padding stays per-component — a button is wider than its
   * text on purpose and an input is not.
   */
  --control-padding-block: var(--space-sm);

  /* ── Component radii ──────────────────────────────────────── */
  --btn-radius:   6px;
  --card-radius:  8px;
  --field-radius: 6px;
  --pill-radius:  9999px;

  /*
   * ── Border weight ─────────────────────────────────────────
   *
   * The structural hairline: a Card's edge, a Field's border, a Table's
   * divider, the Topbar's rule under it. `--rule`/`--rule-strong` theme
   * the border's COLOR and `--*-radius` themes its corner, so a theme
   * could go dark, warm or square and could not go heavy — the whole
   * stamped/neo-brutalist family needs 2–3px and could only get there by
   * shipping a selector, which is the one thing a theme must not do.
   *
   * It is deliberately ONE token for the structure and two use-site
   * escapes for the parts that are their own decision:
   * `--field-border-width` and `--table-border-width` both fall back to
   * this, so a heavy theme gets heavy fields and heavy dividers for free
   * and can pull either back on its own.
   *
   * What does NOT read it: a spinner ring, a disclosure chevron, a
   * tooltip's arrow, a step marker's disc. Those are drawn WITH the
   * border property and are geometry rather than a border — scaling them
   * with the theme's hairline distorts a shape instead of thickening a
   * line.
   */
  --border-width: 1px;

  /*
   * ── Resting elevation on the Block tier ───────────────────
   *
   * `none`, and it is a token rather than an omission. A resting Card is
   * a border and nothing else, which is a good default — elevation is for
   * things that float — but it was not STATABLE: a theme could restyle
   * every overlay's shadow through --shadow-sm/md/lg and not a single
   * card's, so press.css's stamped offset landed on every button and
   * every dialog while the cards it was designed for stayed flat.
   *
   * Read by the surface lineage's base rule. `.raised` is the elevation
   * ladder and still takes --shadow-md.
   */
  --surface-shadow: none;

  /*
   * --badge-radius is deliberately NOT declared here. Badges follow
   * --btn-radius via a use-site fallback in badges.css, so a theme that
   * squares off buttons squares off badges too, for free.
   *
   * Declaring `--badge-radius: var(--btn-radius)` at :root would look
   * equivalent and silently isn't: the var() resolves once, against
   * :root's own --btn-radius, and the computed 6px then inherits past
   * any .theme-* override. Alias tokens have to resolve at the use site.
   *
   * Set --badge-radius in a theme only to diverge from --btn-radius.
   */

  /* ── Button typography ────────────────────────────────────── */
  --btn-font-weight:    600;
  --btn-text-transform: none;
  --btn-letter-spacing: normal;

  /* ── Pill typography ──────────────────────────────────────── */
  --pill-font-weight:    600;
  --pill-text-transform: none;
  --pill-letter-spacing: normal;

  /* ── Badge typography ─────────────────────────────────────── */
  --badge-font-weight:    600;
  --badge-text-transform: uppercase;
  --badge-letter-spacing: 0.04em;

  /*
   * ── The small label ───────────────────────────────────────
   *
   * A table head, a tile's label and a nav group's heading are one
   * typographic role — the small caps that name a region — and they had
   * three different answers to it: 600/0.04em, 500/0.04em and
   * 700/0.06em, none of them documented and none reachable from a theme.
   * So a theme could stamp its buttons and could not touch the three
   * places a house voice shows next.
   *
   * One triple, three readers. The weights are unified rather than each
   * kept behind its own escape: three values for one role is the thing
   * being fixed, not a constraint to preserve.
   */
  --label-font-weight:    600;
  --label-text-transform: uppercase;
  --label-letter-spacing: 0.04em;

  /*
   * ── Motion ────────────────────────────────────────────────
   *
   * Every transition in the package used to be a literal, so nine themes
   * moved identically and a house style that wanted no motion at all had
   * nowhere to say so. Four durations, because the package genuinely uses
   * four, and each is named for the JOB rather than for its length:
   *
   *   --motion-fast   a color, a border, a background changing
   *   --motion-base   a control changing shape — a switch, a chevron
   *   --motion-enter  something arriving or leaving (overlays.css)
   *   --motion-slow   a measurement moving — a progress bar
   *
   * `--motion-spin` is separate and its easing is not a token: a spinner
   * that eases reads as broken hardware rather than as a slower spinner,
   * so `linear` is written at the use site.
   *
   * This is the design's setting. `prefers-reduced-motion` below is the
   * READER's, it wins, and the two are not the same question — which is
   * why a theme setting every duration to 0 is a house style and does not
   * make the media query redundant.
   */
  --motion-fast:  120ms;
  --motion-base:  150ms;
  --motion-enter: 160ms;
  --motion-slow:  200ms;
  /*
   * Two loops, two durations, both named for the job. A spinner's
   * revolution and a skeleton's sweep are 2.3x apart and neither is a
   * transition: they run until the work finishes.
   */
  --motion-spin:    600ms;
  --motion-shimmer: 1.4s;

  --motion-ease:       ease;
  --motion-ease-out:   ease-out;
  --motion-ease-enter: cubic-bezier(0.16, 1, 0.3, 1);

  /*
   * ── Container ─────────────────────────────────────────────
   * --container-pad steps up at the breakpoints; see layout.css.
   */
  --container-max:    1280px;
  --container-narrow: 768px;
  --container-pad:    1rem;

  /* ── App frame ────────────────────────────────────────────── */
  --topbar-height:  3.5rem;
  --sidebar-width:  15rem;
  --screen-pad:     1.5rem;

  /*
   * ── Tone as text ──────────────────────────────────────────
   *
   * The lightness window a tone may occupy, in oklch, when it is rendered
   * as TEXT rather than as a fill. The tones are tuned as fills behind
   * white text and mostly fail the other job, so a component clamps into
   * this range before coloring text with one — a no-op where the tone
   * already reads. `--tone-ink` in tones.css is the derivation; code.css
   * and buttons.css are the readers.
   *
   * These are the LIGHT-surface bounds. A dark theme must invert them,
   * because CSS cannot work it out: relative color syntax exposes the
   * channels of the tone being transformed, never those of the surface it
   * will land on. dark.css, basecamp.css and field.css do; see tones.css.
   */
  --tone-l-min: 0;
  --tone-l-max: 0.46;

  /* ── Shadows ──────────────────────────────────────────────── */
  --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.06);
  --shadow-md: 0 1px 3px rgba(0, 0, 0, 0.06), 0 1px 2px rgba(0, 0, 0, 0.04);
  --shadow-lg: 0 1px 3px rgba(0, 0, 0, 0.1), 0 4px 12px rgba(0, 0, 0, 0.06);

  /*
   * The dim behind an overlay. A literal rather than a blend of --ink,
   * because --ink inverts with the theme and a scrim never does: mixed from
   * ink it would go WHITE on a dark theme, which is the one thing a backdrop
   * cannot be. It is a token so a theme can retune the depth — and so the
   * package's own dialogs and a kit component that portals its own backdrop
   * dim by the same amount, which they did not: @frontierjs/ui's command
   * palette sat at 0.72 against these at 0.45.
   */
  --scrim: rgba(0, 0, 0, 0.45);

  /* ── Focus ring ───────────────────────────────────────────── */
  /*
   * The whole recipe, and the only knobs it has. One rule reads these —
   * focus.css — and every focusable surface in the package goes through
   * it. See that file for why the ring is an outline, and for how a
   * component varies the color without restating the shape.
   *
   * --ring-width was 3px while it only reached the three box-shadow
   * halos. Those were translucent, so they needed the extra spread to
   * read at all. A solid ring does not, and 2px is what every ring added
   * later in v0.6 had already hardcoded.
   */
  --ring-width:  2px;
  --ring-offset: 2px;

  /*
   * --ring is deliberately NOT declared here, for the same reason
   * --badge-radius is not. It follows --color-primary, and the working
   * form of that is a use-site fallback: `var(--ring, var(--color-primary))`
   * wherever a ring color is read.
   *
   * `--ring: var(--color-primary)` at :root looks equivalent and silently
   * is not. The var() resolves once, against :root's own --color-primary,
   * and the resulting computed color inherits straight past any .theme-*
   * override. That is what shipped through v0.6: every theme changes
   * --color-primary, no theme sets --ring, and so every focus ring in
   * every theme was the default blue. Elite's lime brand focused in blue.
   *
   * Set --ring in a theme only to diverge from --color-primary.
   */

  /*
   * ── Fonts ─────────────────────────────────────────────────
   *
   * --font-display is what h1–h6 read. It defaults to --font-primary, so an
   * app that sets one face still has one face and this token is invisible.
   * Set it to diverge — a serif over a sans body is the common case, and
   * without a name for it the only way to say so was to restate the family
   * on every heading rule, which is what the guide did in four places.
   *
   * A literal, not `var(--font-primary)`: substituting here would resolve
   * once against :root and inherit past any scoped --font-primary override.
   * The heading rule reads `var(--font-display, var(--font-primary))` at the
   * USE site instead, which is the form that follows a scope. Same trap as
   * --ring, documented above.
   *
   * --heading-letter-spacing is the sibling and takes the same shape: unset
   * it is `normal`, and typography.css reads it at the use site. Buttons,
   * badges and pills each had one and headings did not, so a theme wanting a
   * tracked display face had to ship a selector — which is the one thing a
   * theme may not do.
   */
  --font-primary: system-ui, sans-serif;
  --font-mono:    ui-monospace, monospace;

  /*
   * ── Type scale ────────────────────────────────────────────
   *
   * One ladder for the whole package. --text-md is the body size and the
   * rung everything else is measured from. `.text-*` in utilities.css and
   * h1–h6 in typography.css read the SAME rungs, which is why .text-xl and
   * h4 are the same size.
   *
   * Every rung is rem. A px size ignores the reader's raised base font, so
   * a px and a rem spelling of the same nominal size are accessible in one
   * place and not the other. No component declares a literal font-size;
   * test/specs/type.spec.js fails if one appears.
   *
   * Values are literal here — never `--text-sm: var(--text-md)`. A var() at
   * :root resolves once against :root's own value, so a theme that retunes
   * --text-md would not move a rung defined in terms of it, and nothing
   * would say so. Same trap as --ring and --badge-radius above.
   *
   * An app running UnoCSS that prefers Uno's 4px scale retunes these and
   * gets one scale across both — see "Using it with UnoCSS" in README.md.
   */
  --text-2xs: 0.6875rem;  /* 11px — the uppercase overline: table head, nav group label */
  --text-xs:  0.75rem;    /* 12px — badge, pill, tooltip, field hint */
  --text-sm:  0.8125rem;  /* 13px — caption, meta, table cell, help */
  --text-md:  0.875rem;   /* 14px — BODY. nav, tab, button, label */
  --text-lg:  1rem;       /* 16px — h5, alert icon */
  --text-xl:  1.125rem;   /* 18px — subhead, h4, empty-state title, the lead paragraph under a page title */
  --text-2xl: 1.375rem;   /* 22px — h3, dialog title */
  --text-3xl: 1.75rem;    /* 28px — h2, tile figure */
  --text-4xl: 2.25rem;    /* 36px — h1 */

  /*
   * Line height rides with the size: small text needs proportionally more
   * leading to stay readable, a display heading needs less or it looks
   * gappy. Unitless, so a nested element multiplies its OWN font-size
   * rather than inheriting a computed pixel leading.
   */
  --leading-display: 1.1;   /* h1 */
  --leading-heading: 1.2;   /* h2–h6 */
  --leading-snug:    1.45;
  --leading-normal:  1.5;
  --leading-body:    1.55;  /* the body default */
  --leading-relaxed: 1.6;

}

/*
 * ── Density ───────────────────────────────────────────────────
 *
 * The third free-standing axis, and the exact mirror of a tone.
 *
 * A tone is registered `inherits: false` because it is a fact about ONE
 * element — a danger Card must not make the button inside it red. Density
 * is a fact about a REGION, so it inherits: set it on a Pane and every
 * Card, Row, Field and Table inside tightens, with no component told
 * anything and no per-component `-sm` name.
 *
 * That is the difference from every other framework's answer to this.
 * Bootstrap has `.btn-sm`, `.table-sm`, `.form-control-sm`; Bulma has
 * `is-small` per component; Radix and MUI take a prop on each component.
 * All of them name the combination. This names the axis.
 *
 * Registered with an initial-value so the multiplication below always
 * resolves. Unregistered, --density would be guaranteed-invalid wherever
 * nobody set it and every rung's calc() would fall over, taking all
 * padding with it. `<number>` also means it interpolates as a number
 * rather than flipping at the halfway point, so density can animate.
 */
@property --density {
  syntax:        "<number>";
  inherits:      true;
  initial-value: 1;
}

.dense { --density: 0.8; }
.roomy { --density: 1.25; }

/*
 * ── Derived density ───────────────────────────────────────────
 *
 * The same axis, answered by the box instead of by you: a Card in a narrow
 * sidebar should be tighter than the identical Card in the main column,
 * and nobody should have to remember to type that.
 *
 * ── Why this is opt-in and not on .card ───────────────────────
 *
 * `container-type: inline-size` is not free. It applies inline-size
 * containment, so the element can no longer be sized by its contents —
 * measured here, a .card in a .cluster went from 83px to 42px, the width
 * of its own padding, and the same in an auto-sized grid track and inside
 * an inline-block. It also makes the box a containing block for
 * position: fixed descendants, so an app's toast rendered inside one
 * drifts. (Dialog and Popover are unaffected: the top layer escapes
 * containment.) Shipping that on a component would break layouts nobody
 * changed.
 *
 * So the app names the box and the package owns the response:
 *
 *   .my-sidebar { container: fjs / inline-size; }
 *
 * The name is required, so this fires only where it was asked for — an
 * unnamed query would react to any container an app happened to create.
 *
 * `*` is (0,0,0), so a stated `.dense` or `.roomy` inside a narrow box
 * still wins: DECLARED BEATS DERIVED, which is the only rule worth
 * remembering here.
 */
@container fjs (inline-size < 30rem) {
  * { --density: 0.9; }
}

@container fjs (inline-size < 20rem) {
  * { --density: 0.8; }
}

/*
 * ── The space ladder ──────────────────────────────────────────
 *
 * The other half of the type scale, and it did not exist: padding was
 * written as a literal rem in every file, which is why size leaked out as
 * scoped modifiers instead — `table.compact`, `bar.narrow`, `bar.wide`.
 * Each of those is a density decision wearing a component's name, and
 * there was nowhere else for it to live.
 *
 * Rungs are on a 2px grid to 16px and coarser above. 10px and 14px are
 * rungs because the package genuinely uses them; rounding them into a
 * prettier ladder would move real layout to flatter a table.
 *
 * ── Why the universal selector ────────────────────────────────
 *
 * This is the same mechanism tones.css uses for the tint ramp, and for the
 * same reason. `--space-sm: calc(0.5rem * var(--density))` at :root would
 * substitute --density ONCE, against :root, and inherit that fixed length
 * past every .dense — the alias trap ruled on 2026-08-02, which is silent
 * because the token still has a perfectly good value. Declared on every
 * element, each one recomputes the rung from the density it inherits.
 *
 * ── Two knobs, and why the shape needs the second one ─────────
 *
 * --density scales the whole ladder uniformly. That is the axis, and for
 * a while it was the ONLY thing a theme could reach: a rung redeclared in
 * a theme file is (0,1,0) and wins on the themed element alone, because
 * every descendant matches the rule below and gets the package's rung
 * back. So a theme could make everything tighter and could not make a
 * ladder that is tight at the small rungs and generous at the large ones
 * — the difference between a data-dense console and an editorial page.
 *
 * The rung is therefore `base × density`, where the BASE is an ordinary
 * inheriting custom property. A theme sets `--space-2xl-base` on its own
 * class, it inherits like any value, and every descendant recomputes its
 * rung from the base it inherited and the density it inherited. Neither
 * half is resolved early, so neither hits the alias trap.
 *
 * An app still has both: `html { --density: 1.15 }` for everything
 * roomier, `--space-*-base` for a different ladder shape. A rung set
 * DIRECTLY on an element (`--space-2xl: 3rem`) still loses to this rule
 * on that element's descendants — set the base instead.
 */
*,
*::before,
*::after {
  --space-3xs: calc(var(--space-3xs-base, 0.125rem) * var(--density));  /*  2px */
  --space-2xs: calc(var(--space-2xs-base, 0.25rem)  * var(--density));  /*  4px */
  --space-xs:  calc(var(--space-xs-base,  0.375rem) * var(--density));  /*  6px */
  --space-sm:  calc(var(--space-sm-base,  0.5rem)   * var(--density));  /*  8px */
  --space-md:  calc(var(--space-md-base,  0.625rem) * var(--density));  /* 10px */
  --space-lg:  calc(var(--space-lg-base,  0.75rem)  * var(--density));  /* 12px */
  --space-xl:  calc(var(--space-xl-base,  0.875rem) * var(--density));  /* 14px */
  --space-2xl: calc(var(--space-2xl-base, 1rem)     * var(--density));  /* 16px */
  --space-3xl: calc(var(--space-3xl-base, 1.25rem)  * var(--density));  /* 20px */
  --space-4xl: calc(var(--space-4xl-base, 1.5rem)   * var(--density));  /* 24px */
  --space-5xl: calc(var(--space-5xl-base, 2rem)     * var(--density));  /* 32px */
  --space-6xl: calc(var(--space-6xl-base, 3rem)     * var(--density));  /* 48px */
}

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

  /*
   * Spinners are the exception: the animation *is* the message. Frozen, a
   * spinner reads as a broken page rather than a working one. So slow it
   * to a rate that still says "in progress" without the fast rotation
   * that triggers vestibular symptoms, and keep it looping.
   *
   * This has to live here, in the same layer as the guard above, because
   * layer order is REVERSED for !important declarations — an !important
   * in the first layer beats one in the last. Put this exception in a
   * later file and the guard would silently win.
   *
   * Among !important declarations specificity still applies normally, so
   * .spinner (0,1,0) beats * (0,0,0).
   */
  .spinner,
  .btn.loading::after {
    animation-duration:        1.6s !important;
    animation-iteration-count: infinite !important;
  }
}
