@tailwind base;
@tailwind components;
@tailwind utilities;

/*
 * Faces. Geist (sans) and Geist Mono, both SIL OFL 1.1, self-hosted from the
 * @fontsource-variable packages. Production CSP is `font-src 'self'`, so a
 * hosted font would silently fall through to the system stack; Vite resolves
 * these bare-package URLs and emits the woff2 under /assets, which is 'self'.
 * Latin subset only — every string on the site is English and the ids are
 * ASCII; the other subsets would be 400 kB of bytes nobody renders.
 *
 * ABC Favorit (Prime Intellect's display mono) is a licensed trial font and is
 * NOT used. Geist Mono is the honest substitute.
 */
@font-face {
  font-family: 'Geist Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src:
    url('@fontsource-variable/geist/files/geist-latin-wght-normal.woff2') format('woff2-variations'),
    url('@fontsource-variable/geist/files/geist-latin-wght-normal.woff2') format('woff2');
}
@font-face {
  font-family: 'Geist Mono Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src:
    url('@fontsource-variable/geist-mono/files/geist-mono-latin-wght-normal.woff2') format('woff2-variations'),
    url('@fontsource-variable/geist-mono/files/geist-mono-latin-wght-normal.woff2') format('woff2');
}

/*
 * PIG's token layer, inlined as literals rather than written onto the root at
 * runtime. This site has no user-chosen accent, so a runtime theme module
 * would be a moving part that buys nothing here.
 *
 * Every colour is an HSL channel triple — `H S% L%` — so Tailwind can compose
 * it with an opacity modifier (`bg-fg/40`). Writing `hsl(...)` into the
 * variable breaks that silently: the class compiles and the opacity is ignored.
 *
 * The palette adopts Prime Intellect's materials (pure-neutral greys, one green,
 * amber and red for status) — measured from primeintellect.ai, not guessed. It
 * does not impersonate: no PI logo, no PI wordmark, and the greys that fail
 * WCAG on their own ground are lifted until they pass (see --muted).
 *
 * Every ratio in this file is WCAG 2.x relative-luminance contrast computed by
 * scratchpad/contrast.mjs from these exact triples. Change a triple, re-run
 * the script, and change the comment: a stale number here is worse than none.
 *
 * Rules that have no lint and rely on review:
 *   - `--danger` as text goes only on bg, surface, surface-2 and danger-subtle.
 *     Never on surface-3 or accent-subtle (4.28 / 4.13 in dark — both fail).
 *   - `--accent` is a FILL token. As text it is only valid in dark, where it
 *     equals `--accent-fg`. Use `--accent-fg` for any green text or icon.
 *   - Every green-filled control carries a 1px `--accent-edge` border. In light
 *     the fill is 1.47:1 against white; the edge is what makes it a control.
 *   - No alpha tints on grounds (`bg-warning/5`, `border-border/60`) — they
 *     vanish on #0F0F0F. Use the `*-subtle` tokens. Alpha survives only on the
 *     overlay scrim (`bg-fg/40`) and the light-mode CTA hover.
 *   - There is no `--muted-2`. Icons at rest and hairlines that must read use
 *     `--border-strong`, which clears 3:1 (non-text) in both themes.
 *
 * Bare :root is the LIGHT theme; next-themes stamps data-theme="dark" (including
 * for a system-dark visitor), so no prefers-color-scheme block is needed.
 */
