/* The AOS token layer — one ruled value per mode for every name the corpus was arguing about.

   Eleven explorations declared 144 root names between them, 51 shared and 32 of those carrying more
   than one value. Every one of them resolves here or retires with its replacement named, and
   `dispositions.json` is where that migration is written down.

   A token's role decides everything about it. `surface`, `ink`, `stroke`, `hairline`, `scrim` and
   `elevation` carry no meaning a reader is asked to tell apart, so they invert by rule and neither
   ground carries a rung the other lacks. `hue`, `signal` and `accent` do carry one, so each is cut
   for its ground and measured on it — rulings.md §Q1, and the bill `calendar/exp-2light` actually
   paid. Everything else is one value on both grounds.

   The floors follow the role too, which is why no rung carries a marker of its own: `ink` and
   `signal` answer to the text floor, `stroke` and `hue` to the UI floor, and a role that answers to
   neither is a role that never carries text. That is what "marked icon-only" means here, and it is
   why a rung below the text floor cannot quietly become a label — it would have to be renamed first.

   `signal` and `hue` sit on either side of that line, and the two ramps look different on paper
   because of it. A signal *speaks*: it sets the words under an invalid field and the label on a stop
   button, so it is held to the text floor and is deep on the light ground because a yellow cut to
   4.5 : 1 is a brown. A hue is *picked*: the dot on a label, the block on a calendar, a series on a
   chart. Held to a floor for text it never carries, the light ramp lost a tenth of its lightness and
   most of its chroma, and `--hue-amber` lost its life — at one lightness and one chroma the two
   ramps had nothing left to tell a 10° difference apart.

   A floor is measured on the deepest surface a name can land on, not on the base rung. `--surface-000`
   is the one ground where a colour cut to exactly 4.5 : 1 clears, and every container in the kit sits
   a rung or three above it — so a ramp measured there is a ramp that fails everywhere it is spent.
   Both ramps below clear the text floor on `--surface-300`, and the check reads them there.

   A composite — a token whose value names another token — is declared in **both** ground blocks.
   `var()` inside a custom property is substituted where the property is declared, not where it is
   spent, so a composite written once at the root carries the dark ground's values into a nested
   light one. `--border-state` did exactly that for as long as it was declared once.

   The layer stands alone: one link over file://, no build step, nothing to fetch. The accent is the
   one value it does not hold, because the accent belongs to the locked mark pack and is read from
   it by slug — apps-design-registry.md publishes both of an accent's dispositions, and an app hands
   them in through `--app-accent` and `--app-accent-paper`.

   node design/ui-design-system/ui-kit/tools/foundations-check.mjs */

