/* ============================================================
   tokens.css
   Mohammed Shaibaaz — portfolio
   The only file in the project containing raw values.
   Nothing downstream declares a hex, a px size, or a duration.
   ============================================================ */

@import url('https://fonts.googleapis.com/css2?family=Anton&family=Inter:wght@400;500&display=swap');

/* Registered so the scroll-fill can interpolate --t in CSS rather
   than having JS compute colours. Without @property the browser
   treats it as a string and the mix jumps instead of animating. */
@property --t {
  syntax: '<number>';
  inherits: false;
  initial-value: 0;
}

:root {

  /* ==========================================================
     RAMP — ten steps, pure neutral.
     No warmth, no accent, no second hue anywhere on the site.
     Every step is R = G = B.
     ========================================================== */
  --w-00: #FFFFFF;
  --w-01: #F7F7F7;
  --w-02: #EBEBEB;
  --w-03: #D6D6D6;
  --w-04: #B4B4B4;
  --w-05: #8C8C8C;
  --w-06: #636363;
  --w-07: #3D3D3D;
  --w-08: #232323;
  --w-09: #000000;

  /* ==========================================================
     TYPE
     Anton has a hard floor of 24px. Below that its condensed
     forms close up and stop being readable. If a display line
     needs to be smaller, it is not a display line — set it in
     Inter 500.
     ========================================================== */
  --font-display: 'Anton', 'Arial Narrow', sans-serif;
  --font-text:    'Inter', system-ui, -apple-system, sans-serif;

  --fs-d1: clamp(40px, 7vw,   104px);   /* hero                */
  --fs-d2: clamp(30px, 6.4vw,  90px);   /* quotes, bookend     */
  --fs-d3: clamp(26px, 3.6vw,  40px);   /* section headers     */
  --fs-d4: clamp(24px, 2.4vw,  28px);   /* card names — Anton's floor is 24px */
  --fs-detail-title: clamp(34px,5.4vw,64px);  /* detail-page project title */
  /* Owner request Aug 2026: the whole sizing system moves to fluid
     clamp()/vw, not flat px — --fs-d1 through --d4 already did this;
     t1-t4 (body-text sizes) didn't. Same derivation each token below:
     preferred = original*0.075vw (so it lands on the exact original px
     at a 1333px-wide viewport — a plain, ordinary desktop width, not a
     tuned magic number), min/max are that same original -20%/+12.5%.
     No mobile floor to protect (see #mobile-gate, site.css — nothing
     below 900px is reachable at all), so the range only has to cover
     900px-and-up desktops through wide monitors, not phones. */
  --fs-t1: clamp(18px, 1.65vw, 25px);   /* lead paragraph      */
  --fs-t2: clamp(14px, 1.28vw, 19px);   /* body                */
  --fs-t3: clamp(11px, 1.05vw, 16px);   /* secondary           */
  --fs-t4: clamp(10px, 0.9vw,  14px);   /* eyebrows, meta      */

  --lh-display: 1.06;
  --lh-body:    1.55;
  --ls-display: -.014em;
  --ls-caps:     .09em;
  --measure:     64ch;

  /* ==========================================================
     SPACE — same fluid clamp() derivation as --fs-t1..t4 above.
     ========================================================== */
  --sp-1:  clamp(3px,   0.3vw,   5px);
  --sp-2:  clamp(6px,   0.6vw,   9px);
  --sp-3:  clamp(10px,  0.9vw,  14px);
  --sp-4:  clamp(13px,  1.2vw,  18px);
  --sp-5:  clamp(19px,  1.8vw,  27px);
  --sp-6:  clamp(26px,  2.4vw,  36px);
  --sp-7:  clamp(38px,  3.6vw,  54px);
  --sp-8:  clamp(51px,  4.8vw,  72px);
  --sp-9:  clamp(77px,  7.2vw, 108px);
  --sp-10: clamp(102px, 9.6vw, 144px);
  --sp-11: clamp(154px, 14.4vw, 216px);
  --sp-12: clamp(205px, 19.2vw, 288px);

  --container: 1160px;
  --r-sm: 2px;
  --r-md: 10px; /* Stack02's skill cards — owner asked for noticeably
                   rounded corners, past what --r-sm's sharp 2px reads
                   as; no other component uses this yet. */

  /* Page-edge horizontal padding — the one spacing value that's fluid
     rather than a flat step. Every section's content sits this far from
     the true viewport edge, at every width, with no snap point. Same
     clamp() shape the type scale already uses; sanctioned exception to
     the flat --sp-* scale, logged in EXCEPTIONS.md. Floor matches the
     old mobile value (--sp-5); ceiling is above the old flat desktop
     value (--sp-7) on purpose, so wide screens keep gaining room. */
  --pad-inline: clamp(24px, 4.8vw, 64px);

  /* ==========================================================
     MOTION — two curves. Not three, not one per component.
     ========================================================== */

  /* anything that travels. leaps, then settles:
     half the distance in the first 15% of the time. */
  --ease-move:   cubic-bezier(.17, .84, .44, 1);

  /* scroll-driven reveals only. slow off the mark, so it reads
     as placed rather than triggered: 19% of the travel in the
     first quarter of the time. */
  --ease-reveal: cubic-bezier(.45, .05, .25, 1);

  --dur-1:  200ms;   /* colour and border ONLY — never movement */
  --dur-2:  300ms;   /* opacity                                 */
  --dur-3:  620ms;   /* small movement                          */
  --dur-4:  900ms;   /* reveals                                 */
  --dur-5: 1109ms;   /* hovers, hero, wordmark                  */

  /* not rounded on purpose. 1109 was tuned by feel; a tidier
     1100 is a different thing. */

  --feather: 12;     /* scroll-fill, locked */

  --z-header: 90;
  --z-wash:   95;
  --z-gate:  100; /* #mobile-gate — above literally everything else */
  --z-loader: 99;
  --z-spill:  9999;
  --z-cursor: 10000; /* custom cursor — above literally everything, incl. spill */

  /* ==========================================================
     SANCTIONED EXCEPTIONS — logged in EXCEPTIONS.md.
     Named here rather than hardcoded in JS/CSS so the exception
     is still a token, just not on the --dur-1..5 scale.
     ========================================================== */
  --dur-spill-in:     2500ms;  /* theme toggle: hover-preview flood   */
  --dur-spill-out:     800ms;  /* theme toggle: hover-out retract     */
  --dur-spill-commit:  400ms;  /* theme toggle: click commits, faster */

  /* measured, not on the --dur-1..5 scale — system_2.md §5 names these
     exactly, distinct from the nearest general token on purpose */
  --dur-hero:     980ms;   /* hero line rise                        */
  --dur-descend: 1000ms;   /* detail-page entrance, from above      */

  /* pinned-stage clearance — not a spacing-scale value; each clears a
     specific sibling (a fixed head, a rail) measured against that
     sibling's real height, not a general rhythm choice */
  --method-track-pad-top:    186px;  /* clears .m__head            */
  --method-track-pad-bottom: 108px;  /* clears .m__rail            */
  /* Owner report: 210px undershot .sc__head's real rendered height
     (top:96px + eyebrow + "What was hard." + the sub line, ~217px+ by
     itself) — the sub line was landing on top of the first row of
     cards. Bumped past a safety margin instead of chasing an exact
     px match; fitGrid() (skills-scatter.js) already scales the grid
     down on shorter viewports if this leaves too little room. */
  --scatter-pad-top:         270px;  /* clears .sc__head           */
  --scatter-pad-top-mobile:  300px;  /* .sc__head wraps to 2 lines */

  /* dark theme's field — a little short of pure black. Owner request
     Aug 2026: pure --w-09 (#000000) felt "too vibrant"/harsh as an
     entire page field (distinct from the permanent Contact/Bookend
     close, which system_2.md deliberately locks to pure black — that
     rule is about the close specifically, not this optional theme
     toggle, itself already a logged exception). Not on the --w-*
     ramp: it's a one-off nudge for this single use, not a new step in
     the core neutral scale. */
  --dark-bg: #0d0d0d;
}

