/* ==========================================================================================
   ND Website Redesign — SITE token layer
   ------------------------------------------------------------------------------------------
   Loads AFTER nd-brand-tokens.css (the canonical, generated brand file — never hand-edit that
   one; re-copy it from Drive when the brand system updates).

   This file does two jobs the brand file doesn't:
     1. SEMANTIC SURFACES — light and dark as EQUAL, swappable peers (see §1). This is what makes
        "a dark page then a light page", or a dark section next to a light one, trivial.
     2. FILLS THE GAPS — the brand tokens have no motion scale, no breakpoints, no layout widths,
        no z-index scale, and a spacing scale marked DRAFT that stops at 64px (too small for the
        editorial negative space this design calls for).

   DESIGN PRINCIPLE (Nathan, 2026-07-30): "nothing must stop me from doing or changing anything
   in a new way I want it." So this layer is an ENABLING PALETTE, not a rulebook:
     • every value is a plain custom property — override it anywhere, at any scope
     • no !important, no locked components, no forced defaults
     • the tokens make the on-brand choice easy; they never make a new choice impossible
   ========================================================================================== */

/* ------------------------------------------------------------------------------------------
   §1  SEMANTIC SURFACES — light and dark are peers, not default-and-override
   ------------------------------------------------------------------------------------------
   Put data-nd-theme="light" or "dark" on <html>, a <section>, or any single element. Children
   inherit. Mix as many as you like on one page — that's the point.

       <section data-nd-theme="dark">  … </section>
       <section data-nd-theme="light"> … </section>

   Accent is deliberately IDENTICAL in both themes (canonical: "orange is unchanged in dark
   mode"), so the one-orange-signal rule reads the same everywhere.
   ------------------------------------------------------------------------------------------ */

/* ⚠️ NO PURE BLACK ON A LIGHT GROUND. Nathan, 2026-08-01: "adjust all black text and buttons on
   the light pages i would like it to be a dark charcoal instead of pure black, we can use the dark
   charcoal that was used on the live site." That charcoal is #303035 — it is already in the brand
   system (--nd-color-dark-charcoal) and it is the same one the live site's dark panels use, so
   this is picking an existing token rather than inventing a colour.

   THE WHOLE RAMP HAD TO MOVE WITH IT, not just the heading colour. --nd-text-soft was #2A2A2A,
   which is DARKER than the new #303035 — leave it and the "soft" body copy reads heavier than the
   headings it sits under, which is worse than the thing being fixed. It steps to the brand's
   --nd-color-grey instead, so the three levels stay in order and all three are brand tokens.
        text    #303035   11.7:1 on the section's #F2F2F2
        soft    #595959    6.3:1
        muted   #6E6E6A    4.6:1   (see the correction below — that figure was against #F2F2F2)
   The rules and hairlines are tinted from the same charcoal rather than from ink, so nothing on a
   light ground is mixed from two different blacks.

   🐛 CORRECTION, 2026-08-02. The ramp above was measured against the SECTION ground (#F2F2F2) and
   every figure is right for it — but --nd-surface on this theme is the PAGE ground #D7D4D7, which
   is a full step darker, and the same colours measure lower there:
        text    #303035    8.9:1   passes
        soft    #595959    4.8:1   passes, only just
        muted   #6E6E6A    3.5:1   ⚠️ FAILS AA for normal text
   So --nd-text-muted stepped to #5C5C58 (4.57:1 on #D7D4D7, 6.00:1 on paper) — the brand muted's
   own warm-grey hue, walked down until it clears AA on the darker of the two grounds it can land
   on. A token is only safe if it is safe on every surface in its own theme.

   ⚠️ THE LIGHT RAMP IS NOW COMPRESSED, and that is a real cost: muted (4.57) sits very close to
   soft (4.77), so the two read almost the same. That is not a token problem, it is the page ground
   being dark for a light theme. If a wider ramp is wanted, LIGHTEN #D7D4D7 — do not lighten the
   text back, which is where this started. */
