:root {
  /* ---------- Color ----------
     Named values come from the Figma file's variables (get_variable_defs on
     464:84045). Values marked "sampled" are read off the frame render because
     the design uses a literal fill there rather than a bound variable --
     notably the hero, which is #00bf00 and NOT the "Redonk Lime" token. */
  --rdk-green: #00bf00;        /* sampled -- hero stage fill (464:84050)   */
  --rdk-lime: #68f318;         /* var: Redonk Lime                         */
  --rdk-lime-bright: #79ff24;  /* var: Redonk Lime Bright — the pricing hero's
                                  eyebrow and the plan banner's star. A SECOND
                                  lime, named in the Figma file, not drift:
                                  yellower than --rdk-lime and used where lime
                                  sits ON the brand green.                  */
  --rdk-dark: #002300;         /* var: Redonk Dark                         */
  --rdk-black-green: #001407;  /* sampled -- intro/case-study page ground  */
  --rdk-scanner-blue: #0860ee; /* var: Scanner Blue -- Datascan card       */
  --rdk-blue: #005eff;         /* var: Brand/Blue                          */
  --rdk-blue-500: #2051f4;     /* var: Blue/500 (= Text/text-accent)       */
  --rdk-blue-900: #010622;     /* var: Blue/900                            */
  --rdk-gold: #fbc215;         /* var: Brand/Gold                          */
  --rdk-white: #ffffff;
  --rdk-off-white: #eff2f3;    /* sampled -- case-study headings           */
  --rdk-grey: #b4b4b4;         /* sampled -- case-study eyebrow            */
  --rdk-rule: #555555;         /* sampled -- 0.5u case-study row dividers  */
  /* Case-study card colours, one per client, from node 689:10256 (2026-09-15,
     replacing 34:3319 -- BlueDolphin and Arctic Wolf came off the homepage,
     Everpure and Exabeam went on). Literal fills, not variables: note
     Datascan is NOT the "Scanner Blue" #0860ee token, and Exabeam's #009d00
     is the CLIENT's green, not --rdk-green. They match the same four brand
     colours tools/projects.py gives the work pages. nuHarbor also carries a
     rotated artwork texture over its fill; see cases.css. */
  --rdk-case-datascan: #0061f1;
  --rdk-case-everpure: #fe5000;
  --rdk-case-exabeam: #009d00;
  --rdk-case-nuharbor: #076ee5;

  /* ---------- Type ----------
     Two faces, both self-hosted (css/fonts.css):
       brand   -- Redonk Avant Garde Bold. Nav, buttons, small caps labels.
                  Confirmed against Figma for the nav (464:84109 et al).
       body    -- Satoshi, variable 300..900. Everything else.
     Every coded section's faces, weights, sizes, tracking and OpenType
     features were audited against node 1:479 on 2026-08-25 -- see the
     Typography section in CLAUDE.md. The client has flagged Satoshi as
     possibly temporary. */
  --rdk-font-brand: "Redonk Avant Garde", system-ui, -apple-system, "Segoe UI", sans-serif;
  --rdk-font-body: "Satoshi", system-ui, -apple-system, "Segoe UI", sans-serif;
  /* ---------- Display / heading ladder (2026-09-09) ----------
     NOT tokens, on purpose -- this is the map, and each rule still carries its
     own literal. Body copy earned tokens because seventeen runs repeated four
     values and drifted; the display runs are mostly one or two per size, and
     the drift here was in TRACKING and LEADING rather than in the sizes. So
     this block is the answer to "what step is there between X and Y", which is
     what nobody could answer before.

     Three steps are NAMED IN FIGMA and are the authority. Their tracking is a
     PERCENTAGE in that file -- reading it as px is what shipped three titles
     at -6%; see the note on .rdk-cases__title.

       Display/XL   104   lh 0.94   -5%   (-5.2)    <- Figma variable
       Display/L     88   lh 1      -4%   (-3.52)   <- Figma variable
       Heading/M     32   lh 1.1    -2%   (-0.64)   <- Figma variable

     The rest are unnamed in Figma and are named here so they can be asked for.
     THEIR TRACKING IS INTERPOLATED BETWEEN THE THREE ANCHORS, not read off a
     frame -- see the ramp note below:

       Display/M     72   -3.43%  (-2.47)  blog post title
       Display/S     62   -3.00%  (-1.86)  about headline
       --            51   -2.69%  (-1.37)  case-study client name
       Heading/XL    46   -2.50%  (-1.15)  intro headline, blog feature title,
                                           blog related title, project
                                           headline, blog article h2
       Heading/L     40   -2.27%  (-0.91)  services intro headline, services
                                           tile name, work tile name, blog card
                                           title, blog author name, work intro
       Heading/S     28   -1.86%  (-0.52)  blog post-nav title

     **THE RAMP (client, 2026-09-09: "the letter-spacing seems a little tighter
     at smaller sizes").** It was, and badly: the ladder ran -5% at 104 but -4%
     at 40 and -3% at 28, so the SMALL end was tracked tighter than the large
     end -- the reverse of the convention, and four rungs were outright
     inversions (72 tighter than 88, 51 tighter than 62, 40 tighter than 46, 28
     tighter than 32). Every rung is now interpolated linearly in SIZE between
     the anchors, `tracking = -2% - (size - 32) * 2/56` up to 88, so the three
     Figma values stay exact and everything between is monotonic. Verified: the
     ladder 104 -> 28 decreases in magnitude at every step.

     TWO THINGS REFLOWED, both content-dependent. The work index intro went 3
     lines -> 4 and was RESOLVED by widening its measure 920u -> 960u (client),
     which it had room for. The other stands and is recorded so nobody hunts it
     as a bug: two of the nine blog card titles went 1 line -> 2, leaving two
     grid rows with unequal card heights (817 against 772). That is not a
     layout fault -- a slightly longer title does the same, and the ninth card
     was already two lines -- but the grid does not equalise, so if it should,
     that is its own change rather than a tracking one.

     Two sizes sit outside the ladder deliberately: 56 (the mobile menu link, a
     phone-only component) and 51 (the case-study client name).

     **38 WAS MERGED INTO 40 ON 2026-09-09** (client). They were two steps
     1.05x apart where every other rung is 1.10-1.22x, and the work index used
     BOTH on one page. Verified before merging: line counts are IDENTICAL at 38
     and 40 for all nine blog cards and the work intro, so nothing reflowed.

     STILL OPEN, and the reason this is a comment rather than a token set:
     LEADING IS NOT YET ONE VALUE PER STEP. Heading/L carries 0.94 / 1 / 1 /
     1.04 / 1.05 / 1.15 across its six rules, Heading/XL carries 1.04 / 1 / 1
     plus the intro's fixed 55u, and four rules use `line-height: normal`,
     which is whatever the face reports. Settle those and this becomes a real
     token set. */

  /* ---------- Body copy scale (2026-09-09) ----------
     FIVE steps, and every body run on the site uses one of them. Before this
     the same job was done at 24 / 20 / 19.2 / 18 / 17 / 16 / 14 across nine
     stylesheets, with leading at 1.3 / 1.35 / 1.4 / 1.5 / 1.55 and tracking
     drifting between 0.0033em and 0.01em -- consistent-looking inside any one
     section and not across any two.

     All five are Satoshi at 450 and 0.01em. Body proper is 150% leading;
     the LEDE takes 130%, because larger type needs less -- the Figma file
     agrees, giving its own 25.6 lede a 1.2. Tracking is written in `em` on
     purpose: it is 1% of whatever the size resolves to, so it stays correct at
     the mobile px values below and at every width in between. That is exactly
     the drift class this pass removes, so do not re-express it in `--u`.

     Sizes are lengths, not numbers -- consume them as `font-size:
     var(--rdk-body-m)`, never `calc(N * var(--rdk-body-m))`.

     WHAT GOES WHERE -- these are ROLES, not just sizes:
       body-xl  lede        a paragraph you actually READ under a display
                            heading. The intro's four lines in a 683 measure.
                            One use, deliberately.
       body-l   standfirst  a SHORT supporting line under a display heading --
                            one to three lines that set up what follows rather
                            than being the reading itself. Footer blurb (under
                            the 104 CTA heading), case-study description (under
                            the 51 client name).
       body-m   default     running prose: project, services, blog article,
                            author bio, dialog blurb.
       body-s   small       cards, tiles, excerpts, form chrome, tables.
       body-xs  fine        legal line, figcaptions.

     **THE STANDFIRST STEP WAS COLLAPSED ONTO body-m FOR ONE COMMIT AND PUT
     BACK (client, 2026-09-09: the footer blurb "looks a little small").** It
     is a real step, not drift, and the evidence is worth keeping because it is
     the same evidence any future "can these two merge?" needs:
       - the frames set BOTH of these at 20 independently, for the same job;
       - at 20 the footer's content block measures the design's own 392 with
         87/87 clearance, and the case row its documented 233 -- at 18 they are
         391 and 233, so the geometry does not distinguish them and the DESIGN
         NUMBER is what 20 reproduces;
       - it costs nothing: one line at both sizes in the footer, three at both
         in the case row, no box moves either way.
     What it buys is the gap under a 104 heading. Merging it made the footer
     read thin, which is exactly what a standfirst is there to prevent.

     A NEW BODY RUN PICKS A STEP; IT DOES NOT DECLARE A SIZE. If none of the
     five fits, that is a design question -- raise it rather than adding a
     sixth number to one stylesheet, which is how the old spread happened. */
  --rdk-body-xl: calc(24 * var(--u));
  --rdk-body-l: calc(20 * var(--u));
  --rdk-body-m: calc(18 * var(--u));
  --rdk-body-s: calc(16 * var(--u));
  --rdk-body-xs: calc(14 * var(--u));
  --rdk-body-lh: 1.5;      /* everything but the lede -- the 150% */
  --rdk-body-lh-xl: 1.3;   /* lede only: 24 is big enough to want less */
  --rdk-body-track: 0.01em;
  /* 450, down from 500 (client, 2026-09-09). Satoshi is declared
     `font-weight: 300 900`, so this is a real point on the variable axis and
     not a snap to 400 -- CONFIRMED BY ADVANCE WIDTH, which is the only way to
     tell a wired-up axis from a dead one, since a declaration reads back the
     same either way. At 200px the same string measures 3527.2 at 400, 3576.7
     at 450 and 3626.2 at 500: even ~24.7px steps per 25 units. The serif
     fallback measures 3188, so the face really is loading.
     450 is ~1.4% narrower than 500 and nothing reflowed -- line counts are
     identical at both weights on all five page types. Changing it is this one
     number; it reaches every body run and nothing else. */
  --rdk-body-weight: 450;

  /* There is deliberately no --rdk-font-mono. The case-study tag chips were
     the only thing using it -- set in three different mono faces across the
     rows in Figma, none of them a project font -- and the 2026-08-25 pass
     moved them to Satoshi Medium. Nothing on the page is mono now. */

  /* ---------- Layout ----------
     --u is "one Figma pixel" as a real CSS length, so design values can be
     written verbatim as `calc(N * var(--u))`. Three behaviours in one
     expression:
       < 1600      -> 100vw/1600  scale DOWN with the viewport
       1600..1880  -> 1px         FIXED: widening only reveals more green
       > 1880      -> 100vw/1880  scale UP so the shapes keep bleeding off
                                  both edges instead of drifting inward
     1880 is not arbitrary: the hero artwork spans -136 (cube's left edge) to
     1744 (X shape's right edge) = 1880u. Pinning the scale-up divisor to that
     span means the cube's left edge always lands exactly on the viewport
     edge, at any width.

     Keep every operand a LENGTH (`1px`, not `1`) or min()/max() cannot
     compare them. Lives on :root so the fixed nav scales identically. */
  --u: max(min(100vw / 1600, 1px), 100vw / 1880);

  /* ---------- --du, the DISPLAY unit (2026-09-16) ----------
     `--u` for every layout length; `--du` for display TYPE and its tracking.
     They are the same thing everywhere except on a touch tablet, where --du
     stops falling — see the floor below. Identical here, so nothing that
     scales with the window changes by declaring it. */
  --du: var(--u);

  /* Design width of the hero stage. The stage is a fixed-width, centred box,
     which is what gives every child v1's "locked to design size, centred, so
     a wider window just reveals more background" behaviour for free. */
  --rdk-frame: 1600;
}