:root {
  /* ---------------------------------------------------------- light grounds */
  --bg: 0 0% 98%;            /* #FAFAFA */
  --surface: 0 0% 100%;      /* #FFFFFF cards, header, panels */
  --surface-2: 0 0% 96%;     /* #F5F5F5 insets, idle keys, chips, row hover */
  --surface-3: 0 0% 93%;     /* #EDEDED code grounds, table heads, active tab, surface-2 hover */
  --border: 0 0% 89%;        /* #E3E3E3 decorative hairline: 1.28 vs surface, never the sole signal */
  --border-strong: 0 0% 55%; /* #8C8C8C any boundary that IS the signal: 3.36 vs surface, 3.08 vs surface-2, 3.22 vs bg */

  /* -------------------------------------------------------------- light ink */
  --fg: 0 0% 6%;             /* #0F0F0F headings, claims, values: 19.17 on surface */
  --fg-2: 0 0% 22%;          /* #383838 prose ink: 11.73 on surface, 9.92 on danger-subtle */
  --muted: 0 0% 40%;         /* #666666 captions, labels: 5.74 on surface, 4.90 on surface-3, 4.86 on danger-subtle */

  /* ------------------------------------------------------------ light accent */
  --accent: 112 77% 69%;     /* #83ED73 FILL ONLY — 1.47 on white as text, forbidden as text in light */
  --accent-fg: 112 60% 27%;  /* #276E1C accent as text/icon/link: 6.30 on surface, 6.04 on bg, 5.33 on danger-subtle */
  --accent-on: 0 0% 6%;      /* #0F0F0F text on an accent fill: 13.08 */
  --accent-edge: 112 60% 27%;/* #276E1C the 1px edge of every green-filled control: 6.30 vs white */
  --accent-subtle: 112 60% 93%; /* #E5F8E2 selected rows, grader box, verified badge */
  --ring: 112 60% 27%;       /* #276E1C focus ring — the neon ring would be 1.47 on white */
  --positive: 112 60% 27%;   /* = accent-fg. One green: live, pass, solved, exact */

  /* ------------------------------------------------------------ light status */
  --warning: 32 95% 30%;     /* #955104 text only: 6.08 on surface. Reads brown as a fill, so tiles never use it */
  --warning-on: 0 0% 100%;   /* text on a warning fill: 6.08 */
  --warning-subtle: 39 90% 94%; /* #FDF4E2 synthetic banner, EDITED chip, counterweight chip, limits card */
  --danger: 357 80% 40%;     /* #B8141D 6.65 on surface, 5.63 on danger-subtle */
  --danger-on: 0 0% 100%;    /* text on a danger fill: 6.65 */
  --danger-subtle: 357 85% 95%; /* #FDE7E8 verify mismatch ground, out-of-tolerance rows */
  --info: 210 22% 36%;       /* #485C70 kept for StatTone 'info' only: 6.91 on surface */
  --shadow: 0 0% 6%;

  /* The tab-bar underline. `--accent` is a fill token and the underline is a
     2px state indicator on the page ground, so in light it takes the text
     green (6.04 vs bg); in dark the two are the same colour. */
  --tab-underline: 112 60% 27%;

  /*
   * Board tiles. Foregrounds are stated explicitly and measured, because the
   * board is the one place on the site where colour carries meaning: an unlit
   * tile and a grey tile differ only by fill, and a letter that fails contrast
   * against its own tile makes the result unreadable exactly when it matters.
   *
   * The exact and present fills are the SAME in both themes: the board is the
   * one theme-invariant object on the site. A green-and-amber board on a white
   * page is deliberate and will be called "loud" on the first screenshot; the
   * answer is the glyph contrast below and the high-contrast mode. Do not
   * soften the fills.
   *
   * The present tile does NOT reuse --warning. That token is darkened until
   * white text clears 4.5:1 on it, and at that lightness it reads BROWN. On a
   * board where people arrive with a hard prior that this square is yellow, a
   * brown square reads as a bug in the scorer — which on a page whose whole
   * argument is "the grader is correct" is the worst thing it could look like.
   * So the fill stays amber and the GLYPH goes dark.
   *
   * Measured against each tile's own fill (light):
   *   exact   near-black on 112 77% 69% -> 13.08
   *   present near-black on  39 87% 65% -> 11.16
   *   absent  white      on   0  0% 40% ->  5.74   (tile vs surface 5.74)
   *   exact vs present 1.17 — hue-only, same as the original game; HC mode is the path
   */
  --tile-exact: 112 77% 69%;
  --tile-exact-fg: 0 0% 6%;
  --tile-present: 39 87% 65%;
  --tile-present-fg: 0 0% 6%;
  --tile-absent: 0 0% 40%;
  --tile-absent-fg: 0 0% 100%;
  --tile-empty-border: 0 0% 78%; /* #C7C7C7 unplayed tile edge: 1.69 vs surface */

  /* ------------------------------------------------------------- non-colour */
  --radius-control: 0.375rem; /* 6px: buttons, inputs, tabs, selects, chips */
  --radius-surface: 0.75rem;  /* 12px: cards, panels, sheets */
  --radius-tile: 0.25rem;     /* 4px: board tiles, table cells, cite chips */

  --measure: 62ch;            /* explaining prose */
  --measure-wide: 72ch;       /* ledes */

  --dur-1: 120ms;             /* hover, colour */
  --dur-2: 180ms;             /* ranking reorder, tab underline */
  --dur-3: 240ms;             /* sheet, drawer */
  --ease-out: cubic-bezier(0.2, 0, 0, 1);
  --ease-in: cubic-bezier(0.4, 0, 1, 1);

  --safe-top: env(safe-area-inset-top, 0px);
  --safe-bottom: env(safe-area-inset-bottom, 0px);
  --safe-left: env(safe-area-inset-left, 0px);
  --safe-right: env(safe-area-inset-right, 0px);

  /* One bar, 56px + safe-top at EVERY width. The old lg step to 4rem is gone:
     the demo tab bar sticks under this and the language's bar is 56px. */
  --app-header-h: calc(3.5rem + var(--safe-top));

  /* Overlays only — sheet, drawer, tooltip, navigation panel. Flat components
     (buttons, tabs, cards, chips) carry no shadow; their borders do the work. */
  --shadow-overlay: 0 8px 24px hsl(var(--shadow) / 0.12);
}