[data-nd-theme="light"] {
  --nd-surface:         var(--nd-color-page-bg);            /* #D7D4D7 */
  --nd-surface-raised:  var(--nd-color-paper);              /* #F2F2F2 */
  --nd-surface-panel:   var(--nd-color-panel);              /* #FFFFFF */
  --nd-text:            var(--nd-color-dark-charcoal);      /* #303035 */
  --nd-text-soft:       var(--nd-color-grey);               /* #595959 */
  --nd-text-muted:      #5C5C58;                            /* was --nd-color-muted #6E6E6A — see above */
  --nd-line:            rgba(48, 48, 53, .26);   /* was .16 - Nathan 2026-08-02: the editorial lines read too faint */
  --nd-line-strong:     rgba(48, 48, 53, .48);   /* was .34 - same note */
  --nd-scrim:           rgba(48, 48, 53, .55);
  color-scheme: light;
}

[data-nd-theme="dark"] {
  --nd-surface:         var(--nd-color-ink);                /* #0D0D0D */
  --nd-surface-raised:  var(--nd-color-dark-charcoal-deep); /* #1C1C21 */
  --nd-surface-panel:   var(--nd-color-dark-charcoal);      /* #303035 */
  --nd-text:            var(--nd-color-dark-heading);       /* #F4F2EE */
  --nd-text-soft:       var(--nd-color-dark-body);          /* #C6C4BE */
  --nd-text-muted:      var(--nd-color-dark-muted);         /* #9A9A9F */
  --nd-line:            rgba(255, 255, 255, .26);   /* was .16 - same note, dark ground */
  --nd-line-strong:     rgba(255, 255, 255, .48);   /* was .34 */
  --nd-scrim:           rgba(0, 0, 0, .62);
  color-scheme: dark;
}

/* Accent — same in both themes. Named here so components never reach for the raw brand token. */
:root {
  --nd-accent:      var(--nd-color-accent);       /* #F23207 — a SIGNAL, not a fill */
  --nd-accent-deep: var(--nd-color-accent-deep);  /* #A62103 */

  /* ⚠️ --nd-accent-text IS NOT --nd-accent, AND THE DIFFERENCE IS A WCAG FAILURE (2026-08-02).
     Measured: #F23207 is 4.84:1 on the dark ground and PASSES AA, but only 3.58:1 on paper and
     2.73:1 on the light page ground — which fails AA for normal text AND fails the 3:1 floor for
     large text, so there is no size at which brand orange is legible as TEXT on a light ground.
     #A62103, already in the brand file, measures 5.03 / 6.60 / 7.39 on those same grounds.

     So: --nd-accent stays the brand signal and is what SURFACES are painted (the .nd-fill sweep
     is still #F23207, and its ink label on that fill is 4.84:1 — a fill is measured against its
     own label, not against the page). --nd-accent-text is what INK is set in, and it follows the
     ground. Redefined per theme below; the :root value is the dark-ground one so anything outside
     a themed subtree still resolves.

     🚨 IF YOU ARE SETTING `color:` TO AN ACCENT, IT IS THIS TOKEN. Reaching for --nd-accent puts
     the failure straight back, silently, and only on light sections — which is exactly where it
     will not be noticed until an audit. */
  --nd-accent-text: var(--nd-color-accent);       /* dark ground: #F23207 @ 4.84:1 */
}

[data-nd-theme="light"] { --nd-accent-text: var(--nd-color-accent-deep); }  /* #A62103 @ 5.03-7.39:1 */
[data-nd-theme="dark"]  { --nd-accent-text: var(--nd-color-accent); }       /* #F23207 @ 4.84:1 */

/* Sensible starting point; any section can override.

   ⚠️ --nd-page-bg IS THE BELT TO lvh's BRACES. Nathan, 2026-08-01: "we need to ensure that whatever
   the page color is it bleeds across the entire screen regardless of what the browser UI does."
   Sizing the covering panels in lvh (see nd-services.css / nd-home.css) is the structural fix and
   handles it. This is the second line of defence: the scroll runtime sets --nd-page-bg to the
   colour of whatever ground is currently covering the screen, so if anything is ever a pixel short
   — a rounding error, a browser that reports lvh differently, a section built later that forgets —
   what shows through is the RIGHT colour rather than the dark default.
   Unset, it falls back to the theme surface and nothing has changed. */
html { background: var(--nd-page-bg, var(--nd-surface)); color: var(--nd-text); }