/* The design has no mobile frame. Below 768px the stage stops being a fixed
   1600x897 canvas and becomes the full hero box, with the artwork re-anchored
   to the top/bottom edges (see the media query in hero.css) -- otherwise a
   897u-tall stage on a 375px phone is a 210px sliver at the bottom of the
   screen. --u stays tied to viewport width so the wordmark keeps its designed
   89%-of-frame width rather than being cropped. */

/* ---------- Body copy scale, phones ----------
   --u is 100vw/1600, so a 18u body run is 4.4px on a 390 screen -- the
   collapsing-unit trap this file's --u note describes. The four steps are
   therefore restated as straight px below 768, exactly as every section's own
   mobile block does for its type.

   THEY ARE OVERRIDDEN HERE AND NOWHERE ELSE. A section that also sets a
   font-size on a body run in its own mobile block wins on cascade order and
   silently takes that rule back off the scale, which is what the old
   17/16/15/15.4/14 spread was. The 2026-09-09 pass deleted every one of those
   overrides; do not reintroduce one.

   The steps compress on a phone -- 18/17/17 for lede, standfirst and body
   against 24/20/18 -- because the display sizes above them compress too: the
   intro headline is 25px here against 46u, so a 20px lede under it would read
   as the same size rather than as a step down.

   **THE STANDFIRST MEETS BODY EXACTLY ON A PHONE, AND THAT IS DELIBERATE.**
   body-l and body-m are both 17 below 768, so the step is a DESKTOP
   distinction that collapses here. The mobile frames do the same thing: they
   set the footer blurb at 16, the case description at 15 and the project,
   services and dialog body all at 16, i.e. one size for the lot. A 390 screen
   has no room for a five-step body scale, and holding the two apart here
   would be inventing a distinction the designs do not draw. Keep them equal
   unless a phone frame actually separates them. */