/* ============================================================
   SEMANTIC — the only names a component may reference.
   A component that reaches for --w-04 directly cannot be
   re-themed, and will be wrong the moment it sits inside the
   inverted close.
   ============================================================ */
:root,
[data-theme="light"] {
  --bg:           var(--w-01);   /* the field: off-white, not #FFF */
  --surface:      var(--w-00);   /* cards and panels lift off it   */
  --border:       var(--w-03);
  --text:         var(--w-09);
  --text-muted:   var(--w-05);

  --fill-dim:     var(--w-03);   /* scroll-fill, start             */
  --fill-bright:  var(--w-09);   /* scroll-fill, end               */
  --thread:       var(--w-04);   /* the connector in Work          */

  --invert-bg:    var(--w-09);   /* pure black. not #232323.       */
  --invert-text:  var(--w-00);

  --shadow:       rgba(0, 0, 0, .35);   /* card lift — darkens the field */
}

[data-theme="dark"] {
  --bg:           var(--dark-bg);
  --surface:      var(--w-08);
  --border:       var(--w-07);
  --text:         var(--w-01);
  --text-muted:   var(--w-04);

  --fill-dim:     var(--w-06);
  --fill-bright:  var(--w-01);
  --thread:       var(--w-06);

  --invert-bg:    var(--w-00);
  --invert-text:  var(--w-09);

  /* Not a darker rgba(0,0,0,..) — the field is already near-black, so
     darkening it further reads as nothing at all. A faint light glow
     is the standard dark-UI substitute for a drop shadow: it's the
     --surface/--bg contrast doing the visible lifting, this just
     softens the card's lower edge into it. */
  --shadow:       rgba(255, 255, 255, .07);
}