/* ------------------------------------------------------------------------------------------
   §2  SPACING — extends the brand scale (which is DRAFT and stops at 64px)
   ------------------------------------------------------------------------------------------ */
:root {
  /* brand steps re-exposed under a shorter name (0 / 8 / 16 / 24 / 32 / 48 / 64) */
  --nd-space-0:  var(--nd-spacing-0);
  --nd-space-1:  var(--nd-spacing-1);
  --nd-space-2:  var(--nd-spacing-2);
  --nd-space-3:  var(--nd-spacing-3);
  --nd-space-4:  var(--nd-spacing-4);
  --nd-space-6:  var(--nd-spacing-6);
  --nd-space-8:  var(--nd-spacing-8);

  /* editorial extension — the negative space this direction lives on */
  --nd-space-10: 80px;
  --nd-space-12: 96px;
  --nd-space-16: 128px;
  --nd-space-20: 160px;
  --nd-space-24: 192px;
  --nd-space-32: 256px;

  /* Section rhythm moved to §3a — it is a fluid ramp now, and defining it twice would mean two
     sources of truth for the same value. */
}

/* ------------------------------------------------------------------------------------------
   §3  LAYOUT
   ------------------------------------------------------------------------------------------ */
:root {
  --nd-container:        1440px;   /* OPT-IN cap — use .nd-container. Most of this design does not
                                      want it; see .nd-container--full and §3a. */
  --nd-container-narrow:  820px;   /* long-form reading measure */
  --nd-container-wide:   1760px;
}

/* ------------------------------------------------------------------------------------------
   §3a  FLUID SCALE — the responsive engine (Nathan, 2026-07-30)
   ------------------------------------------------------------------------------------------
   ⚠️ THIS REPLACED A BREAKPOINT-STEPPED, FIXED-PX APPROACH THAT WAS WRONG.

   Nathan's correction: "we build for 1080p as a single TEST resolution but the final result needs
   to be a dynamic responsive system that adapts to ANY resolution like a proper designed website
   would." The previous attempt hard-coded pixel sizes that stepped at 480/768/1280 and capped out
   around 1600px — so a 4K screen got 1080p's absolute sizes floating in a field twice the size.
   1080p is a place we CHECK. It is not the thing we design to.

   THE RULE: size things with a continuous ramp across the whole real range of screens. Reach for a
   breakpoint only when the LAYOUT genuinely changes (a different arrangement), never to resize.
   Prefer aspect-ratio over width when the question is really "is this screen tall or wide?".

   ── THE FORMULA ────────────────────────────────────────────────────────────────────────────
   Every ramp below interpolates linearly between two anchors: 360px and 3840px viewport width —
   the narrowest and widest screens in Nathan's verification set. For a value going v1 → v2:

       slope(vw)  = (v2 - v1) / 34.8
       intercept  = v1 - (v2 - v1) × 0.103448
       token      = clamp(v1, calc(<intercept>px + <slope>vw), v2)

   The clamp ends AT the anchors, so the value is linear the whole way and only ever holds still
   outside 360–3840. To retune anything, change its two anchor numbers and recompute — the comment
   on each line records the anchors so nobody has to reverse-engineer the maths.

   ── TWO SPEEDS, DELIBERATELY ───────────────────────────────────────────────────────────────
   Type and space scale HARD (they carry the composition — they should feel proportionally the
   same at every size). Interactive chrome scales GENTLY: a button that is 38px on a phone must
   not become 200px on a 4K screen just because that is "proportional". Ergonomics cap it, not
   geometry. That distinction is the whole reason this looks designed rather than zoomed.
   ------------------------------------------------------------------------------------------ */