/*
 * Dark — the home theme. Grounds and greys are Prime Intellect's measured
 * values (#0F0F0F, #141414, #1C1C1C, #242424, #F5F5F5). Two of their greys
 * FAIL on their own ground: #737373 is 4.04:1 and #6E6E6E is 3.76:1 on
 * #0F0F0F. `--muted` is deliberately lifted to 58% (#949494, 6.32 on bg,
 * 4.94 on accent-subtle) so captions pass everywhere they land. Do not
 * "correct" it back to PI's number.
 */
:root[data-theme='dark'] {
  --bg: 0 0% 6%;             /* #0F0F0F */
  --surface: 0 0% 8%;        /* #141414 */
  --surface-2: 0 0% 11%;     /* #1C1C1C */
  --surface-3: 0 0% 14%;     /* #242424 */
  --border: 0 0% 13%;        /* #212121 1.14 vs surface, by design — never the sole signal */
  --border-strong: 0 0% 42%; /* #6B6B6B 3.46 vs surface, 3.20 vs surface-2, 3.60 vs bg */

  --fg: 0 0% 96%;            /* #F5F5F5 17.58 on bg, 13.74 on accent-subtle */
  --fg-2: 0 0% 80%;          /* #CCCCCC 11.94 on bg, 9.33 on accent-subtle */
  --muted: 0 0% 58%;         /* #949494 6.32 on bg, 5.12 on surface-3, 4.94 on accent-subtle (lifted, see above) */

  --accent: 112 77% 69%;     /* #83ED73 fills: CTA, progress hairline, tile-exact, bars */
  --accent-fg: 112 77% 69%;  /* = accent in dark: 13.08 on bg, 10.22 on accent-subtle */
  --accent-on: 0 0% 6%;      /* #0F0F0F on accent: 13.08 */
  --accent-edge: 112 77% 69%;/* = accent: the 1px edge is a no-op in dark and exists so the class is theme-blind */
  --accent-subtle: 112 30% 13%; /* #1A2B17 */
  --ring: 112 77% 69%;
  --positive: 112 77% 69%;   /* = accent */

  --warning: 39 87% 65%;     /* #F3BD58 PI amber: 11.16 on bg, 8.72 on accent-subtle */
  --warning-on: 0 0% 6%;     /* on a warning fill: 11.16 */
  --warning-subtle: 39 40% 12%; /* #2B2212 */
  --danger: 357 100% 60%;    /* #FF333D PI red: 5.29 on bg, 5.08 on surface, 4.70 on surface-2, 4.72 on danger-subtle.
                                FAILS on surface-3 (4.28) and accent-subtle (4.13) — never put it there */
  --danger-on: 0 0% 6%;      /* on a danger fill: 5.29. `text-white` on this red is 3.63 and fails */
  --danger-subtle: 357 40% 13%; /* #2E1415 */
  --info: 210 16% 72%;       /* #ACB8C3 near-neutral steel, StatTone 'info' only: 9.49 on bg */
  --shadow: 0 0% 0%;

  --tab-underline: 112 77% 69%; /* = accent */

  /*
   * Measured against each tile's own fill (dark):
   *   exact   near-black on 112 77% 69% -> 13.08
   *   present near-black on  39 87% 65% -> 11.16
   *   absent  0 0% 96%   on   0  0% 30% ->  7.75   (tile vs surface 2.18 — raised from 24% so an absent tile reads FILLED, not empty)
   *   empty border 0 0% 24% vs surface 1.70 — a visible step above --border (1.14), quieter than border-strong across 30 tiles
   */
  --tile-exact: 112 77% 69%;
  --tile-exact-fg: 0 0% 6%;
  --tile-present: 39 87% 65%;
  --tile-present-fg: 0 0% 6%;
  --tile-absent: 0 0% 30%;
  --tile-absent-fg: 0 0% 96%;
  --tile-empty-border: 0 0% 24%;

  --shadow-overlay: 0 8px 24px hsl(var(--shadow) / 0.35);
}