:root,
[data-ground="dark"] {
  color-scheme: dark;

  --surface-000: #000000;
  --surface-100: #0E0E11;
  --surface-200: #16161A;
  --surface-300: #1E1E23;

  --ink: #FFFFFF;
  --ink-body: rgba(255, 255, 255, 0.78);
  --ink-muted: rgba(255, 255, 255, 0.60);
  --ink-subtle: rgba(255, 255, 255, 0.46);

  --stroke: rgba(255, 255, 255, 0.40);
  /* The ring's own stroke. It was `--ink`, which is the loudest value the layer holds — 16.60 : 1
     against the deepest rung it can land on, on a shape that is drawn around whatever the keyboard
     is standing on and is therefore on screen for the whole of a keyboard-driven pass. Cut to 6.90 on
     this ground and 6.82 on paper it is well under half the shout and still more than twice the UI
     floor a focus indicator answers to, which is the margin the halved width is spent out of. It is a `stroke` rather than a `focus`
     name so that the floor is enforced on it by role: the check reads it against every rung of the
     ladder, on both grounds, the way it reads `--stroke`. The two grounds are matched rather than
     eyeballed — 8.73 and 8.72 on their own base rungs, which is the same ring on both. */
  --stroke-focus: #A7A7A7;
  --hairline: rgba(255, 255, 255, 0.16);
  --hairline-faint: rgba(255, 255, 255, 0.08);
  --scrim: rgba(0, 0, 0, 0.62);

  /* The four interaction rungs are cut from the control's own ink rather than from a fixed white or
     black, so one recipe serves every fill: a 6% white laid over a near-white primary plate is that
     plate again, which is how hover, press and rest came to render identically. They are `state` and
     not `surface` because a rung of the ladder is a ground a reader stands things on and these are
     an overlay on whatever the ladder already put down.

     **The share is per ground, and it was not.** One set of percentages served both, which reads as
     one rule and is not: the same share of ink lands a different distance from its ground depending
     on which end of the range that ground sits at. Measured on the base rung each ground publishes —
     which is where a toolbar, a rail and this kit's own specimen cell all put a control — an 8%
     hover was 1.19 : 1 on paper and 1.12 : 1 here, and the paper figure is the one the drawing was
     tuned against. A hover nobody can see is a control that does not answer the pointer, and the
     icon button is where it showed worst: its whole drawing is a stroke, so the plate is the only
     thing hover changes. The dark shares are opened until the two grounds are separated alike, and
     the light ones keep the values they were tuned at. */
  --state-hover: color-mix(in srgb, currentColor 14%, transparent);
  --state-press: color-mix(in srgb, currentColor 28%, transparent);
  --state-selected: color-mix(in srgb, currentColor 20%, transparent);

  /* Two more cut the same way, and for the same reason: a plate standing on a plate cannot be named
     from the ladder, because a rung is a fixed colour and the surface under it moves — to a drop
     rung, to a hover rung, to nothing at all when the control goes disabled. At three of those the
     two met and the upper plate vanished into the lower one. A share of the ink above lands the same
     distance above whatever it lands on, so the recipe is stated once here rather than in each file
     that needs one: §3.4's scope, §3.11's browse control and §3.11's progress track were three
     spellings of two values, in a chapter whose whole claim is that no value is written twice. */
  --plate-on-plate: color-mix(in srgb, currentColor 12%, transparent);

  /* The ink a coloured plate resolves, and the one ink in the layer that does not invert with the
     ground. The eleven hues are cut at the UI floor on `--surface-300`, so they land light on the
     dark ground and mid on paper — and an ink that flipped with the ground would be white on a mid
     hue, which measures 3.84 : 1 against a text floor of 4.5. One value holds both: 11.61–12.82 : 1
     on the dark ground and 5.42–5.47 : 1 on paper, across all eleven. */
  --plate-ink: #000000;
  --track-on-plate: color-mix(in srgb, currentColor 24%, transparent);

  /* An alarm and a label are told apart by colour alone, so the two ramps are held 10 ΔE2000 apart —
     the separation floor the mark pack already answers to, applied to the two palettes this layer
     publishes rather than only to the three teals. The signals keep the angles the corpus struck and
     the hues moved off them: `--hue-amber` retires into the warning signal it sat on, and the green
     that sat on the success signal is `--hue-jade` at a hue of its own. */
  --signal-danger: #F27166;
  --signal-warning: #D19000;
  --signal-success: #31BA6F;
  --signal-info: #5F9DFF;

  /* The plate a signal is read on. A signal cut to clear the text floor is a deep colour, and a deep
     colour on bare paper states a fault quietly — the wash is where the alarm gets its presence back
     without spending contrast on it. Each is opaque rather than a tint of the signal, so it carries
     its own floor onto whatever rung it lands on instead of inheriting one. */
  --signal-danger-wash: #43221E;
  --signal-warning-wash: #3B290A;
  --signal-success-wash: #153420;
  --signal-info-wash: #1C2D46;

  --hue-flamingo: #FFAABB;
  --hue-apricot: #FFB185;
  --hue-citron: #D1CA6E;
  --hue-sage: #ADD584;
  --hue-jade: #65DEC6;
  --hue-teal: #59DAED;
  --hue-peacock: #86CFFF;
  --hue-indigo: #BDBFFF;
  --hue-lavender: #DEB0FD;
  --hue-plum: #F7A8DE;
  --hue-graphite: #CDC4BB;

  --accent-artwork: var(--app-accent, var(--ink));
  --accent-ink: var(--app-accent, var(--ink));

  /* A shadow does not invert. The role is derived, so the light ground's rungs are re-cut from the
     dark ground's — but re-cutting a black shadow for a black ground only deepens a black that was
     already invisible, and all five rungs read as one on the dark side for as long as that was the
     whole of the value. Each dark rung carries a lit top edge as well, because on a ground the
     shadow cannot darken the lift has to come from the light. */
  --elevation-raised: 0 1px 2px rgba(0, 0, 0, 0.60), inset 0 1px 0 rgba(255, 255, 255, 0.06);
  --elevation-popover: 0 8px 24px rgba(0, 0, 0, 0.70), inset 0 1px 0 rgba(255, 255, 255, 0.09);
  --elevation-sheet: 0 -8px 32px rgba(0, 0, 0, 0.70), inset 0 1px 0 rgba(255, 255, 255, 0.09);
  --elevation-dialog: 0 24px 64px rgba(0, 0, 0, 0.80), inset 0 1px 0 rgba(255, 255, 255, 0.12);
  --elevation-toast: 0 6px 20px rgba(0, 0, 0, 0.72), inset 0 1px 0 rgba(255, 255, 255, 0.09);

  --palette-order: flamingo, apricot, citron, sage, jade, teal, peacock, indigo, lavender, plum, graphite;

  /* Three faces, and the third is not a second monospace. `--font-mono` is the system's own, spent on
     the one literal machine string a screen carries — the `aos://` route chip, and the masked run in
     a secret field. `--font-code` is the face code is *read* in: an identifier, a path, a diff, a
     snippet a reader means to copy. JetBrains Mono is named first and the system stack carries the
     fall, so the kit renders on a machine that has never installed it and the shipped client is the
     one place the face has to be provisioned. They are two names because they answer to two rules:
     the route chip's face is rationed by [navigation-and-routes.md §8.2] and this one is not. */
  --font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Helvetica Neue", Inter, Roboto, sans-serif;
  --font-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  --font-code: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;

  --type-100: 13px;
  --type-200: 14px;
  --type-300: 15px;
  --type-400: 17px;
  --type-500: 20px;
  --type-600: 24px;
  /* The seventh step, and the one thing above the chrome. Six steps ended at 24px because 24px is
     the largest thing a *control* surface says — a pane head, a dialog title, a numeric readout —
     and every one of those sits in a row with other rows. A document has one line that sits in no
     row: its title. Set at the step under it, that title was one notch off the section head beneath
     it and read as a heading somebody had enlarged rather than as the name of the thing being read
     (§6.21). It is the ramp's own next multiple rather than a size cut for one component, so the
     five heading levels are five consecutive steps of one scale and nothing in the chapter has to
     state a number. */
  --type-700: 30px;

  --leading-tight: 1.2;
  --leading-body: 1.45;

  --weight-regular: 400;
  --weight-medium: 560;
  --weight-strong: 660;

  --numerals-aligned: tabular-nums;

  --space-000: 0px;
  --space-025: 2px;
  --space-050: 4px;
  --space-100: 8px;
  --space-150: 12px;
  --space-200: 16px;
  --space-300: 24px;
  --space-400: 32px;
  --space-600: 48px;

  --control-100: 20px;
  --control-200: 26px;
  --control-300: 36px;
  --control-400: 40px;
  --control-500: 48px;

  --rail-width: 264px;
  --rail-width-min: 216px;
  --rail-width-max: 400px;
  --rail-width-step: 8px;
  --rail-width-collapsed: 48px;
  --toolbar-height: 48px;
  --side-width: 384px;
  --measure: 660px;

  --radius-100: 8px;
  --radius-200: 12px;
  --radius-300: 18px;
  --radius-400: 24px;
  --radius-pill: 999px;

  /* The stacking order the overlay chapter unwinds in, named rather than numbered at each site. A
     rung's shadow and its stacking are the same rung, so the names are the elevation names and the
     scrim sits where §7.10 puts it: between the sheet it dims and the dialog it belongs to. */
  --layer-raised: 1;
  --layer-sheet: 2;
  --layer-scrim: 3;
  --layer-dialog: 4;
  --layer-popover: 5;
  --layer-toast: 6;

  --border-state: 1px solid var(--stroke);
  /* The same sanctioned border, at the weight a *curve* needs to hold it. §1.5 rules one border and
     this does not add a second — it adds the width that border takes when the edge it draws is a
     circle. A straight 1px edge lands on one device row and reads at full strength; the same pixel
     spread around a 20px ring is anti-aliased along almost its whole circumference, so it arrives
     paler than the square beside it and paler than the label it sits next to. Half a pixel is the
     whole correction: measured against 2px, which was tried, the doubled ring reads as a chunky
     annulus and swallows the centre dot §3.8 grows inside it. The fraction is safe here for the
     reason §1.9 refuses it on the focus ring — that ring is a straight-sided rectangle, where a
     fractional width rasterises across two rows as a blur, and this one has no straight side to
     lose. */
  --border-mark-width: 1.5px;
  --focus-ring: var(--focus-ring-width) solid var(--stroke-focus);

  /* One curve for arriving and one for leaving. `--ease` is byte-identical in all eleven
     explorations and is adopted as found; not one of them named the reverse, and a surface that
     leaves on an ease-out hangs on screen after the reader has finished with it. */
  --ease: cubic-bezier(0.2, 0.7, 0.25, 1);
  --ease-exit: cubic-bezier(0.35, 0, 0.85, 0.3);
  /* The third curve, and the one a *cycle* needs. The two above are directional — one for a surface
     arriving, one for a surface leaving — and a loop is neither: the same mark leaves and returns to
     where it started, so it wants a curve that is symmetric about its own middle. Neither directional
     curve is; spending one on a revolution accelerates out of rest and arrives at full speed, or the
     reverse, and either way the turn has a seam in it at the point the reader is looking at.

     It is spelled at element level rather than inside a keyframe on purpose. `animation-timing-function`
     is legal in a keyframe and a `var()` there does not resolve in Chrome — measured, an eased turn
     built that way fell back to the UA's own `ease` and sampled at 0.407 of its rotation a quarter of
     the way in, which is `ease` to three figures and not the curve that was asked for. A curve that
     silently becomes a different curve is worse than one stated in the wrong place. */
  --ease-loop: cubic-bezier(0.65, 0, 0.35, 1);
  --duration-100: 90ms;
  --duration-200: 150ms;
  --duration-300: 240ms;
  --duration-400: 380ms;
  --motion-attention: running;

  /* The period an attention loop turns on, and it is deliberately outside the four rungs above. Those
     four are transition lengths — how long a colour takes to cross, how long a surface takes to
     arrive — and §1.8 rules five captured literals over half a second dead for exactly that use, on
     the grounds that a wait is not a transition. A loop is the other thing: not how long a change
     takes but how often a drawing repeats itself, and nothing about the dead list applies to it. The
     kit had no name for it, so the spinner borrowed `--duration-400` and turned a full revolution in
     380ms — fast enough to read as agitation rather than as waiting, and fast enough that the ink
     quarter smeared into the hairline three. One period serves every shape §2.8 draws, which is also
     what makes the six comparable when a designer is choosing between them.

     It answers to reduced motion through `--motion-attention`, which pauses the loop outright, so it
     is not compressed here the way the transition rungs are — compressing a loop makes it faster,
     which is the opposite of what the reader asked for. */
  --duration-loop: 1200ms;

  /* The rung a *journey* is timed on, separate from the rungs a colour crossing is timed on, because
     the two answer to reduced motion differently. `reducedMotion.drops` names "every transform", and
     compressing a duration does not drop one: at 90ms the toggle knob still travelled and the select
     chevron still turned under `reduce`, which is the rung this name exists to end. Set to nothing,
     a transform arrives without a journey — the knob is already at the far end and the chevron is
     already pointing up, so the state is still reported and only the motion is gone. That is the
     same shape `--motion-attention: paused` takes for a keyframe: the drawing survives, the movement
     does not. A component that moves something names this; one that crosses a colour names a
     duration rung, and those survive under `reducedMotion.survives`. */
  --motion-travel: var(--duration-300);

  --contrast-text-floor: 4.5;
  --contrast-ui-floor: 3;
  /* A hairline rather than two pixels: the ring is drawn outside the control and offset clear of it,
     so its whole job is done by the gap and the edge, and 2px of it read as a second border wrapped
     around every control the keyboard had passed through.

     One pixel and not one and a half. A fractional outline is rasterised across two device rows on a
     1× display, so it arrives as a blurred two-pixel line rather than as a thin one — and a ring is
     the one shape in the kit that has to look the same everywhere it lands. It is also what made a
     rasteriser fault visible under the checks; that one is answered in `tools/cdp.mjs` rather than
     by picking a width around it.

     It is under the 2px perimeter WCAG 2.2 §2.4.11 sizes an indicator against, and it buys that back
     with contrast and clearance rather than hiding it: 6.90 : 1 and 6.82 : 1 on the deepest rung each
     ladder holds, offset clear of the control instead of laid on it. §1.9 states the trade. */
  --focus-ring-width: 1px;
  --focus-ring-offset: 2px;
  --hit-target-min: 44px;
  --disabled-opacity: 0.45;
}