:root {
  /* Page gutter — the distance content is held off the screen edge.        20px → 160px */
  --nd-gutter: clamp(20px, calc(5.517px + 4.023vw), 160px);

  /* ---- FLUID SPACE (scales hard) ---- */
  --nd-fs-1:  clamp(4px,  calc(1.103px  + 0.805vw),  32px);   /*   4 →  32 */
  --nd-fs-2:  clamp(8px,  calc(2.207px  + 1.609vw),  64px);   /*   8 →  64 */
  --nd-fs-3:  clamp(12px, calc(3.310px  + 2.414vw),  96px);   /*  12 →  96 */
  --nd-fs-4:  clamp(16px, calc(4.414px  + 3.218vw), 128px);   /*  16 → 128 */
  --nd-fs-6:  clamp(24px, calc(6.621px  + 4.828vw), 192px);   /*  24 → 192 */
  --nd-fs-8:  clamp(32px, calc(8.828px  + 6.437vw), 256px);   /*  32 → 256 */
  --nd-fs-12: clamp(48px, calc(13.241px + 9.655vw), 384px);   /*  48 → 384 */

  /* ---- HEADER CHROME (scales gently — see "two speeds" above) ----

     ⚠️ RETUNED 2026-07-31. Nathan: "the logo and header buttons must decrease in size as they are
     a bit too large, make sure 4k is still displaying correctly so that it does not become too
     small" — and mobile was explicitly approved as-is, so it must not move either.

     THAT IS THREE CONSTRAINTS ON A STRAIGHT LINE, WHICH ONLY HAS TWO ENDS. Pinning 360 and 3840
     and lowering the middle is impossible: a linear ramp through fixed endpoints has no middle to
     lower. Dropping the 3840 end instead is worse — 1920 sits at 44.8% of the way, so every pixel
     removed from desktop costs 2.2px at 4K, which is exactly what he asked me not to do.

     The answer is the FLOOR, which these ramps already carry for ergonomics. Steepen the line and
     let clamp's minimum hold the small end flat: below roughly 1000–1300px the floor is what
     renders, so phones and tablets are untouched by construction, the line is free to be much
     steeper above that, and 4K lands where it always did. Still continuous, still no media query —
     clamp() cannot produce a jump. The flat stretch is not a compromise here: it IS the ergonomic
     minimum, and the old ramp was already sitting within a pixel or two of it down there.
        control height  38 flat to ~1024, 45 at 1920 (was 50), 60 at 3840 (was 64)
        control text    10 flat to ~1192, 12.2 at 1920 (was 13.6), 18 at 3840 (unchanged)
        header padding  14 flat to ~1034, 26 at 1920 (was 31), 52 at 3840 (unchanged)
     Net at 1920 the header is 97px tall against 112px, and every value at 360–768 is within 0.3px
     of what it was. The logo has the same treatment in nd-header.css. */
  --nd-hdr-pad-y:     clamp(14px,   calc(0px      + 1.3542vw),  52px);  /*  14 flat →  52 */
  --nd-hdr-ctl-h:     clamp(38px,   calc(30px     + 0.7813vw),  60px);  /*  38 flat →  60  ergonomic */
  --nd-hdr-ctl-fs:    clamp(10px,   calc(6.4px    + 0.3021vw),  18px);  /*  10 flat →  18 */
  --nd-hdr-ctl-pad:   clamp(11px,   calc(8.621px  + 0.6609vw),  34px);  /*  11 →  34 */
  --nd-hdr-ctl-gap:   clamp(6px,    calc(4.552px  + 0.4023vw),  20px);  /*   6 →  20 */
  --nd-hdr-ctl-inner: clamp(7px,    calc(5.862px  + 0.3161vw),  18px);  /*   7 →  18 */
  --nd-hdr-icon:      clamp(15px,   calc(13.034px + 0.5460vw),  34px);  /*  15 →  34 */
  --nd-hdr-bars:      clamp(12px,   calc(10.552px + 0.4023vw),  26px);  /*  12 →  26 */
  --nd-hdr-logo-mark: clamp(19px,   calc(15.172px + 1.0632vw),  56px);  /*  19 →  56 */
  --nd-hdr-logo-word: clamp(7.5px,  calc(6.207px  + 0.3592vw),  20px);  /* 7.5 →  20 */
  --nd-hdr-logo-gap:  clamp(9px,    calc(7.241px  + 0.4885vw),  26px);  /*   9 →  26 */

  /* The logo's height, from 768 up. It lives here rather than only in nd-header.css because
     --nd-header-h below has to know about it. Retuned 2026-07-31 — see the note in nd-header.css
     for why the cap rather than the floor does the work in this one. */
  --nd-hdr-logo-h:    clamp(30px,   calc(26px     + 2.1875vw), 110px);  /*  30 → 110 (capped) */

  /* 🐛 Derived, and it has to be derived from the TALLEST thing in the header — which is the LOGO
     on every screen from 768 up, not a button. This read `pad*2 + ctl-h` until 2026-07-31, which
     understated the real header by 23px at 1920 (97 against 120) and by 35px before the retune.
     Nothing had collided yet, because the hero's padding was generous enough to absorb it. The
     moment the hero headline was lifted toward the logo, it slid straight under the header —
     the padding that was supposed to clear the header did not actually clear the header.
     ⚠️ Below 768 the <picture> swaps to the square MARK on its own smaller ramp, so this
     OVERSTATES the header there by a couple of pixels. That direction is safe: extra clearance. */
  --nd-header-h: calc((var(--nd-hdr-pad-y) * 2) + max(var(--nd-hdr-ctl-h), var(--nd-hdr-logo-h)));

  /* Fluid section rhythm — kept as a ramp for the same reason. */
  --nd-section-y:       clamp(72px, calc(48.28px + 6.44vw), 296px);
  --nd-section-y-tight: clamp(48px, calc(32.28px + 4.37vw), 200px);
}