/*
 * High contrast — tiles only. Green/amber/grey is a colour-only distinction,
 * which is exactly why the original game ships a high-contrast mode. Toggled
 * from the header and the Play tab, persisted per-viewer; the tiles also carry
 * a glyph (● exact, ◆ present, none for absent) at full opacity in this mode,
 * so the result survives being printed, screenshotted or seen by someone with
 * deuteranopia.
 *
 * White glyph on each fill:
 *   exact   209 100% 40% (#0069CC) -> 5.39   (3.41 vs the dark surface, 5.39 vs the light one)
 *   present  27 100% 36% (#B85300) -> 4.91   THE PREVIOUSLY SHIPPED 27 100% 42% MEASURES 3.80 AND FAILS. One number, committed here.
 *   absent    0   0% 38% (#616161) -> 6.19   light theme; 2.97 vs the dark surface, hence the override below
 *   exact vs present 1.10 — the glyphs are the distinction, not the hues
 */
:root[data-contrast='high'] {
  --tile-exact: 209 100% 40%;
  --tile-present: 27 100% 36%;
  --tile-absent: 0 0% 38%;
  --tile-exact-fg: 0 0% 100%;
  --tile-present-fg: 0 0% 100%;
  --tile-absent-fg: 0 0% 100%;
}
:root[data-theme='dark'][data-contrast='high'] {
  --tile-absent: 0 0% 45%;   /* #737373: white glyph 4.74; 3.89 vs the dark surface so it still reads filled */
}

@layer base {
  * { @apply border-border; }
  body {
    @apply bg-bg text-fg font-sans antialiased;
    /* Painted explicitly: the viewer's chrome shows through a transparent body
       and the page would borrow the host's theme instead of its own. */
    text-rendering: optimizeLegibility;
    /* Tabular figures site-wide: every number on this site is compared to
       another number, and a proportional "1" in a reward column makes a
       ranking look re-ordered when it is not. */
    font-variant-numeric: tabular-nums;
  }
  /* Kept as an explicit no-op for compat: body already sets tabular-nums. */
  .nums { font-variant-numeric: tabular-nums; }
  :focus-visible { @apply outline-none ring-2 ring-ring ring-offset-2 ring-offset-bg; }
}

@layer components {
  .card { @apply rounded-xl border border-border bg-surface; }
  /* 44px minimum touch target — the board keys and step chips are the two
     places this is load-bearing on a phone. */
  .tap { @apply min-h-11 min-w-11; }
}

/*
 * Touch targets follow the POINTER, not the width. Components step their
 * controls down from 44px to 36-40px at `lg` (1024px) on the assumption that
 * a 1024px viewport has a mouse. An iPad in landscape is exactly 1024px and
 * has a finger; measured there, the header icons were 36px, the nav triggers
 * 40px and the nested tab strips 36px. On a coarse pointer every control keeps
 * the 44px floor at every width, and icon-only controls (an aria-label and a
 * lone svg) keep it on both axes. `:root :is(...)` outranks the single-class
 * `lg:min-h-9` utilities without `!important`. `a[href]` rather than a class:
 * the header CTA is `hidden lg:inline-flex`, so no display class is reliable,
 * and inline links in prose are unaffected because min-height does not apply
 * to inline boxes.
 */
@media (pointer: coarse) {
  :root :is(button, a[href], [role='button'], [role='tab'], [role='radio'], [role='switch'], [role='slider'], summary, select) {
    min-height: 2.75rem;
  }
  /* Icon-only controls, and the segmented options ("#1", "1×") whose text is
     two glyphs wide, keep the floor on both axes. Segments wrap, so this
     cannot widen a row past the viewport. */
  :root :is(button, a)[aria-label]:has(> svg:only-child),
  :root [role='radio'] {
    min-width: 2.75rem;
  }
}

/*
 * Reduced motion clamps everything to effectively zero. That is correct, and
 * it is also why every state change in this app must ALSO be announced: with
 * the flip gone, colour alone carries the result, and colour alone is not a
 * result for a screen-reader user.
 */
@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;
  }
}

/*
 * Board motion. Declared here rather than in the demo because Tailwind's
 * arbitrary `animate-[...]` needs the keyframes to exist in the stylesheet, and
 * a demo-local <style> block would be a second place for the token values to
 * drift from. Pop 120ms, flip 520ms staggered 100ms/tile, shake 400ms.
 */
@keyframes tile-flip {
  0% { transform: rotateX(0deg); }
  50% { transform: rotateX(90deg); }
  100% { transform: rotateX(0deg); }
}
@keyframes tile-pop {
  0% { transform: scale(1); }
  60% { transform: scale(1.06); }
  100% { transform: scale(1); }
}
@keyframes tile-shake {
  0%, 100% { transform: translateX(0); }
  15%, 45%, 75% { transform: translateX(-5px); }
  30%, 60%, 90% { transform: translateX(5px); }
}
