/**
 * The design system, as data.
 *
 * Every visual constant this interface has ever agreed on already existed — it
 * was written once in the `:root` block of `index.html`, and then written again,
 * slightly differently, in each of the four modules that inject their own
 * stylesheet at runtime (`tools/godmode.ts`, `profile/webcamPanel.ts`,
 * `profile/editor.ts`, `media/officeScreenPanel.ts`). Those four cannot read
 * `index.html`'s custom properties at author time, so each of them carries a
 * fallback — `var(--s4, 16px)` — and the fallback is the copy that rots. A token
 * whose value moves in one file and not in five is a token that no longer means
 * anything.
 *
 * This module is the answer to that: the values live here, in TypeScript, where
 * a test can read them; `index.html`'s `:root` block is *generated from* this
 * file's text and asserted against it by `src/test/ui/tokens.test.ts`. The
 * injected stylesheets keep their `var(--x, fallback)` form — they must, because
 * a `<style>` element mounted into a page that never loaded `index.html` (the
 * office-screen panel is reachable from an embed) still has to look right — but
 * the fallbacks are now checked against the same source rather than remembered.
 *
 * ### Why the z-index scale is the part that earns its own file
 *
 * Before this there were seven raw z-index literals across five files and no
 * document anywhere saying what order they were meant to be in. Two of them were
 * collisions nobody had decided: the webcam "camera active" indicator at 12 was
 * painting over the shortcuts modal at 10, and godmode's HUD at 6 was sitting on
 * top of the play HUD at 4. Neither was wrong exactly — both are defensible — but
 * neither had been *chosen*, and the difference between a stacking order that is
 * chosen and one that is emergent shows up the first time somebody adds an
 * eighth layer.
 *
 * So the layers are named, ordered and justified here, once. A module that needs
 * to stack something looks up a name; nothing anywhere writes a bare number.
 *
 * This file imports nothing. It is read by the mount applier, by the onboarding
 * and device-panel stylesheets, and by tests; it must never grow a DOM
 * dependency, because half its value is being assertable in Node.
 */

// ---- Stacking -------------------------------------------------------------

/**
 * The complete stacking order of this product, lowest first.
 *
 * The numbers are deliberately sparse and deliberately small. Sparse, so a layer
 * can be slipped between two of these without renumbering the world; small,
 * because a `z-index: 9999` is a confession that the author did not know what
 * else was on screen, and everything on this screen is in this list.
 */
export const Z_LAYERS = {
  /**
   * Ambient chrome: the panel, the top-right column, the rail, the honesty line.
   * Everything that is *about* the scene and is never modal.
   */
  chrome: 3,
  /**
   * The mode dock and the panel toggle — controls that must stay reachable while
   * a card is open, because they are how you get out of the state the card is
   * describing.
   */
  dock: 4,
  /**
   * The play surfaces: the play HUD, the touch joystick and its action pad, the
   * scrim behind the phone's panel sheet. Above the dock because a thumb resting
   * on the joystick must not press a dock button through it.
   */
  play: 5,
  /**
   * Instruments: godmode's HUD and pose dock, and the onboarding coach.
   *
   * Above `play` on purpose, and this is one of the two orderings that used to be
   * an accident. A debug readout that a play HUD can hide is a debug readout you
   * will misread, and a first-run coach pointing at the joystick has to be drawn
   * over the thing it is pointing at.
   */
  instrument: 6,
  /** Modal overlays: the shortcuts sheet, the profile editor, the screen panel. */
  modal: 10,
  /**
   * Live-capture alerts, and nothing else — today only "camera active".
   *
   * Above `modal`, which is the second ordering that used to be an accident and
   * is now a decision: an indicator saying a camera is on is a safety affordance,
   * and a dialog that can cover it turns "your webcam is live" into a fact the
   * interface knows and the person does not. It is scoped to the top-right column
   * and only while the indicator is actually shown — see the `body:has()` rule in
   * `index.html` — so nothing else rides up there with it.
   */
  alert: 12,
  /**
   * The boot card. Above everything, because until a real frame is on the glass
   * there is nothing underneath worth seeing, and because a modal that could open
   * behind it would trap focus in an invisible dialog.
   */
  boot: 20,
} as const;

export type ZLayer = keyof typeof Z_LAYERS;

export const Z_LAYER_NAMES: readonly ZLayer[] = [
  "chrome",
  "dock",
  "play",
  "instrument",
  "modal",
  "alert",
  "boot",
];

/** The custom-property name for a layer: `"dock"` → `"--z-dock"`. */
export function zVarName(layer: ZLayer): string {
  return `--z-${layer}`;
}

/**
 * What a stylesheet writes: `zIndex("dock")` → `"var(--z-dock, 4)"`.
 *
 * The fallback is included because the three panels that inject their own
 * `<style>` can be mounted into a document that never loaded `index.html`'s
 * `:root` block, and a `z-index: var(--z-modal)` that resolves to nothing is not
 * "auto", it is invalid-at-computed-value-time — which is worse, because the
 * element quietly loses its stacking context instead of visibly failing.
 */
export function zIndex(layer: ZLayer): string {
  return `var(${zVarName(layer)}, ${Z_LAYERS[layer]})`;
}

// ---- The rest of the system ----------------------------------------------

/**
 * One accent, and the two shades of it that are still the same colour.
 *
 * `#f2b134` is spent only on things that are *active*: the current chapter, the
 * current board, the office door, a focus ring. The moment it also means
 * "heading" and "border" and "hover" it stops meaning anything — which is what a
 * second pass at this looked like before it was pulled back.
 */
export const ACCENT = {
  base: "#f2b134",
  lit: "#ffc555",
  ink: "#ffd68a",
} as const;