/* ------------------------------------------------------------------------------------------
   §4  MOTION — the brand file ships only the locked easing; the rest is proposed here
   ------------------------------------------------------------------------------------------
   Animation carries ~90% of this design, so it needs a real scale. Keep animation to
   `transform` and `opacity` wherever possible: they're GPU-composited and don't force reflow.
   ------------------------------------------------------------------------------------------ */
/* ══════════════════════════════════════════════════════════════════════════════════════════
   SHARED MECHANICS — tokens that belong to an EFFECT rather than to a component.
   ------------------------------------------------------------------------------------------
   🚨 THESE LIVE ON :root FOR ONE HARD-WON REASON. A var() that cannot resolve takes its WHOLE
   declaration with it, silently — no error, no warning, nothing in the console. Four separate
   bugs on this project have been exactly that, and the most recent cost a day: --nd-char-dur was
   declared on .nd-hero__title, so re-using the per-character blur anywhere else dropped the
   animation entirely and the text rendered as nothing at all.

   THE RULE, now enforced by dev/lint-tokens.py: a token used by more than one component is
   declared here. A token used by one component may live on that component, and any var() reaching
   across a component boundary must carry a fallback. The linter fails the deploy otherwise.
   ══════════════════════════════════════════════════════════════════════════════════════════ */
:root {
  /* The per-character blur-focus mechanic — the hero headline, the rotating word, and anything
     carrying .nd-chars-in. An effect, not a component. */
  --nd-char-blur: 0.09em;
  --nd-char-dur:  520ms;

  /* The orange sweep behind a .nd-fill button. Quick to commit, slow to arrive. */
  --nd-fill-dur:  .62s;
  --nd-fill-ease: cubic-bezier(.12,.85,.22,1);
}

:root {
  --nd-dur-instant: 120ms;
  --nd-dur-fast:    220ms;
  --nd-dur-base:    380ms;
  --nd-dur-slow:    640ms;
  --nd-dur-slower: 1000ms;

  --nd-ease:         var(--nd-motion-easing-standard);  /* cubic-bezier(.22,1,.36,1) — locked ND easing */
  --nd-ease-out:     cubic-bezier(0, 0, .2, 1);
  --nd-ease-in:      cubic-bezier(.4, 0, 1, 1);
  --nd-ease-in-out:  cubic-bezier(.65, 0, .35, 1);

  --nd-stagger: 60ms;   /* base delay step for sequenced entrances */
}

/* WCAG 2.2 — honour reduced motion globally. Content must still be complete and readable. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --nd-dur-instant: 1ms; --nd-dur-fast: 1ms; --nd-dur-base: 1ms;
    --nd-dur-slow: 1ms;    --nd-dur-slower: 1ms; --nd-stagger: 0ms;
  }
  *, *::before, *::after {
    animation-duration: 1ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 1ms !important;
    scroll-behavior: auto !important;
  }
}

/* ------------------------------------------------------------------------------------------
   §5  EDITORIAL LINE-WORK — kept per Nathan (line art / editorial lines carry into the new site)
   ------------------------------------------------------------------------------------------ */