[data-ground="light"] {
  color-scheme: light;

  --surface-000: #FFFFFF;
  --surface-100: #F4F4F6;
  --surface-200: #ECECEF;
  --surface-300: #E3E3E8;

  --ink: #000000;
  --ink-body: rgba(0, 0, 0, 0.80);
  --ink-muted: rgba(0, 0, 0, 0.66);
  --ink-subtle: rgba(0, 0, 0, 0.56);

  --stroke: rgba(0, 0, 0, 0.48);
  --stroke-focus: #4B4B4B;
  --hairline: rgba(0, 0, 0, 0.16);
  --hairline-faint: rgba(0, 0, 0, 0.08);

  /* Paper keeps the shares the three rungs were struck at. They are restated rather than inherited
     for the reason every other composite in this block is: a rung re-cut on one ground and left to
     fall through on the other is a value that moves when nobody moved it. */
  --state-hover: color-mix(in srgb, currentColor 8%, transparent);
  --state-press: color-mix(in srgb, currentColor 18%, transparent);
  --state-selected: color-mix(in srgb, currentColor 12%, transparent);
  --scrim: rgba(0, 0, 0, 0.38);

  --signal-danger: #C2181D;
  --signal-warning: #885C00;
  --signal-success: #00743E;
  --signal-info: #005ED7;

  --signal-danger-wash: #FFE4E0;
  --signal-warning-wash: #FDE8CA;
  --signal-success-wash: #D5F5DE;
  --signal-info-wash: #E0ECFF;

  /* Cut at the UI floor, which is the floor a mark answers to, and authored the same way the dark
     ramp was: the angle its dark rung struck, held to under a degree; the lightest rung that clears
     3 : 1 on `--surface-300`; and the chroma the gamut allows there. Every pair is over the 10 ΔE2000
     separation floor — lavender and plum are the closest at 10.91, and the pair is what sets the
     chroma, because at the dark ramp's own chroma they land 9.84 apart and collide. */
  --hue-flamingo: #C8617A;
  --hue-apricot: #C7682A;
  --hue-citron: #8D8400;
  --hue-sage: #608F1A;
  --hue-jade: #02927E;
  --hue-teal: #008F9F;
  --hue-peacock: #148AC5;
  --hue-indigo: #7B7AC8;
  --hue-lavender: #A469C9;
  --hue-plum: #C060A5;
  --hue-graphite: #898078;

  --accent-artwork: var(--app-accent-artwork-paper, var(--ink));
  --accent-ink: var(--app-accent-paper, var(--ink));

  --elevation-raised: 0 1px 2px rgba(0, 0, 0, 0.08);
  --elevation-popover: 0 8px 24px rgba(0, 0, 0, 0.12);
  --elevation-sheet: 0 -8px 32px rgba(0, 0, 0, 0.14);
  --elevation-dialog: 0 24px 64px rgba(0, 0, 0, 0.18);
  --elevation-toast: 0 6px 20px rgba(0, 0, 0, 0.16);

  /* Re-declared, not inherited: both name a token this block re-cuts, and a composite carries the
     values of the block it was declared in. */
  --border-state: 1px solid var(--stroke);
  --focus-ring: var(--focus-ring-width) solid var(--stroke-focus);
}