@media (max-width: 767px) {
  :root {
    --rdk-body-xl: 18px;
    --rdk-body-l: 17px;   /* see below -- meets body-m on purpose */
    --rdk-body-m: 17px;
    --rdk-body-s: 15px;
    --rdk-body-xs: 14px;
  }
}

/* TABLETS FALL BETWEEN THE TWO AND GET NEITHER (2026-09-16).
   --u is 100vw/1600, so the calc() sizes above are a share of the WINDOW --
   and the px floor that rescues them only exists below 767. In between, the
   body scale collapses with nothing to catch it: measured at 820 wide,
   body-m renders 9.2px, body-s 8.2 and body-xs 7.2, i.e. the tablet reads
   SMALLER than a phone does. Same fault as the About eyebrow rendering
   4.875px -- the collapsing unit, at a width nobody had put a floor under.

   IT IS A `max()`, NOT A FLAT px VALUE, AND THAT IS WHAT MAKES IT SAFE.
   Stated flat it would LOWER the size wherever the scaled value has already
   overtaken it -- body-m passes 17 at about 1511 wide -- so a wide touch
   screen would have been pulled back down to the floor, and the boundary
   itself would be a cliff. As a floor it only ever raises, it is a no-op
   once the design's own size is bigger, and the standfirst step (body-l
   against body-m) reappears on its own as the window widens.

   IT TAKES THE PHONE'S OWN NUMBERS, NOT A THIRD SET. One floor is easier to
   keep true than two, and the display type above it is --u-scaled and small
   here too, so the phone's compressed steps are the right relationship --
   the same argument the phone block makes for collapsing body-l onto body-m.

   THE SECOND QUERY IS FOR A TABLET IN LANDSCAPE, AND IT HAS TO ASK ABOUT
   THE POINTER BECAUSE WIDTH CANNOT TELL THEM APART. An iPad Air on its side
   is 1180 and an iPad Pro is 1366 -- both past any sane portrait ceiling,
   both still tablets, and both sat in exactly the hole this block exists to
   fill (13.3px and 15.4px body-m). A laptop is those widths too, so a
   width-only rule would have to choose which one to get wrong. `hover: none`
   chooses correctly by construction: a desktop, and a touchscreen laptop
   with a mouse, both report `hover: hover` and CANNOT match this. Behaviour
   on input capability, styling on width -- this is the one place styling has
   to ask as well, because the two devices are the same width.

   Anything below 768 is the phone block above; the two never both match. */