:root {
  --nd-rule:        1px;
  --nd-rule-strong: 2px;
  --nd-tick:        10px;   /* measurement ticks / registration marks */
  --nd-line-inset:  clamp(12px, 2vw, 28px);
}

/* ------------------------------------------------------------------------------------------
   §6  RADII — brand radii re-exposed, plus a square option (the new direction is more editorial)
   ------------------------------------------------------------------------------------------ */
/* 🐛 FIXED 2026-07-30 — these two used to be re-declared here as:
       --nd-radius-panel: var(--nd-radius-panel, 16px);
       --nd-radius-pill:  var(--nd-radius-pill,  999px);
   A custom property whose value references ITSELF is invalid at computed-value time, so both
   resolved to NOTHING. Every `border-radius: var(--nd-radius-pill)` therefore computed to 0 and
   the header buttons rendered as square rectangles instead of pills — silently, with no error.
   Nathan spotted it as a design problem ("change the menu and lets talk buttons to a pill shape").
   The brand file already defines both correctly, so the fix is simply not to shadow them here.
   ⚠️ Never write `--x: var(--x, fallback)` — that is not how you inherit a token, it destroys it. */
:root {
  --nd-radius-none:  0px;
  --nd-radius-sm:    6px;
  /* --nd-radius-panel and --nd-radius-pill come from nd-brand-tokens.css (16px / 999px). */

  /* THE CORNER ON EDITORIAL ARTWORK — the eight service pictures and the case-study images.
     Nathan, 2026-08-04: "give the 8 services and case studies images, rounder edges." They were 0
     and 2px, i.e. square. ONE token for both sets, on :root so it is safe to use from any
     component, because the two must round by the same amount — they are the same kind of object
     seen in two sections, and a difference between them reads as a mistake rather than a choice.
     Fluid rather than fixed so a phone does not get a desktop-sized corner on a much smaller
     picture. Turn it here to change both. */
  --nd-media-radius: clamp(8px, 0.7vw, 18px);
}

/* ------------------------------------------------------------------------------------------
   §7  Z-INDEX — one named scale so nothing ends up at z-index: 99999
   ------------------------------------------------------------------------------------------ */
:root {
  --nd-z-base:    0;
  --nd-z-raised:  10;
  --nd-z-sticky:  100;
  --nd-z-header:  500;
  --nd-z-overlay: 1000;
  --nd-z-modal:   1100;
  --nd-z-toast:   1200;
  --nd-z-menu:    1500;   /* the burger overlay — above the header, below the load scene */
  --nd-z-intro:   2000;   /* the load scene covers EVERYTHING, including a toast or a modal */
}

/* Scroll lock, used only while the intro scene is on screen. Added and removed by nd-intro.js,
   which ties removal to the same finish() that deletes the overlay — so the page can never be
   left unscrollable if something goes wrong mid-sequence. */
html.nd-intro-open,
html.nd-intro-open body { overflow: hidden; }

/* ------------------------------------------------------------------------------------------
   §8  BREAKPOINTS (reference only)
   ------------------------------------------------------------------------------------------
   CSS custom properties cannot be used inside media-query conditions, so these are documentation
   for humans and JS — keep the literals below in sync with the @media rules you write.
       sm 480px · md 768px · lg 1024px · xl 1280px · 2xl 1600px
   Mobile-first: write the base styles for small screens, then min-width up.
   ------------------------------------------------------------------------------------------ */
:root {
  --nd-bp-sm:  480px;
  --nd-bp-md:  768px;
  --nd-bp-lg: 1024px;
  --nd-bp-xl: 1280px;
  --nd-bp-2xl: 1600px;
}

/* ------------------------------------------------------------------------------------------
   §9  RETIRED — do not use in this build
   ------------------------------------------------------------------------------------------
   nd-brand-tokens.css still ships the liquid-glass tokens and the .nd-glass* utilities. They are
   vendored verbatim so the file stays diff-able against Drive — but the glass language and the
   ND Revolution 3D objects (camera / drone / lens / light) are RETIRED by this project's North
   Star. Don't build with them. (Line-art and editorial scaffolding DO carry over — see §5.)
   ------------------------------------------------------------------------------------------ */