[data-corners="squarish"] {
  --radius-100: 3px;
  --radius-200: 5px;
  --radius-300: 8px;
  --radius-400: 11px;
}

/* The two slots an app fills, defaulted once on the root rather than inside the dark block. They sit
   in a rule the ground selectors cannot re-run: a nested `[data-ground="dark"]` matching that block
   would re-declare them guaranteed-invalid and shadow whatever the page handed in, which is how a
   dark surface came to draw the accent as plain ink. */
:root {
  --app-accent: initial;
  --app-accent-artwork-paper: initial;
  --app-accent-paper: initial;
}

/* The same hazard the composites carry, on the other axis. These override the rungs declared on
   `:root, [data-ground="dark"]`, and a declaration that matches an element directly beats one that
   reaches it by inheritance whatever the specificity — so on any element carrying a ground of its
   own, the ground block re-declared the full-length rung and the reader's stated preference reached
   the element as an inherited value that had already lost. It cost nothing where a page sets the
   ground once on the root and everything where a surface sets its own, which is every nested ground
   this layer went to the trouble of supporting — and it was visible on the token sheet, whose cells
   each carry a ground so that the two can be read side by side. Naming the attribute here puts the
   override in the same place the value it overrides was declared. */
@media (prefers-reduced-motion: reduce) {
  :root,
  [data-ground] {
    --duration-200: var(--duration-100);
    --duration-300: var(--duration-100);
    --duration-400: var(--duration-100);
    --motion-attention: paused;
    --motion-travel: 0s;
  }
}