@media (min-width: 768px) and (max-width: 1024px),
       (min-width: 768px) and (hover: none) {
  :root {
    --rdk-body-xl: max(18px, calc(24 * var(--u)));
    --rdk-body-l:  max(17px, calc(20 * var(--u)));
    --rdk-body-m:  max(17px, calc(18 * var(--u)));
    --rdk-body-s:  max(15px, calc(16 * var(--u)));
    --rdk-body-xs: max(14px, calc(14 * var(--u)));
  }
}

/* ---------- PORTRAIT TAKES THE WHOLE SCALE DOWN 1px (client, 2026-09-16) ----
   "Decrease that font size by 1px universally on tablet portrait." Asked of
   the intro lede, confirmed as the whole scale: every step drops one, so the
   five keep the relationships to each other they already had.

   IT IS A SEPARATE BLOCK BECAUSE THE FLOOR ABOVE ALSO SERVES LANDSCAPE. That
   query is width-OR-pointer and says nothing about orientation, so editing
   its numbers would take a landscape tablet with it — and a landscape tablet
   was never the complaint. Both of its clauses are repeated here with
   `and (orientation: portrait)` so the pairing cannot drift: a 768-1024
   desktop window in portrait and a touch tablet in portrait both match, and
   1024 x 768 landscape matches neither.

   A media query adds no specificity, so this MUST stay after the block above
   — at equal weight source order is the whole of it.

   STILL A `max()`, and that is the part not to lose. The floor may only ever
   RAISE the scaled value; lowering the number moves where it stops binding,
   it does not pin the size. body-xl now hands over to `24 * --u` at about
   1133 rather than 1200, and between those two widths portrait is unchanged
   because the scaled value is what wins there anyway. */