/* ============================================================
   THE FIELD — one background for the whole page, inverting once.

   01 hero · 02 quote 01 · 03 work · 04 quote 02 · 05 method
   06 stack · 07 about        all on --bg
   08 contact · 09 bookend    inverted

   Not alternating. A colour change marks a change in REGISTER,
   and the page has exactly one: everything up to Contact is
   argument and evidence, then it closes. Flip every other
   section and by the third the reader has stopped reading it as
   a signal and started reading it as wallpaper.

   Three mechanics depend on the field staying put:

     the scroll fill  runs --fill-dim to --fill-bright at Quote
                      01 and Quote 02. one on light and one on
                      dark and the same device reads as two
                      different effects.
     the thread       is --thread at mid grey. on a dark field it
                      needs re-toning, giving the component a
                      second state to maintain.
     the ribbon       bakes card surfaces at draw time. a field
                      change means re-rendering every card.

   Scope the overrides to the inverting section. NEVER flip these
   on :root — a section that does breaks every component above
   and below it.
   ============================================================ */
.is-inverted {
  background: var(--invert-bg);
  color:      var(--invert-text);

  --bg:         var(--invert-bg);
  --surface:    var(--w-08);
  --border:     var(--w-07);
  --text:       var(--invert-text);
  --text-muted: var(--w-05);

  --fill-dim:    var(--w-06);
  --fill-bright: var(--invert-text);
  --thread:      var(--w-06);
}

/* ============================================================
   BASE
   ============================================================ */
* { box-sizing: border-box; }

html {
  /* mandatory — without it mobile Safari inflates text and
     breaks every clamp() on the page */
  text-size-adjust: none;
  -webkit-text-size-adjust: none;
  /* Owner request: clicking a nav link should glide to its section,
     not hard-jump. Safe here specifically because this page was never
     lifted into position:fixed+transform (the one technique that
     would need scroll-behavior:auto, "JS owns scrolling" — see
     collision-protocol.md) — native anchor scrolling and the various
     Engine-driven eased-scroll mechanics read the same real scrollY
     independently either way, so this doesn't fight them. */
  scroll-behavior: smooth;
  /* Owner report: overscrolling past the very bottom of the page (trackpad
     rubber-banding) revealed body's own light --bg for a frame, reading as
     a leftover gap of page padding below the (dark, permanently inverted)
     Bookend — the last section on the page. html's canvas sits behind
     body, so matching it to --invert-bg here means that overscroll now
     shows more of the same dark field Bookend already ends on, instead of
     a mismatched light flash. */
  background: var(--invert-bg);
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-text);
  font-size: var(--fs-t2);
  line-height: var(--lh-body);
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

h1, h2, h3 {
  font-family: var(--font-display);
  font-weight: 400;
  line-height: var(--lh-display);
  letter-spacing: var(--ls-display);
  margin: 0;
}

/* text-wrap:pretty stops a paragraph ending on one orphaned
   word; balance evens the rag on short display lines. Both
   degrade to normal wrapping where unsupported. */
p            { max-width: var(--measure); text-wrap: pretty; }
h1, h2, blockquote { text-wrap: balance; }

a { color: inherit; text-decoration: none; }
ul { list-style: none; margin: 0; padding: 0; }

.container {
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: var(--pad-inline);
}

.sr-only {
  position: absolute; width: 1px; height: 1px;
  padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 3px;
}