/**
 * Four steps of ink and no more.
 *
 * Anything that wanted a fifth was saying something the type scale should have
 * said instead. They are white at four alphas rather than four greys, because
 * this chrome floats over a photographic background that runs from a noon sky to
 * a night one, and a fixed grey is only ever right against one of them.
 */
export const INK = {
  1: "rgba(255, 255, 255, 0.78)",
  2: "rgba(255, 255, 255, 0.56)",
  3: "rgba(255, 255, 255, 0.4)",
  4: "rgba(255, 255, 255, 0.26)",
} as const;

/**
 * The 4px rhythm. Every gap, pad and offset in the product is a multiple of it,
 * which is what lets two columns written a month apart line up without anyone
 * nudging a value.
 */
export const SPACE = {
  1: "4px",
  2: "8px",
  3: "12px",
  4: "16px",
  5: "24px",
  6: "32px",
} as const;

/**
 * Five type sizes. 9px is reserved for uppercase micro-labels, where the caps and
 * the tracking carry the legibility that the size does not.
 */
export const TYPE = {
  title: { size: "11px", tracking: "0.2em", transform: "uppercase" },
  body: { size: "11px", tracking: "0.01em", transform: "none" },
  ctrl: { size: "12px", tracking: "0.01em", transform: "none" },
  micro: { size: "10px", tracking: "0.06em", transform: "none" },
  caps: { size: "9px", tracking: "0.1em", transform: "uppercase" },
} as const;

export type TypeStep = keyof typeof TYPE;

/**
 * One glass recipe, two weights.
 *
 * Every card carries three separations at once, and all three are needed: a fill
 * dark enough to hold white text against a white sky, a hairline that is light on
 * the inside so the card has a rim, and one soft drop shadow so it sits *above*
 * the scene rather than in it. A panel tuned only for the dark case — which is
 * what this was — reads as a grey smudge at noon, because dark-on-dark needs no
 * edge and dark-on-bright very much does.
 *
 * The strong weight is for anything that must be read over the brightest part of
 * the sky with no scene context to help: the boot card, the shortcuts sheet, the
 * phone's bottom sheets, the onboarding coach.
 */
export const GLASS = {
  fill: "rgba(9, 13, 18, 0.62)",
  fillStrong: "rgba(9, 13, 18, 0.86)",
  inset: "rgba(255, 255, 255, 0.05)",
  hairline: "rgba(255, 255, 255, 0.11)",
  blur: "blur(14px) saturate(1.2)",
  shadow: "0 6px 22px rgba(3, 6, 10, 0.45)",
} as const;

export const RADIUS = { base: "8px", small: "5px", pill: "999px" } as const;

/**
 * The one transition. 150ms is under the ~200ms at which motion starts reading as
 * lag and over the ~80ms at which it stops reading as motion at all.
 */
export const MOTION = { transition: "150ms cubic-bezier(0.4, 0, 0.2, 1)" } as const;

/**
 * The smallest thing a finger is allowed to be asked to hit.
 *
 * 44px, which is 11 steps of the 4px rhythm and the smallest target anyone has
 * managed to defend. The device panel's sliders and switches are checked against
 * it by test, because a mixing desk rendered at desktop density is exactly the
 * kind of control that quietly drops below it.
 */
export const TOUCH_TARGET_PX = 44;

/**
 * The two breakpoints, and no more.
 *
 * At 900 the left column stops being furniture and becomes a sheet you open; at
 * 600 the page stops being a scaled-down desktop and becomes a different layout.
 * `deviceProfile()` in `engine/stage.ts` keys its pixel budget off the same 600,
 * so the stylesheet and the renderer cannot drift apart about what a phone is.
 */
export const BREAKPOINT = { phone: 600, compact: 900 } as const;

// ---- The `:root` block ----------------------------------------------------

/**
 * Every token as a `--name: value` pair, in the order they appear in `:root`.
 *
 * This is what makes the system checkable rather than merely documented: the
 * stylesheet test walks this list and asserts each declaration appears verbatim
 * in `index.html`. A token added here and forgotten there fails a test rather
 * than producing an element styled with an invalid value.
 */
export const CSS_TOKENS: readonly (readonly [string, string])[] = [
  ["--amber", ACCENT.base],
  ["--amber-lit", ACCENT.lit],
  ["--amber-ink", ACCENT.ink],

  ["--glass", GLASS.fill],
  ["--glass-strong", GLASS.fillStrong],
  ["--glass-inset", GLASS.inset],
  ["--hairline", GLASS.hairline],
  ["--blur", GLASS.blur],
  ["--shadow", GLASS.shadow],

  ["--ink", INK[1]],
  ["--ink-2", INK[2]],
  ["--ink-3", INK[3]],
  ["--ink-4", INK[4]],

  ["--s1", SPACE[1]],
  ["--s2", SPACE[2]],
  ["--s3", SPACE[3]],
  ["--s4", SPACE[4]],
  ["--s5", SPACE[5]],
  ["--s6", SPACE[6]],

  ["--r", RADIUS.base],
  ["--r-sm", RADIUS.small],
  ["--r-pill", RADIUS.pill],
  ["--t", MOTION.transition],
  ["--tap", `${TOUCH_TARGET_PX}px`],

  ...Z_LAYER_NAMES.map((layer) => [zVarName(layer), String(Z_LAYERS[layer])] as const),
];

/** The declarations as CSS text, one per line, for a generated `:root` block. */
export function tokenCss(indent = "        "): string {
  return CSS_TOKENS.map(([name, value]) => `${indent}${name}: ${value};`).join("\n");
}