@media (min-width: 768px) and (max-width: 1024px) and (orientation: portrait),
       (min-width: 768px) and (hover: none) and (orientation: portrait) {
  :root {
    --rdk-body-xl: max(17px, calc(24 * var(--u)));
    --rdk-body-l:  max(16px, calc(20 * var(--u)));
    --rdk-body-m:  max(16px, calc(18 * var(--u)));
    --rdk-body-s:  max(14px, calc(16 * var(--u)));
    --rdk-body-xs: max(13px, calc(14 * var(--u)));
  }
}

/* ---------- THE DISPLAY LADDER HAS A FLOOR ON TOUCH TABLETS (2026-09-16) ----
   Client: "the headlines should scale and the body copy should have fixed
   sizes for tablet. Is there something wrong with this approach? Now that the
   body copy sizes have been fixed the headlines seem too small proportionally."

   Half right, and the half that was missing is the floor. Body pinned while
   headlines keep scaling from a 1600 baseline is exactly what collapses the
   contrast — measured on the homepage, every ratio fell about 40%:
   intro headline to body **1.92 -> 1.31**, cases title to description
   **4.40 -> 2.65**, case name **2.55 -> 1.54**, footer heading **5.20 ->
   3.14**.

   **AND IT WAS AN INVERSION, NOT A MATTER OF TASTE.** A tablet was drawing
   SMALLER headlines than a PHONE, on a screen more than twice as wide —
   intro headline 23.6 against the phone's 25, cases title 45.1 against 46,
   footer heading 53.3 against 56, case name 26.1 against 34. The phone has
   designed px sizes and the desktop scales; the tablet fell below both, the
   same hole the body scale was in a week ago.

   **THE DESKTOP RATIO CANNOT BE THE TARGET, and that is worth stating so
   nobody tries.** Holding 4.4:1 against a floored 17px body would put a 75px
   headline on an 820 screen. The phone does not hold it either — its intro
   headline is 1.39:1 against its body, next to the tablet's 1.31. So the aim
   is only that a tablet sits BETWEEN the phone's designed sizes and the
   desktop's, which at 0.68 it does on every step:

       step   phone   tablet @ 820   desktop
       104     40/56      70.7         104     services name / footer heading
        88       46        59.8         88     cases title
        72     30.4        49.0         72     About headline
        51       34        34.7         51     case name
        46       25        31.3         46     intro headline

   **THERE IS NO STEP AT THE BOUNDARY, because `max()` makes the two curves
   MEET.** The floor binds while `--u` < 0.68, i.e. below about 1088, and the
   scaled value takes over above it at exactly the same size — so a tablet
   crossing 1088 by rotating sees a continuous change, not a jump.

   **IT CANNOT REACH A DESKTOP**: `hover: none` is the gate, and a desktop —
   or a touchscreen laptop with a mouse — reports `hover: hover`. Above 768
   only, so the phone keeps the designed px sizes its own frames give it.

   ONE NUMBER TUNES THE WHOLE LADDER. That is the point of doing it as a unit
   rather than as fifty floors: the steps keep their designed ratios to each
   other by construction, and there is a single dial to move. */
@media (min-width: 768px) and (hover: none) {
  :root {
    --du: max(0.68px, var(--u));
  }
}
