/* ── tokens.css ─────────────────────────────────────────────────────────────
   The whole design system's vocabulary. Two rules:

   1. This is the ONLY file allowed to contain a literal color or font name.
      Everything else references var(--…). That is what makes the light/dark
      switch and any future retheme a single-file edit.

   2. There are deliberately no tokens for shadow, radius, or transition.
      The rebuild is flat, square, and static by default. When a component
      later earns a radius or a motion, it gets a token here first — so the
      exceptions stay countable instead of spreading.
   ───────────────────────────────────────────────────────────────────────── */

/* ═══ COLOR ════════════════════════════════════════════════════════════════
   Warm-neutral dark. Not pure black, not purple-black, not neon.
   Named by role, never by hue, so the light theme can invert cleanly.
   ═══════════════════════════════════════════════════════════════════════ */
:root {
  --ground:      #090b0b;   /* page */
  --surface:     #101413;   /* a panel sitting on the page */
  --surface-alt: #171c1b;   /* a panel on a panel */

  /* Two contrast passes now. First: fg 16.1 -> 19.0, muted 7.8 -> 9.7,
     faint 3.4 -> 4.3, rule 1.4 -> 1.7. Second (this one): muted and faint
     still read washed out in actual body copy and labels, so both go up
     again — muted 9.7 -> ~13, faint 4.3 -> ~6.3. fg itself is untouched;
     it was already near the 21:1 ceiling. */
  --fg:          #fbfbf9;   /* primary text */
  --fg-muted:    #d2cfc5;   /* secondary text */
  --fg-faint:    #9c968a;   /* labels, metadata */

  /* Neutral gray, no hue — a teal cast on the grid read as an accent color
     leaking everywhere instead of staying spotted on isolated elements.
     Brighter than a first pass at this too: gray still read as gray, and
     the ask was for the content-box borders to read closer to white. */
  --rule:        #4a4a4a;   /* hairline */
  --rule-strong: #747474;   /* emphasized hairline */

  --accent:      var(--accent-candidate, #4e7d80);
  --accent-fg:   #0e0e0c;   /* text sitting on --accent */

  /* Switch hardware. Housing, edge, and plate invert with the theme so the
     control keeps its contrast against either ground. */
  --switch-housing: #f4f4f2;
  --switch-edge:    #f4f4f2;
  --switch-plate:   #c2261f;
  --switch-grip:    #f4f4f2;

  color-scheme: dark;
}

[data-theme="light"] {
  /* No glow on paper. Shallower and tighter than the dark cast, because a
     heavy black shadow on a cream ground reads as grime rather than depth. */
  --p-glow: transparent;
  --shot-lift:
    0 1px 1px rgba(5,8,9,.12),
    0 4px 10px -4px rgba(5,8,9,.16),
    0 20px 40px -20px rgba(5,8,9,.30);
  --shot-lift-lead:
    0 2px 2px rgba(5,8,9,.12),
    0 8px 20px -8px rgba(5,8,9,.18),
    0 40px 80px -34px rgba(5,8,9,.34);

  --ground:      #f7f9f9;
  --surface:     #eef1f2;
  --surface-alt: #ffffff00;  /* deliberately unused: no pure white surfaces */

  --fg:          #050809;
  --fg-muted:    #40494e;
  --fg-faint:    #6d777d;

  --rule:        #a2a2a2;
  --rule-strong: #868686;

  --accent-fg:   #f4f2ec;

  --switch-housing: #0a0a0a;
  --switch-edge:    #0a0a0a;
  --switch-plate:   #c2261f;
  --switch-grip:    #0a0a0a;

  color-scheme: light;
}

/* ── Rule treatments ──────────────────────────────────────────────────────
   Set [data-rule] on <html> to audition one. Default is "slate".
   The grid is most of what you see on this site, so its weight and hue
   change the whole page more than any single element does.
   ──────────────────────────────────────────────────────────────────────── */
[data-rule="hairline"] { --rule: #3a3a3a; --rule-strong: #5c5c5c; }  /* quiet */
[data-rule="slate"]    { --rule: #4a4a4a; --rule-strong: #747474; }  /* default */
[data-rule="blue"]     { --rule: #2a3b4a; --rule-strong: #425c72; }  /* slate blue — kept, the one deliberately colored option */
[data-rule="visible"]  { --rule: #7a7a7a; --rule-strong: #a8a8a8; }  /* strongest — active default, closer to white per feedback */

[data-theme="light"][data-rule="hairline"] { --rule: #c5c5c5; --rule-strong: #9a9a9a; }
[data-theme="light"][data-rule="slate"]    { --rule: #b3b3b3; --rule-strong: #898989; }
[data-theme="light"][data-rule="blue"]     { --rule: #a3b3c2; --rule-strong: #73879a; }
[data-theme="light"][data-rule="visible"]  { --rule: #707070; --rule-strong: #636363; }  /* rule was 3.2:1 against --ground, well under dark's 4.6:1 for the same variant — the .sheet frame border reads as barely-there. Now ~4.7:1, matching. */

/* ── Accent candidates ─────────────────────────────────────────────────────
   Set [data-accent] on <html> to audition one. Default is "clay".
   Deliberately excluded: neon, purple, and pastel SaaS blues.
   ──────────────────────────────────────────────────────────────────────── */
[data-accent="clay"]   { --accent-candidate: #c2703d; }  /* warm, earthen */
[data-accent="slate"]  { --accent-candidate: #4e7d80; }  /* chosen default */
[data-accent="signal"] { --accent-candidate: #c9a227; }  /* dull brass */
[data-accent="none"]   { --accent-candidate: var(--fg); } /* monochrome */

[data-theme="light"][data-accent="clay"]   { --accent-candidate: #9c5326; }
[data-theme="light"][data-accent="slate"]  { --accent-candidate: #34605f; }
[data-theme="light"][data-accent="signal"] { --accent-candidate: #8a6d13; }

/* ── Per-plugin theming ─────────────────────────────────────────────────────
   [data-plugin] on <html> is what makes a product page feel like its own room
   without becoming a different site. It changes the accent, tints the ground,
   and sets the colour of that plugin's glow.

   --p is sampled straight out of each plugin's own interface art. The accents
   are the same hue moved only in lightness, far enough to clear 6.5:1 on the
   dark ground and 5.5:1 on the light one:

     freebounce   #68a0d0  the blue of its panel        (usable as-is on dark)
     liplock      #c868b8  the magenta of its lips
     kaffin8      #6070e0  blueberry blue
     sillyslices  #c03058  the crimson of its sliders
   ═════════════════════════════════════════════════════════════════════ */
[data-plugin="freebounce"]  { --accent-candidate: #68a0d0; --ground: #090c0f; }
[data-plugin="liplock"]     { --accent-candidate: #cd76be; --ground: #0d090c; }
[data-plugin="kaffin8"]     { --accent-candidate: #818ee6; --ground: #090a0f; }
[data-plugin="sillyslices"] { --accent-candidate: #dc7491; --ground: #0d090a; }

[data-theme="light"][data-plugin="freebounce"]  { --accent-candidate: #30699b; --ground: #f5f8fa; }
[data-theme="light"][data-plugin="liplock"]     { --accent-candidate: #a33b92; --ground: #faf6f9; }
[data-theme="light"][data-plugin="kaffin8"]     { --accent-candidate: #4457da; --ground: #f6f7fb; }
[data-theme="light"][data-plugin="sillyslices"] { --accent-candidate: #ba2f55; --ground: #faf6f7; }

/* The raw interface colour and its glow. Declared for both the page attribute
   and the class, because the homepage shows all four at once and so cannot
   carry a single [data-plugin]. --p-glow is --p at the alpha the dark-mode
   shot glow wants; a plain hex would be opaque. */
[data-plugin="freebounce"],  .plugin--freebounce  { --p: #68a0d0; --p-glow: rgba(104,160,208,.30); }
[data-plugin="liplock"],     .plugin--liplock     { --p: #c868b8; --p-glow: rgba(200,104,184,.30); }
[data-plugin="kaffin8"],     .plugin--kaffin8     { --p: #6070e0; --p-glow: rgba(96,112,224,.30); }
[data-plugin="sillyslices"], .plugin--sillyslices { --p: #c03058; --p-glow: rgba(192,48,88,.30); }

/* Grain's --p wash (grain.css, [data-grain="plugin-tinted"]) blends this
   colour via `screen`, which reads very differently depending on how warm
   the plugin's own dark ground already is: screening a pink/magenta accent
   onto liplock/sillyslices's already-reddish near-black ground (#0d090c /
   #0d090a above) compounds into a muddy brown rather than a clean tint,
   while screening blue onto freebounce/kaffin8's cooler ground reads as a
   clean glow. This dials the wash's strength per plugin to compensate —
   not a --p change, since --p drives other things (buttons, highlights)
   that shouldn't shift. 35% is the untouched baseline (halved 2026-09,
   the whole effect read as too strong at the original 70%/90%/45%/40%
   spread — kept the same relative balance between plugins, just scaled
   down). */
[data-plugin="freebounce"]  { --grain-p-alpha: 45%; }
[data-plugin="kaffin8"]     { --grain-p-alpha: 45%; }
[data-plugin="liplock"]     { --grain-p-alpha: 22%; }
[data-plugin="sillyslices"] { --grain-p-alpha: 20%; }

/* ═══ TYPE ═════════════════════════════════════════════════════════════════
   Three roles: display (headlines), text (reading), data (labels/specs).
   Set [data-pair] on <html> to audition a pairing. Default is "press".

   Inter, Geist, and Space Grotesk are excluded on purpose — they read as
   defaults. Every face below was picked for having an actual voice.
   ═══════════════════════════════════════════════════════════════════════ */
:root {
  --font-display: "Instrument Serif", Georgia, serif;
  --font-text:    "IBM Plex Mono", ui-monospace, Menlo, monospace;
  --font-data:    "IBM Plex Mono", ui-monospace, Menlo, monospace;
  --font-brand:   "Karantina", Impact, sans-serif;
}

/* Chosen pairing: Instrument Serif for display over IBM Plex Mono for
   everything else. Karantina remains available as the brand face for
   display use; it is condensed and never takes the text role. */

/* ── Type scale — mobile first ─────────────────────────────────────────────
   Values are the MOBILE sizes. The clamp upper bound only engages on wider
   viewports, so the phone layout is the one being designed, not derived.
   1rem = 10px via the 62.5% base in base.css.
   ──────────────────────────────────────────────────────────────────────── */
:root {
  --fs-display: clamp(6.6rem, 18vw, 10.8rem);
  --fs-h1:      clamp(2.6rem, 6vw,   4.8rem);
  --fs-h2:      clamp(2.0rem, 4vw,   3.2rem);
  --fs-h3:      clamp(1.7rem, 2.4vw, 2.2rem);
  --fs-lead:    clamp(2.0rem, 4.6vw, 2.4rem);
  --fs-text:    1.6rem;
  --fs-small:   1.4rem;
  --fs-data:    1.2rem;

  --lh-tight: 1.05;
  --lh-snug:  1.25;
  --lh-text:  1.55;

  --track-tight:  -0.005em;
  --track-data:    0.14em;
  --track-display: -0.005em;
}

/* ═══ SPACE ════════════════════════════════════════════════════════════════
   One ratio, no improvised values. Section padding is its own token so the
   page's vertical rhythm can be retuned in one place.
   ═══════════════════════════════════════════════════════════════════════ */
:root {
  --s1: 0.4rem;  --s2: 0.8rem;  --s3: 1.2rem;  --s4: 1.6rem;
  --s5: 2.4rem;  --s6: 3.2rem;  --s7: 4.8rem;  --s8: 6.4rem;  --s9: 9.6rem;

  /* Screenshots only. A plugin UI is a physical object photographed against
     the page: a hairline radius and a low, wide shadow give it depth without
     the card idiom. Nothing else on the site may use these. */
  --shot-radius: 5px;
  /* Dark: the panel is lit, so it glows in its own colour. Light: nothing
     glows on paper, so --p-glow goes transparent and only the cast remains.

     The glow is NOT baked into --shot-lift. A var() inside a custom property
     resolves where that property is declared — here, :root — so it would
     always pick up the fallback below and never the plugin's colour. The
     glow layer is composed on the element instead, in plugins.css and
     product.css, where --p-glow resolves against the cell. It is offset
     downward rather than centred, so the colour pools beneath the panel like
     light bouncing off the page instead of haloing it. */
  --p-glow: rgba(255,255,255,.09);
  --shot-lift:
    0 1px 1px rgba(0,0,0,.55),
    0 20px 44px -22px rgba(0,0,0,.8);
  --shot-lift-lead:
    0 2px 2px rgba(0,0,0,.55),
    0 44px 90px -38px rgba(0,0,0,.85);

  /* The three motions in the system. Declared here so the exceptions stay
     countable: a throw on a physical switch, the crossfade between light and
     dark, and the buy button stating which platform it detected. All three are
     a control reporting its state, not decoration. */
  --dur-throw: 190ms;
  --dur-theme: 200ms;
  --dur-detect: 420ms;   /* the buy button naming the detected platform */
  --ease-throw: cubic-bezier(0.2, 0.8, 0.3, 1);

  /* v3 workstream 4 — the one motion signature beyond the buy button:
     grid-cell entrances (plugin cells, FAQ rows). One curve, reused
     everywhere it's added, rather than each component inventing its own —
     that repetition is what makes it read as a signature instead of
     scattered polish. Exponential ease-out, same family as --ease-throw. */
  --dur-reveal: 420ms;
  --ease-reveal: cubic-bezier(0.16, 1, 0.3, 1);

  /* Motion system v2 (motion-system-handoff.md) — stroke-draw: a line or box
     border drawing itself in, once, on scroll entrance. Reuses --ease-reveal
     rather than inventing a new curve. --dur-draw-sm is for small elements
     (a single rule, an index number's underline); --dur-draw is the full-box
     case, reserved for when stroke-draw gets applied to actual boxes rather
     than the single-line case this pass ships (FAQ item rules). --dur-whisper
     is the trailing content-settle fade that follows a completed draw —
     small and fast, never the first-firing move on anything. */
  --dur-draw-sm: 260ms;
  --dur-draw: 460ms;
  --dur-whisper: 200ms;

  /* Boot sequence only (signal-lock.css) — the mark's dot orbiting the ring
     once it's ejected onto it. A genuine fourth move beyond the three above,
     approved as a one-off scoped to that sequence, not a reusable class: see
     ANIMATION-VOCAB.md. Symmetric slow-fast-slow, unlike --ease-reveal
     (decel-only) or --ease-throw (spring). */
  --dur-orbit: 800ms;
  --ease-orbit: cubic-bezier(0.65, 0, 0.35, 1);

  /* Grain (grain.css): the one exception to "pure flat colour." A procedural
     SVG turbulence layer, not an image asset, so it scales losslessly and
     costs nothing to ship. [data-grain] overrides this per variant — the
     token exists so the opacity is declared and countable in one place
     rather than a magic number buried in the overlay rule itself. This
     bare fallback (0) only matters for a page with no [data-grain]
     attribute at all — every real page now sets one explicitly (whisper,
     visible, or plugin-tinted; see grain.css), so grain is on site-wide by
     default, not opt-in per page anymore. */
  --grain-opacity: 0;

  --gutter:  var(--s4);          /* page edge padding, mobile */
  --section: var(--s7);          /* vertical section padding, mobile */
  --measure: 62ch;               /* max reading width */
  --frame:   132rem;             /* max content width */
  --sheet-top: var(--s5);        /* air above the frame */
}

/* Same lift, cut down for mobile: the wide/deep desktop spread reads as
   grime rather than depth once the panel it's lifting is small — less
   blur, less spread, lower opacity, same two-layer shape. */
@media (max-width: 47.99em) {
  :root {
    --shot-lift:
      0 1px 1px rgba(0,0,0,.4),
      0 10px 20px -14px rgba(0,0,0,.55);
    --shot-lift-lead:
      0 1px 1px rgba(0,0,0,.4),
      0 16px 30px -18px rgba(0,0,0,.6);
  }
}

@media (min-width: 48em) {
  :root {
    --gutter:  var(--s6);
    --section: var(--s9);
    --sheet-top: var(--s6);
  }
}

/* Page width, as a live test: each step widens the sheet by that percentage.
   --frame's base is 132rem. */
[data-width="10"] { --frame: 145.2rem; }
[data-width="15"] { --frame: 151.8rem; }
[data-width="20"] { --frame: 158.4rem; }
