/* ============================================================
   babah3d.com — the whole stylesheet. There is only one.

   No build step, no framework, no fonts to download, no npm. A page
   here is an HTML file and this file, and that is the entire stack.
   It is meant to stay that way: the site will be edited far more often
   than it is engineered, usually to change a sentence or swap a photo,
   and a toolchain is a thing that breaks between those visits.

   THE RULE THIS DESIGN IS BUILT ON: one idea per screen, and the
   picture does the talking. Big type, a lot of empty space, almost no
   chrome, and never two paragraphs where one line will do. If a
   section needs explaining, the photograph in it is the wrong
   photograph. Adding copy is how this design dies.
   ============================================================ */

:root {
    /* Surfaces. Near-white rather than white: a page of pure #fff with
       #fff cards has no depth at all, and the soft grey is what makes a
       full-bleed photograph look placed rather than pasted. */
    --bg: #ffffff;
    --bg-soft: #f5f5f7;
    --ink: #1d1d1f;
    --ink-2: #6e6e73;
    --hairline: rgba(0, 0, 0, 0.12);
    --hairline-soft: rgba(0, 0, 0, 0.06);

    /* The pink is Babah3D's, carried over from the app's stylesheet so
       the site and the tool are visibly the same shop.

       THE RULE, IN BOTH DIRECTIONS — the first version of this comment
       only stated the first half, and the second half shipped broken for
       weeks behind it:

         1. --pink must never be a `color:`.  #ff7aa8 on #ffffff = 2.44:1.
         2. --pink must never be a fill that carries white `color:`.
            #ffffff on #ff7aa8 = 2.44:1 — THE SAME TWO COLOURS AND THE
            SAME RATIO. Contrast is symmetric: which of the pair is the
            ink makes no difference whatsoever to the number.

       So --pink is decorative only: a wash, never a boundary and never
       under type. It is also under the 3:1 that WCAG 1.4.11 asks of a
       border or a control's edge, which is why the .todo outline and the
       .ph border are --pink-text and not this.

       Four tokens, and each one has exactly one job:

       --pink       #ff7aa8  decorative wash. Never ink. Never under ink.
       --pink-text  #b3275d  ink ON the page background. Scheme-aware,
                             because the page background flips. Its dark
                             value is #de6896 — the SAME hue and the SAME
                             saturation as #b3275d, lifted in lightness.
                             Not a sixth pigment: it is the light value on
                             a dark page, exactly as --pink-btn-ring is
                             --pink-btn lifted. See the dark block below.
       --pink-btn   #90194c  a fill that carries WHITE ink. Deliberately
                             NOT --pink-text and deliberately NOT flipped
                             in dark mode: its partner is #ffffff, and
                             white does not flip. It must stay a separate
                             token or dark mode would repaint the buttons
                             with --pink-text's dark value and put white
                             back under the bar — 1.70:1 when that value
                             was #ff9dc0, 2.96:1 now it is #de6896. Both
                             fail 4.5:1; toning the secondary down did not
                             make this any less true.
       --pink-btn-ring #bb2163  the button's EDGE, applied in dark mode
                             only. A tint of --pink-btn's own hue, not a
                             fifth colour. It lives here rather than
                             inline in the rule so that the whole palette
                             is still readable in one place — the reason
                             this block exists at all. Nothing in light
                             mode uses it; see .btn further down for why.

       WHY #90194c AND NOT #b3275d. #b3275d was a colour chosen to clear a
       bar. #90194c is the logo's own magenta — the value in the mesh
       markup on services.html, alongside orange #ed5f2f and teal #207a64.
       The owner picked it. It is the brand's colour rather than a
       near-miss of it, and it is also the better number in light mode.

       The site now has THREE pinks, not two: --pink-text is still
       #b3275d, so the filled pill and the plain text link beside it are
       no longer the same pigment. In light mode that is an improvement
       and it is measured below — the primary now out-contrasts its own
       secondary, which it did not when they were the same value.

       Measured this session in a real headless Chrome, from the computed
       style of an actual .btn against the backdrop resolved by walking
       its ancestors — not from the token (fleet/STATUS-A53.md §3):
         #ffffff on --pink-btn #90194c ........ 8.64:1  (bar 4.5)  was 6.24
         #ffffff on hover      #c2185b ........ 5.87:1  (bar 4.5)
         --pink-btn vs #ffffff page ........... 8.64:1  (bar 3)    was 6.24
         --pink-btn vs #f5f5f7 --bg-soft ...... 7.94:1  (bar 3)    was 5.73
         --pink-btn vs #000000 page ........... 2.43:1  <-- FAILS  was 3.37
         --pink-btn vs #0e0e10 --bg-soft ...... 2.23:1  <-- FAILS  was 3.09

       THE LAST TWO LINES ARE WHY THE DARK-MODE RING EXISTS. #90194c is
       darker than #b3275d, so every light-mode figure got better and both
       dark-mode figures went under WCAG 1.4.11's 3:1. The colour was not
       quietly swapped for a compliant one — the owner chose this pigment.
       The pill keeps it in BOTH schemes and dark mode gets an edge instead.

       Hover goes BRIGHTER, not deeper, which is the opposite of what
       this file used to do. Deeper was measured first and it fails: the
       obvious hover (#a22354) is 7.23:1 with white but only 2.90:1
       against a black page, so the pill's own edge disappears in dark
       mode. Brighter clears both bars at once.

       #c2185b is now 9.7 points of lightness above the base rather than
       0, so the hover reads as a bigger step than it used to. That is a
       second colour decision and the owner made one, so it is left alone
       and written up in fleet/STATUS-A53.md §5 instead of adjusted here. */
    --pink: #ff7aa8;
    --pink-text: #b3275d;
    --pink-btn: #90194c;
    --pink-btn-hover: #c2185b;
    --pink-btn-ring: #bb2163;

    --radius: 14px;
    --nav-h: 48px;

    /* THE FACET. Added 2026-08-01.

       The owner asked for "some slight pixelated theme ... just like the
       logo". The logo is not pixelated — it is LOW-POLY: triangular facets
       with the gradient running across them. The distinction is the whole
       design, because a low-poly mesh is not a decoration here. An STL file
       IS a triangulated surface. The brand mark is a mesh and so is every
       object this shop prints, so the motif is the craft rather than a
       texture laid over it.

       Three placements and no more — see the FACET MOTIF block near the end
       of this file for what they are and what was tried and dropped. The
       rule at the top of this file has not moved: one idea per screen, and
       the picture does the talking. A faceted texture that competes with a
       product photograph has failed, so none of these three sit ON a
       photograph — they sit on its frame, on a label, or behind the logo.

       --facet is the corner a picture frame loses.

       IT USED TO BE clamp(11px, 1.9vw, 20px) — fluid, so a 300px card and a
       980px hero read as the same shape. 2026-08-02 it became a fixed
       multiple of --px, and that is not a regression, it is the point.

       THE OWNER ASKED FOR A PIXELATED THEME, SUBTLY, WITH THE LAYOUT KEPT.
       A pixel grid that scales with the viewport is not a pixel grid: the
       whole idea is one unit that never changes, so a step is a step is a
       step. --px is that unit. Everything the motif draws is an integer
       multiple of it, which is also the only way the staircases stay crisp
       — 4px and 8px are whole device pixels at DPR 1 and at DPR 2, and a
       stepped edge on a fractional boundary is just a blurry diagonal.

       WHAT IT COSTS THE LAYOUT, measured rather than asserted. I wrote
       "nothing moves" here first and it was wrong; a53-layoutdiff.mjs
       records the bounding rect of every element on all five pages at 375
       and 1280, injects the pre-pixel rules back, and diffs:

         every page's total scrollHeight  IDENTICAL, to the pixel
         every element's width and height IDENTICAL
         154 elements shifted VERTICALLY  by 0.63px to 1.65px
         404.html and guide.html          0 elements moved

       All of it is one cause: the .eyebrow mark is 9px tall where the
       triangle was 8px, which grows that one line box, so whatever follows
       an eyebrow sits about a pixel lower. Nothing reflows, nothing
       rewraps, nothing overflows, no page gets longer. Keeping it at 8px
       would put the three steps on 2.67px boundaries, which is a blurred
       diagonal — the exact thing this pass exists to avoid. A pixel of
       baseline for a crisp staircase is the trade, and it is stated here
       rather than glossed. */
    --px: 4px;
    --facet: calc(var(--px) * 4);
    --facet-lg: calc(var(--px) * 8);

    /* The monospace stack was already in this file, on .ph__path. It is now
       a token because it does a second job: the small technical labels.
       Every one of these ships with the OS — ui-monospace resolves to SF
       Mono on Apple, and Consolas is on every Windows since Vista — so this
       costs nothing to fetch, exactly like the body stack below. */
    --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
        "Liberation Mono", monospace;

    /* THE DISPLAY FACE. Added 2026-08-02. The full argument, the licence,
       the provenance and the measured legibility floor are in the DISPLAY
       FACE block below — this is only the stack.

       The fallback behind it is the body stack verbatim, and it is not
       decoration: font-display is `optional`, so on a cold visit that
       loses the 100ms race THIS is what paints the headings, for the whole
       of that page load. It has to be a face somebody would have chosen. */
    --display-face: "Pixelify Sans", -apple-system, BlinkMacSystemFont,
        "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;

    /* One measure for body copy. 34em is about 68 characters, which is
       the width text stops being tiring to read at. */
    --measure: 34em;
    --page: 1120px;
    --gutter: clamp(20px, 5vw, 40px);

    /* Vertical rhythm. A "chapter" gets a lot of air — this number is
       the single biggest reason the page reads as calm. */
    --chapter-y: clamp(72px, 13vh, 168px);

    color-scheme: light dark;
}

@media (prefers-color-scheme: dark) {
    :root {
        --bg: #000000;
        --bg-soft: #0e0e10;
        --ink: #f5f5f7;
        --ink-2: #a1a1a6;
        --hairline: rgba(255, 255, 255, 0.16);
        --hairline-soft: rgba(255, 255, 255, 0.08);

        /* WAS #ff9dc0, AND IT WAS THE LOUDEST THING ON A DARK PAGE.
           Changed 2026-08-02 on the owner's decision, and this is the
           only line in either scheme that moved.

           The defect, measured rather than argued (A51 §3.4 saw it, A53
           §3.5 put numbers on it, and I reproduced both this session in a
           real headless Chrome against the backdrop resolved by walking
           each button's ancestors):

             the filled primary pill --pink-btn #90194c ... 2.43 on #000
                                                            2.23 on #0e0e10
             the plain text link beside it #ff9dc0 ........ 10.84 / 9.95

           The secondary shouted 4.46x louder than the primary. That is
           the hierarchy inverted: the call to action was a dark wine pill
           and the "or send an email" next to it jumped off the page.

           WHY THE PRIMARY WAS NOT TOUCHED INSTEAD. #90194c is the owner's
           choice — the logo's own magenta — and it stays exactly that in
           both schemes. Its dark-mode edge ring #bb2163 stays too; that
           ring is what took the tightest dark figure from 3.09 to 3.22
           and it is holding WCAG 1.4.11 on its own.

           #de6896 IS #b3275d, LIFTED. Not a new colour:

             #b3275d  hsl(336.9, 64.2%, 42.7%)   the light-mode value
             #de6896  hsl(336.9, 64.2%, 64.0%)   this one
                      hue identical, saturation identical, lightness +21.3

           which is the same construction --pink-btn-ring already uses on
           --pink-btn. So --pink-text is now one ink with two lightnesses
           rather than two different pinks, and the dark page gets the
           site's own secondary rather than a neon.

             #de6896 vs #000000 ......... 6.53   was 10.84
             #de6896 vs #0e0e10 ......... 5.99   was  9.95   <- the tighter
             multiple over the pill ..... 2.69x  was  4.46x

           STILL COMFORTABLY LEGIBLE, WHICH IS THE POINT — this is a
           hierarchy fix, not a dimmer switch. 5.99:1 is 33% clear of the
           4.5:1 bar on the smallest thing that wears this token (.eyebrow
           and .ph__label at 12px), and every boundary it draws — .ph's
           dashed border, .todo's ring, .guide .callout's rule and
           :focus-visible's outline — is at 5.99 against a 3:1 bar.

           THE ONE FIGURE THAT GETS WORSE, and it should be said out loud:
           the focus outline is this token, and the primary button's edge
           ring is #bb2163, so dimming this moves the two closer. Outline
           against ring goes 3.09 -> 1.86. They still do not read as one
           object — 3px of page between them, different radii, 2px each,
           and the outline is 6.53:1 against the page it sits on where the
           bar is 3:1 — but a reviewer is entitled to call that the cost.
           Rendered at 4x in a53/a63 shots before believing it.

           REJECTED: dimming further. 4.5:1 on #0e0e10 is the floor for
           12px type, and the pill is 2.23 there, so the best multiple any
           legal value can reach is 2.02x. Buying 0.67x more hierarchy by
           spending the whole legibility margin is not a trade this site
           should make. */
        --pink-text: #de6896;
    }
}

*,
*::before,
*::after {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

html {
    scroll-behavior: smooth;
    -webkit-text-size-adjust: 100%;
}

@media (prefers-reduced-motion: reduce) {
    html {
        scroll-behavior: auto;
    }

    *,
    *::before,
    *::after {
        animation-duration: 0.01ms !important;
        transition-duration: 0.01ms !important;
    }
}

body {
    /* THE BODY STACK, AND IT IS STILL THE SYSTEM STACK. On a Mac or an
       iPhone this is SF Pro, on Windows Segoe, on Android Roboto — every
       one of them already installed, so there is no font file to fetch for
       the text somebody actually reads, and no flash of the wrong typeface
       on a paragraph.

       THIS PARAGRAPH USED TO SAY THERE WAS NO WEBFONT ON THIS SITE. There
       is one now, and leaving the old text here would have made this
       comment a description of a stylesheet that no longer exists — the
       failure this file keeps finding in itself. What was rejected on
       2026-08-01 (fleet/STATUS-A36.md) was a geometric display sans, and
       the reason was that a geometric sans reads ROUNDER than the logo's
       wordmark while costing a font file: it moved away from the brand and
       charged for the privilege. That judgement still stands and the face
       that landed is not that face — see the DISPLAY FACE block below.

       The bar that block had to clear is the one this comment set, and it
       was set deliberately high: not "does it look nice" but "is it worth
       the site's only external-shaped request", with a real fallback and
       no flash. It is answered there with measurements rather than with an
       opinion, and the answer includes a floor size below which the face
       is NOT used — which is why you are reading the system stack here and
       will keep reading it on every paragraph, every link in prose, every
       form label and all the legal copy.

       The third face is --mono, for the small technical labels. Unchanged,
       still zero bytes, and still the part that echoes the engineering
       half of the logo. */
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
        "Helvetica Neue", Arial, sans-serif;
    font-size: 17px;
    line-height: 1.5;
    letter-spacing: -0.01em;
    color: var(--ink);
    background: var(--bg);
    -webkit-font-smoothing: antialiased;
}

img,
svg,
video {
    display: block;
    max-width: 100%;
    height: auto;
}

a {
    color: inherit;
}

/* One global focus ring, so a control added next year is visible to the
   keyboard the moment it exists rather than when somebody remembers. */
:focus-visible {
    outline: 2px solid var(--pink-text);
    outline-offset: 3px;
    border-radius: 4px;
}

.skip-link {
    position: absolute;
    left: -9999px;
    top: 0;
    z-index: 100;
    padding: 12px 18px;
    background: var(--bg);
    border: 1px solid var(--hairline);
    border-radius: 0 0 var(--radius) 0;
}

/* 6px and not 0, and the reason is the focus ring rather than the box.
   :focus-visible above draws `outline: 2px` at `outline-offset: 3px`, and an
   outline paints OUTSIDE the border box — so with the link flush at 0,0 the
   top and left of the ring land 3px outside the VIEWPORT and are clipped
   away. What a keyboard user actually saw was a pink arc down the right-hand
   side and along the bottom, cut flat at the corner: it reads as a rendering
   fault rather than as "you are here", on the first control on every page.

   Nothing a test asserts was wrong — the ring was present, 2px, and the
   right colour in both schemes. It had to be looked at. See
   fleet/STATUS-A51.md §2.1 for the before and after rasters.

   Inset the BOX, not the ring: 6px clears the 3px offset plus the 2px stroke
   with a pixel to spare, and the link still arrives in the top-left corner
   where a skip link belongs. The radius goes to the full --radius because
   the `0 0 var(--radius) 0` above is shaped for a box that meets the corner
   of the window, and this one no longer does. */
.skip-link:focus {
    left: 6px;
    top: 6px;
    border-radius: var(--radius);
}

.sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
}

/* ============================================================
   LAYOUT
   ============================================================ */

.wrap {
    width: 100%;
    max-width: var(--page);
    margin-inline: auto;
    padding-inline: var(--gutter);
}

.wrap--narrow {
    max-width: 760px;
}

/* A chapter is one idea. Headline, at most one sentence, one picture.
   If you find yourself adding a third element, it is two chapters. */
.chapter {
    padding-block: var(--chapter-y);
}

.chapter--soft {
    background: var(--bg-soft);
}

.chapter__head {
    max-width: var(--measure);
    margin-inline: auto;
    text-align: center;
    margin-bottom: clamp(32px, 6vh, 64px);
}

/* ============================================================
   THE DISPLAY FACE — Pixelify Sans, self-hosted. Added 2026-08-02.

   The owner, having seen the 4px staircase this file already draws on the
   picture frames and the eyebrows: "the website is not redesign with
   pixelated font". The staircase was too quiet to read as a decision. So
   the pixel goes into the type, which is the loudest thing on every page.

   PROVENANCE, so nobody has to guess later:
     face      Pixelify Sans, variable, weight axis 400-700
     designer  Stefie Justprince
     copyright Copyright 2021 The Pixelify Sans Project Authors
     licence   SIL Open Font License 1.1 — verbatim at fonts/OFL.txt,
               which is not documentation: OFL §2 requires the notice and
               the licence to travel WITH the font. No Reserved Font Name,
               which is why the family below is the designer's own name
               rather than an alias that would hide where it came from.
     upstream  https://github.com/eifetx/Pixelify-Sans
     source    Google Fonts, css2?family=Pixelify+Sans:wght@400..700,
               the `latin` subset, v3, from fonts.gstatic.com
     obtained  2026-08-02
     file      fonts/pixelify-sans-v3-latin-wght.woff2, 12,016 bytes

   SELF-HOSTED, AND THAT IS NOT A PREFERENCE. This site makes no external
   request at all and the deploy gate follows what the pages reference, so
   a <link> to fonts.googleapis.com would be a new third-party origin on
   every page — and it is also two round trips before a heading paints.
   12 KB from our own origin is one, and it is preloaded.

   WHY THIS FACE AND NOT THE OBVIOUS ONES. Five OFL pixel faces were
   rendered on this actual page rather than on a specimen sheet
   (a72-mock.mjs), and measured (a72-type.mjs, a72-floor.mjs). Two numbers
   decided it:

   1. ADVANCE WIDTH against the system stack, same px size. A pixel face is
      usually much wider per character, and the nav labels are text, so
      width is what breaks a header:

        Pixelify Sans .... 1.02x - 1.11x     <- essentially metric-neutral
        Silkscreen ....... 1.18x - 1.53x
        Silkscreen Bold .. 1.38x - 1.80x
        Press Start 2P ... 1.67x - 2.32x
        VT323 ............ 0.67x - 0.93x

   2. WHETHER IT SURVIVES clamp(). This is the one that actually
      disqualified the favourites. Silkscreen and Press Start 2P are true
      bitmap designs on an em/8 grid: they are perfectly crisp at multiples
      of 8px and greyed at everything else. Measured as the fraction of
      marked pixels that are neither ink nor paper, at DPR 1:

        Silkscreen   8px 0.003 · 16px 0.000 · 24px 0.003 · 32px 0.002
                     13px 0.483 · 17px 0.278 · 21px 0.205 · 27px 0.255

      Every display size on this site is a clamp() — a fluid value that is
      whatever the viewport makes it. So on a real page Silkscreen is a
      different quality of thing at every window width, and it is worse
      than blurry: at 13, 15, 19, 21, 22, 27, 29, 30, 35, 37 and 38 px the
      counters of o, 8, 9 and 0 CLOSE COMPLETELY (a72-floor.mjs). A zero
      that swallows its own hole at some window widths and not others is
      not a typeface, it is a bug with a licence.

      Pixelify Sans is an OUTLINE face drawn as pixels rather than a bitmap
      snapped to them, so it anti-aliases like any other font and its
      counters grow smoothly with size. That is exactly the property this
      site's fluid type needs, and it is why the loudest-looking candidate
      is not the one that shipped.

   THE LEGIBILITY FLOOR IS 21px, AND IT IS MEASURED, NOT A TASTE CALL.
   a72-floor.mjs renders a, o, 8, B, D, 9, 0 and 6 at every size from 8 to
   40 at DPR 1 and DPR 2, floods the background in from the border to find
   the counters, and records the thinnest horizontal run of ink in DEVICE
   pixels. At DPR 1 Pixelify Sans stops drawing whole-device-pixel strokes
   below 21px:

        19px  minRun 2      21px  minRun 2      24px  minRun 2
        20px  minRun 1      22px  minRun 2      32px  minRun 3

   Below 21 the face is still READABLE — that is not the point. Below 21
   its own pixel is smaller than the screen's, so it renders as a blurred
   approximation of a pixel font, which is the worst of both: it stops
   looking deliberate and starts looking like a rendering fault. Looked at
   as well as counted, 8x nearest-neighbour at DPR 1:
   a72-type\a72-wordmark-8x-wordmark.png.

   SO EVERY SURFACE BELOW 21px KEEPS THE FACE IT HAD, and the list of what
   that excludes is the honest half of this change:

     .nav__brand__label  17px  the wordmark. The brief asked for it and
                               the measurement refused it — and raising it
                               to 21px would also widen the brand lockup
                               and move A70's 410px no-script threshold.
     .card__title        17px
     .eyebrow            12px  and it already carries the 4px staircase
     .foot h2            12px
     .ph__label          12px
     .todo               17px
     .nav__links a       14px  navigation, and A70's thresholds live here
     .btn                17px  a control, not display
     every paragraph, lede, caption, table cell and line of legal copy

   There are no numerals in stat-like positions on this site to set — no
   counts, no prices, no measurements on a page. If any are ever added,
   they are display and they belong in the list below, above 21px.

   font-display: OPTIONAL, and this is the whole no-flash-no-shift answer.
   `swap` guarantees a shift: the fallback paints, the face arrives, every
   heading moves. `block` hides the text and then shifts it if the face is
   late. `optional` gives the browser about 100ms and then commits for the
   life of that page load — it NEVER swaps in afterwards, so a heading
   cannot move because of font loading, by construction rather than by
   luck. The cost is real and is stated rather than hidden: on a cold visit
   that loses the race, that page renders in the system stack. The preload
   in each page's <head> and the year-long immutable cache in _headers are
   what make losing it rare; --display-face's fallback is what makes losing
   it survivable.
   ============================================================ */

@font-face {
    font-family: "Pixelify Sans";
    src: url("../fonts/pixelify-sans-v3-latin-wght.woff2") format("woff2");
    /* One file, both ends of the variable axis. .display sets 700 and the
       headings 600, and the browser interpolates rather than synthesising
       a fake bold — a synthesised bold on a pixel face smears every
       module sideways by a fraction of a pixel, which is the one thing
       this whole block exists to avoid. */
    font-weight: 400 700;
    font-style: normal;
    font-display: optional;
    /* Google Fonts' own `latin` range, copied from the @font-face this
       file was cut from. It is stated rather than omitted so that a
       character outside the subset — a Malay diacritic, a currency sign —
       falls straight through to the body stack instead of arriving as a
       missing glyph. Nothing in a display position on this site is outside
       it today; every heading, on all five pages, is plain ASCII. */
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6,
        U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC,
        U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ============================================================
   TYPE
   ============================================================ */

h1,
h2,
h3 {
    font-weight: 600;
    letter-spacing: -0.03em;
    line-height: 1.06;
    text-wrap: balance;
}

/* 700 and -0.045em, not 600 and -0.04em. This is the whole of the "change
   the font" answer and it is deliberately not a new typeface.

   What the logo's wordmark actually is: heavy, tight, and squared off. A
   geometric display face (the obvious reach) is ROUNDER than that, so it
   moves away from the wordmark while costing a font file. Weight and
   tracking move toward it and cost nothing. Only .display changes — the
   headline and subhead stay at 600, because at 32-52px the extra weight
   stops being confident and starts being shouty. */
.display {
    font-family: var(--display-face);
    font-size: clamp(2.6rem, 8vw, 5.5rem);
    font-weight: 700;
    letter-spacing: -0.045em;
}

.headline {
    font-family: var(--display-face);
    font-size: clamp(2rem, 5vw, 3.25rem);
}

/* 22px at its smallest, which clears the 21px floor by one pixel. That is
   the tightest thing on this list and it is deliberately the last one in.
   If .subhead's clamp minimum is ever lowered, this line comes out with
   it. (It has no user in the markup today — grep: zero occurrences across
   all five pages — so it is here as part of the scale, not as something
   currently on screen.) */
.subhead {
    font-family: var(--display-face);
    font-size: clamp(1.375rem, 3vw, 1.75rem);
    font-weight: 600;
    letter-spacing: -0.02em;
}

/* THE TECHNICAL LABEL. The eyebrow, the placeholder's label and the footer
   column headings are all the same thing: a four-to-twenty-character
   uppercase tag that names a block. Monospace, because the other half of
   this brand is a workshop — a mesh, a slicer, a file with dimensions in it
   — and a monospace tag reads as something machine-set rather than
   art-directed. It is also the one type change that costs nothing at all:
   every platform ships a monospace face, so --mono is a zero-byte accent
   where a display webfont would have been 15-25 KB.

   The COLOUR IS UNCHANGED: --pink-text, measured 6.24:1 on white and
   5.73:1 on --bg-soft, and re-measured after this change. Monospace faces
   run narrow, so the size drops 13px -> 12px and the tracking opens
   0.04em -> 0.14em; neither moves the contrast, and neither is a tap
   target. Do not put --pink here — see the token block. */
.eyebrow {
    font-family: var(--mono);
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--pink-text);
    margin-bottom: 14px;
}

/* THE FACET MARK — placement 1 of 3. A single triangle, drawn with
   clip-path rather than set as a "▲" character, for three reasons: it is
   geometry rather than a glyph so it is identical on every platform, a
   font may not have the character at all, and a real triangle can be given
   the exact proportions of a facet in the logo instead of whatever the
   font designer chose. currentColor, so it can never drift away from the
   label's own measured pink. Purely decorative and announced by nothing —
   it is generated content, so a screen reader that reads it at all reads
   an empty string. */
/* PIXELATED, 2026-08-02. This was a 9x8 solid triangle — placement 1 of the
   facet motif, and the only one of the three A51 said read well. It is now
   a 9x9 THREE-STEP STAIRCASE rising left to right: three 3px columns, 3px
   then 6px then 9px tall.

   Why change something that worked: it is the mark a visitor sees most
   often on this site — every chapter head on every page — so it is the
   cheapest place to say "pixel" once and be consistent. A triangle and a
   staircase are the same gesture at different resolutions, which is
   exactly the joke the low-poly logo is already making. Same 9px width,
   same currentColor, no new colour; percentages keep the three steps at
   exactly 3px each.

   THE BOX IS 9px TALL WHERE THE TRIANGLE WAS 8, and that is the one thing
   in this whole pass that moves anything: it grows this line box, so
   content after an eyebrow sits 0.63-1.65px lower. Total page height is
   unchanged on every page. 8px would put the steps on 2.67px boundaries
   and blur them. The measurement is in the --px block above.

   Still `background: currentColor` and still purely decorative — it
   inherits --pink-text from .eyebrow, announced by nothing, and carries no
   information a screen reader loses. */
.eyebrow::before {
    content: "";
    display: inline-block;
    width: 9px;
    height: 9px;
    margin-right: 9px;
    background: currentColor;
    clip-path: polygon(
        0 100%,
        0 66.667%,
        33.333% 66.667%,
        33.333% 33.333%,
        66.667% 33.333%,
        66.667% 0,
        100% 0,
        100% 100%
    );
}

.lede {
    font-size: clamp(1.0625rem, 2vw, 1.3125rem);
    line-height: 1.45;
    color: var(--ink-2);
    max-width: var(--measure);
    margin-top: 18px;
}

.chapter__head .lede {
    margin-inline: auto;
}

.fine {
    font-size: 0.8125rem;
    line-height: 1.5;
    color: var(--ink-2);
}

/* ============================================================
   BUTTONS AND LINKS
   ============================================================ */

.actions {
    display: flex;
    flex-wrap: wrap;
    gap: 14px 28px;
    align-items: center;
    margin-top: 32px;
}

.chapter__head .actions,
.hero .actions {
    justify-content: center;
}

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    min-height: 44px; /* thumb-sized, not decorative */
    padding: 12px 26px;
    border: 0;
    border-radius: 999px;
    /* --pink-btn, not --pink: see the token block. White on --pink is
       2.44:1 against a 4.5:1 bar, and it was the worst thing on the site.
       Do not "restore the brand pink" here — the brand pink is still on
       this button, it is just the one that can be read. */
    background: var(--pink-btn);
    color: #ffffff;
    font: inherit;
    font-weight: 600;
    letter-spacing: -0.01em;
    text-decoration: none;
    transition: background 0.2s ease, transform 0.2s ease;
}

.btn:hover {
    background: var(--pink-btn-hover);
}

.btn:active {
    transform: scale(0.98);
}

/* THE PRIMARY BUTTON'S EDGE, DARK MODE ONLY.

   The problem, measured rather than assumed: --pink-btn #90194c is
   2.43:1 against the #000 page and 2.23:1 against the #0e0e10 of a
   .chapter--soft. WCAG 1.4.11 asks 3:1 of the visual information that
   identifies a control, and for a filled pill with no border that
   information is the fill itself. Under 3:1 the button's own shape stops
   being reliably findable on a black page. Both numbers are in the token
   block above, next to the light-mode ones that all got better.

   THE FIX IS THE EDGE, NOT THE PIGMENT. Lightening the fill to clear the
   bar was measured too — #bb2163 does it — but the pill would then be a
   colour the owner did not choose, in the scheme where the change is
   hardest to notice, and dark mode would silently keep the old #b3275d
   in all but name. So the fill stays #90194c in both schemes and dark
   mode draws a boundary around it.

   This is the same device .todo already uses further down this file: a
   wash that cannot carry the bar on its own, made compliant by a 2px
   box-shadow ring rather than by changing the wash. Same argument, same
   shape of solution.

   #bb2163 is a TINT OF THE SAME HUE, not another colour:
       #90194c  hsl(334.3  70.4%  33.1%)
       #bb2163  hsl(334.3  70.0%  43.1%)
   hue held to 0.0 degrees, saturation to 0.4 points, lightness raised 10.

   Measured on the ring as painted:
       #bb2163 vs #000000 page ........ 3.51:1  (bar 3)
       #bb2163 vs #0e0e10 --bg-soft ... 3.22:1  (bar 3)
   Both with margin. 1px was tried and rejected: it is a hairline that
   disappears at DPR 1, which is the failure a ring is supposed to prevent.
   Brighter rings were tried and rejected too — past about #cf2a70 the rim
   reads as a glow, and worse, as a focus indicator that is always on.

   box-shadow and not border: a border would change the button's box and
   every measured 44px min-height with it. A shadow paints outside the
   border box and takes no layout at all.

   .btn--quiet is excluded on purpose. It is transparent with a
   --hairline border, so its edge is already its own and already flips.

   No :root[data-theme="dark"] twin. This file carries seven such
   selectors for the photo filter and the logo plate and NOTHING IN THE
   SITE EVER SETS data-theme — grep the HTML and site.js. Adding an
   eighth dead rule would make that worse. If a toggle is ever built,
   this rule needs a twin and so do the other seven. */
@media (prefers-color-scheme: dark) {
    .btn:not(.btn--quiet) {
        box-shadow: 0 0 0 2px var(--pink-btn-ring);
    }
}

.btn--quiet {
    background: transparent;
    color: var(--ink);
    border: 1px solid var(--hairline);
}

.btn--quiet:hover {
    background: var(--bg-soft);
}

/* The "Learn more ›" of this site. Deliberately not a button: a page
   with two buttons has one call to action and one distraction. */
.more {
    display: inline-flex;
    align-items: center;
    gap: 5px;
    min-height: 44px;
    color: var(--pink-text);
    font-weight: 500;
    text-decoration: none;
}

.more::after {
    content: "\203A"; /* › */
    font-size: 1.25em;
    line-height: 1;
    transform: translateY(-1px);
    transition: transform 0.2s ease;
}

.more:hover::after {
    transform: translate(3px, -1px);
}

/* ============================================================
   HEADER — a hairline and nothing else.
   ============================================================ */

.nav {
    position: sticky;
    top: 0;
    z-index: 50;
    /* min-height, not height. The bar is allowed to become two rows on a
       narrow screen — see .nav__inner. A fixed height would keep the bar
       48px tall and let the second row hang out of the bottom of it. */
    min-height: var(--nav-h);
    /* Solid first, translucent second. A browser that does not know
       color-mix() drops that line and keeps the opaque bar — without
       the fallback it would drop the background entirely and the nav
       would sit unreadable on top of the page content. */
    background: var(--bg);
    background: color-mix(in srgb, var(--bg) 82%, transparent);
    backdrop-filter: saturate(180%) blur(20px);
    -webkit-backdrop-filter: saturate(180%) blur(20px);
    border-bottom: 1px solid var(--hairline-soft);
}

/* THE WRAP FALLBACK, which is the actual fix for the nav overflow.

   The old bar was one row that could never become two: .nav__links was
   `display:flex; overflow-x:auto` with `white-space:nowrap` links and no
   wrap anywhere. So whenever the links were wider than the space they
   got, the last one was cut mid-word — 29px clipped at 320px on every
   page, and 15.4px past the viewport at 375px the moment a fourth link
   exists. It stopped reproducing at 375px only because the Reviews link
   was removed; the bug was never fixed, the nav just got shorter, and it
   comes back the day anybody renames or adds a link.

   Two levels of give, so no single word can push a link off the screen:
   the menu drops to its own row under the brand, and if the links still
   do not fit on that row they wrap among themselves. Neither is a
   breakpoint — nothing here is keyed to a width, so it cannot be wrong
   for the next set of words. */
.nav__inner {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    /* row gap first: it only exists once the bar is two rows. */
    gap: 6px clamp(16px, 4vw, 34px);
    min-height: var(--nav-h);
    padding-block: 6px;
    max-width: var(--page);
    margin-inline: auto;
    padding-inline: var(--gutter);
}

/* The <nav> landmark added around the list. It takes the auto margin
   that used to sit on the <ul>, because the <ul> is no longer a child of
   the flex row. The declaration on .nav__links stays as well — it is
   inert here, and the one page without the wrapper (reviews.html) was
   deleted on 2026-08-02, so it now keeps nothing aligned. Harmless, and
   left alone rather than removed in the same change that deleted a page:
   one thing at a time on a stylesheet 2,400 lines long. */
.nav__menu {
    margin-left: auto;
}

/* THE BRAND LOCKUP: logo mark + text wordmark, side by side.
   Added 2026-08-01 — the owner pointed out that their logo was nowhere on
   their own website (it existed only as A2's derived favicon monogram and
   inside img/og.jpg, which nobody sees unless they paste a link).

   inline-flex and not block: this is an <a> inside a flex row, and it has
   to size to its contents so .nav__menu's `margin-left:auto` still pushes
   the menu to the far edge. */
.nav__brand {
    display: inline-flex;
    align-items: center;
    gap: 9px;
    font-size: 1.0625rem;
    font-weight: 600;
    letter-spacing: -0.02em;
    text-decoration: none;
    white-space: nowrap;
}

/* The mark is a FIXED box, and both dimensions are stated on purpose.
   The global `img { max-width:100%; height:auto }` near the top of this
   file would otherwise let the intrinsic 100x64 of the file drive layout
   and the bar would jump as the PNG decoded. `flex:none` stops it being
   squeezed when the brand row gets tight at 320px — a squashed logo is
   worse than a wrapped nav.

   50x32 displayed from a 100x64 file: 2x, so it stays sharp on a phone
   without a second asset or a srcset. --nav-h is 48px and .nav__inner
   adds 6px of padding top and bottom, so 32px is the tallest the mark can
   be without driving the bar taller than 48px. Do not raise it without
   re-measuring the bar.

   THE CROP WAS REDONE 2026-08-02 AND THE BOX DELIBERATELY DID NOT MOVE.
   The previous logo-mark.png cut the B3D block on three sides at once,
   which is why it read as "B3)": measured on the file, the bottom row of
   the old 100x64 was 63% painted and the right-hand column 66% painted —
   coverage that high at the frame is a letterform running off the edge,
   not a finished edge. The D's bottom-right stroke was the casualty. The
   top row was 20% painted, which was the wireframe cut mid-object that
   read as a compression artifact at 50px.

   The new crop is source logo-full.png x[-3..494] y[290..608]. y=290 is
   the last row before the letters' wireframe cage begins (rows 291-294
   are its top band, so cutting any lower severs it again) and y=608 sits
   in the empty band between the letters and the "Babah3D" wordmark, which
   is unreadable at this size and stays excluded. 3px of transparent
   padding either side makes the source region 497x318, which is 1.563 —
   the 100x64 box's own aspect ratio to within 0.03%. THAT is why the
   50x32 display box, the 48px bar and every measurement above are
   unchanged. Verified: .nav is 49px (48px inner + hairline) at 1280 and
   375, and 77px at 320, exactly as before.

   Same file, new frame: on the new mark the top row is 3% painted, the
   bottom row 2%, and the right-hand painted column is 97 of 100. Nothing
   is cut. The 3% at the top is the tip of the pen's orange filament,
   which is drawn attached to the cage's apex and cannot be excluded
   without cutting the cage. Erasing it would be redrawing the owner's
   artwork, which is the thing the block below refuses to do. */
.nav__brand img {
    width: 50px;
    height: 32px;
    flex: none;
}

/* THE WORDMARK GOES WHEN IT STOPS FITTING — NOT AT A WIDTH.

   THIS WAS `@media (max-width: 420px)` UNTIL 2026-08-02, AND 420 HAD GONE
   STALE. It was an honest number when it was written: with the wordmark
   the brand measured 126px, the menu 226px and the gap 16px — 368px of
   content, needing about a 408px viewport — so the text was dropped just
   below that and the bar was 48px again.

   Then the menu became four items. Measured this session in a real
   headless Chrome across a 1px sweep (a63-nav.mjs, a63-nav-before-fine.json),
   index.html, .nav height at each viewport width:

       <= 381   two rows, 76.5px   wordmark already hidden, still no room
       382-420  ONE ROW,    49px   the breakpoint doing its job
       421-466  two rows, 76.5px   <-- the wordmark comes back at 421 and
                                       does not fit again until 467
       >= 467   ONE ROW,    49px

   421-466 is not a rounding error, it is 46px of phone: 428, 430 and 440
   are all in it, and 430 is an iPhone Pro Max. The bar there was two rows
   with the wordmark showing, which is the worst of both — the tall bar
   AND the long brand. Nobody had measured above 420 since the guide link
   landed, because the number looked like it had been chosen carefully.
   It had. It was chosen carefully for a menu that no longer exists.

   SO THE NUMBER IS GONE AND site.js MEASURES INSTEAD. It hides the
   wordmark only when hiding it actually buys a single row, and shows it
   again when it does not — which is why the two-row bar below the wrap
   point carries the whole lockup instead of a lone 50px mark on an empty
   first row (A51 §3.6 called that out and it was right). Same argument as
   .nav__links.is-scrollable further down, same reason: a width is a guess
   about words, and the words change.

   THEN THE MENU BECAME THREE ITEMS AGAIN — WITHIN THE DAY — WHICH IS THE
   ARGUMENT ABOVE HAPPENING TWICE. The owner asked for Guide off the top
   bar on 2026-08-02; the nav comment on index.html has the reasoning and
   where the guide went. Re-measured the same way, same day, same browser
   (a70-nav.mjs, 1px sweep 316-500, index.html):

       <= 331   two rows, 76.5px   wordmark shown, still no room
       332-409  ONE ROW,    49px   wordmark hidden
       >= 410   ONE ROW,    49px   wordmark shown

   375 CAME BACK. It was two rows from the day the guide link landed until
   the day it left — deficit 6.23px, measured by A63, which A53 had
   already declined to close with a 3px link-gap tweak because one
   browser's text metrics differ by more than 3px on a 117px string. That
   refusal was right and it is why 375 waited for this. Nothing in this
   file changed to win it: a 35.64px label and its 14px gap left the menu,
   the one-row threshold moved 382 -> 332, and 375 now clears by 43.41px.
   The wordmark also returns 57px earlier, 467 -> 410, so a 430px iPhone
   Pro Max gets one row AND the full lockup.

   AND HERE IS THE TRAP. DO NOT WALK INTO IT. The menu is 225.59px wide
   now, within half a pixel of the 226px that made 420 an honest number in
   the first place — so `@media (max-width: 420px)` would appear to work
   again if somebody put it back. It would be right for exactly as long as
   these three labels never change, and this menu has now changed twice in
   two days, each time silently invalidating a width somebody had reasoned
   about carefully. That is the whole failure, and it does not become a
   different failure because the number happens to fit again today.

   HOW IT IS HIDDEN IS UNCHANGED AND IS STILL THE WHOLE POINT. These are
   the .sr-only declarations. display:none would be one line shorter and
   would BREAK THE LINK: the <img> carries alt="", so the visible text is
   the only thing giving the link an accessible name, and display:none
   takes it out of the accessibility tree. Clipped text stays in it.
   Verified at 375px — the link's computed accessible name is still
   "Babah3D". See fleet/STATUS-A22.md.

   The label is position:absolute, so it stops being a flex item and the
   9px gap above collapses on its own. No gap override needed.

   WITH JAVASCRIPT OFF the class is never set, the wordmark is always
   shown, and the bar is two rows below 410px. That figure is measured
   with script execution actually disabled in Chrome rather than derived
   from the gap arithmetic (a70-jsoff.mjs) — colGap is a clamp() and so
   depends on the width you are solving for, and the derived answer came
   out a pixel low. It was 467px while the guide link was in the menu, so
   the no-script fallback improved by 57px as well. That is the designed
   fallback — 0 elements overflow, every target stays 24px — and it is
   the same trade .nav__links.is-scrollable already makes. */
.nav__inner.is-compact .nav__brand__label {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
}

.nav__links {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: 6px clamp(14px, 3vw, 30px);
    margin-left: auto;
    list-style: none;
    /* The sideways scroll is kept as the last resort — for one link so
       long that even a row of its own cannot hold it. It is no longer
       the only thing standing between a narrow screen and a clipped
       word; flex-wrap above is. */
    overflow-x: auto;
    scrollbar-width: none;
    -webkit-overflow-scrolling: touch;
}

.nav__links::-webkit-scrollbar {
    display: none;
}

/* 24px is WCAG 2.5.8's floor for a target, and a 14px link is 19px tall
   — measured, on all four pages, for all three of these and all eight in
   the footer. inline-flex rather than padding so the extra height is
   centred on the text and the row does not shift. */
.nav__links a {
    display: inline-flex;
    align-items: center;
    min-height: 24px;
    font-size: 0.875rem;
    color: var(--ink-2);
    text-decoration: none;
    white-space: nowrap;
    transition: color 0.15s ease;
}

.nav__links a:hover,
.nav__links a[aria-current="page"] {
    color: var(--ink);
}

/* The sideways scroll above is the design, but nothing on screen said so.
   A3 and A5 both measured the same thing: the last link was cut off flat
   at the edge, which reads as a rendering fault rather than as a list you
   can push. A soft right edge says "there is more this way".

   KEPT ON PURPOSE after the wrap fallback landed above, and it now
   almost never fires: once the links can wrap there is usually nothing
   left to scroll, so site.js measures no overflow and this class is not
   set. It is the affordance for the one case wrap cannot rescue — a
   single link too long for a row of its own. A fade is not a layout fix
   and was never sold as one; the layout fix is .nav__inner.

   Keyed off a class, not a width. The obvious version of this is a media
   query — and it is wrong. When the reviews link was removed the nav went
   from four items to three and stopped overflowing at 375px altogether
   (measured: scrollWidth 226 === clientWidth 226), while still
   overflowing at 320 (226 vs 197). Any breakpoint I picked would be a
   magic number derived from the current four words in the nav, and would
   silently start fading un-clipped text the next time somebody renames a
   link. site.js measures the actual overflow instead and toggles this
   class; with JavaScript off there is no fade, which is exactly the
   behaviour this site had before and is not broken.

   @supports because a browser without mask-image should get no mask
   rather than a broken one. */
@supports (mask-image: linear-gradient(#000, #000)) {
    .nav__links.is-scrollable {
        -webkit-mask-image: linear-gradient(to left, transparent 0, #000 20px);
        mask-image: linear-gradient(to left, transparent 0, #000 20px);
    }
}

/* ============================================================
   HERO — one picture, one line. Nothing else may be added here.
   ============================================================ */

.hero {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    text-align: center;
    /* svh, not vh: on a phone, vh is the height WITHOUT the browser's
       address bar, so a 100vh hero is taller than the screen and the
       first thing a visitor does is scroll past the top of it. The vh
       line is the fallback for anything that does not know svh. */
    min-height: min(84vh, 760px);
    min-height: min(84svh, 760px);
    padding: clamp(56px, 10vh, 110px) var(--gutter) clamp(40px, 8vh, 80px);
}

.hero__media {
    width: min(100%, 980px);
    margin-top: clamp(40px, 7vh, 80px);
}

/* ============================================================
   MEDIA AND ITS PLACEHOLDER

   Every picture slot on this site is a .media. Until a real photograph
   is dropped in it holds a .ph, which states the exact file it wants
   and the size it wants it at, and looks unmistakably unfinished.

   That is the point. A placeholder that looks like a design decision
   gets published. This one cannot be mistaken for finished work, and
   it tells whoever fills it precisely what to put where.
   ============================================================ */

.media {
    border-radius: var(--radius);
    overflow: hidden;
    background: var(--bg-soft);
}

/* THE FACET — placement 2 of 3, and the only one that is on every page.

   A picture frame loses two OPPOSITE corners to a chamfer instead of
   rounding them. That is the low-poly idea applied to the one element this
   whole site is built around, and it is deliberately applied to the FRAME
   and never to the photograph: nothing is laid over the product, nothing is
   tinted, nothing competes. The rule at the top of this file survives — the
   picture still does the talking, it is just cut rather than rounded.

   Two corners and not four. Four chamfers is a bevelled plate and reads as
   1998; two opposite ones read as a facet, because that is what a facet is
   — one plane meeting another at an angle, not a uniform edge treatment.

   :has(img) is what keeps this off the placeholders. A .ph carries its own
   2px dashed boundary, and clip-path CUTS rather than draws, so chamfering
   a .media that holds one would take two bites out of a border whose 5.73:1
   contrast is a measured accessibility item. So the chamfer only applies to
   frames that actually hold a photograph. A browser without :has() gets the
   rounded corner it has always had, which is a fine thing to fall back to. */
/* PIXELATED, 2026-08-02. The corner was a single 45-degree chamfer. It is
   now a FOUR-STEP STAIRCASE of --px, which is the owner's pixel theme and
   also the fix for the thing A51 measured: in light mode the chamfer did
   not exist on screen. Every product photograph is shot on white and the
   page is white, so a diagonal cut through white against white removes
   nothing you can see. In dark mode it was unmistakable. A brand pass that
   only exists at night is not a brand pass.

   Two things make it visible now. The staircase reads as a made shape
   rather than as a corner that happens to be missing — a diagonal is what
   a photo edge looks like when it is badly cropped, a 4px staircase is
   obviously deliberate. And the inset ring below traces it.

   THE CUT MOVED FROM THE FRAME TO THE PHOTOGRAPH, and that is the half
   that actually solves light mode. Clipping .media removed the frame's
   background along with the corner, so on a white page a white photo lost
   a corner to nothing and there was nothing to see. Now .media KEEPS its
   whole box and carries a --ink-2 ground, and the staircase is cut out of
   the <img> — so each notch shows the ground through it as a stepped grey
   wedge. Visible on white. Visible on black. Same shape in both.

   An inset box-shadow was tried first and it does not work: an inset
   shadow paints BELOW the element's content, and the <img> covers the
   whole box, so it was hidden under the photograph on every card. It
   looked exactly like a working fix in the source and rendered nothing.

   --ink-2 and not --hairline: this is a filled wedge, not a hairline, and
   a hairline-alpha wedge is invisible against white for the same reason
   the chamfer was. --ink-2 flips with the scheme (#6e6e73 / #a1a1a6), so
   the wedge is dark on the light page and light on the dark one. It is
   also, incidentally, the "dark mat" A51 offered as option 2 for the
   dark-mode photo glare: the seam between page and photograph is now on a
   frame element rather than on the product. */
/* THE STAIRCASE IS GONE, 2026-08-02, owner's call: "rather than in border
   like that". Everything above is the argument for a notched frame, kept
   because it records why the notch existed and what it cost — a chamfer
   invisible in light mode, then a staircase built to fix that, then a
   --ink-2 ground added so the notches had something to show through.
   Three rounds of work to make a frame visible. The owner looked at it and
   wanted the picture instead, which was the rule at the top of this file all
   along: the picture does the talking.

   So the frame keeps its rounded corner and its soft ground, the --ink-2
   wedge goes, and the reveal moves from the EDGE to the SURFACE — see the
   blur block below. */
.media:has(img) {
    border-radius: var(--radius);
    background: var(--bg-soft);
}

/* NO `display: block` here, deliberately. It looks like tidiness and it is a
   layout change: it removes the inline strut's descender space under every
   photograph and each page gets 20-46px shorter. Measured — a53-layoutdiff.mjs.
   The owner's constraint was that the layout stays, so the img keeps the
   display it has always had and clip-path applies to a replaced inline
   element perfectly well. */
/* ============================================================
   THE REVEAL — every picture arrives soft and resolves under the pointer.

   Owner's brief, 2026-08-02: "rather than in border like that ... become
   blurred ... and then when mouse going into it become real picture no
   border with smooth animation transition ... apply to all other".

   WHAT THIS REPLACED. A four-step clip-path staircase cut notches out of
   every photograph's corners, doubling them on hover, over a --ink-2
   ground added so the notches had something to show through. Three rounds
   of work went into making that frame visible — a chamfer that did not
   exist in light mode, then the staircase, then the ground. It is all
   gone. The corner is --radius again and nothing is cut out of anybody's
   product photograph. The rule at the top of this file is the reason: the
   picture does the talking.

   WHY BLUR AND NOT A REAL PIXELATION. CSS cannot pixelate a
   high-resolution image on its own — `image-rendering: pixelated` only
   bites when a SMALL source is scaled up, so a genuine block effect needs
   a second, tiny file per picture. The hero has one and still uses it
   (hero-q4.webp, four inks, the app's own quantizer — see the block
   below). The other fourteen pictures do not, and generating them would
   mean a build step this site deliberately does not have. Blur is the
   honest approximation: it reads as "not resolved yet" without claiming
   to be a print simulation, which the quantize block below is equally
   careful never to claim.

   scale(1.04) IS NOT DECORATION. A blur samples past the element's own
   edge, so a blurred image at natural size shows a soft translucent
   fringe all round — the exact "border" the owner asked to be rid of.
   The overscale pushes that fringe outside the frame, where `overflow:
   hidden` clips it.

   @media (hover: hover) WRAPS THE WHOLE THING and it is the load-bearing
   line. A phone has no pointer to move into the picture, so a
   hover-gated reveal would leave every photograph permanently blurred
   for every mobile visitor — on a site whose entire job is showing
   printed work. Touch gets the photograph, sharp, immediately.

   :focus-within resolves it as well, so a keyboard user tabbing through
   the cards sees what a mouse user sees.
   ============================================================ */
@media (hover: hover) and (pointer: fine) {
    .media img:not(.quant) {
        filter: blur(12px) saturate(1.12);
        transform: scale(1.04);
        transition: filter 0.55s ease, transform 0.55s ease;
    }

    .media:hover img:not(.quant),
    .media:focus-within img:not(.quant),
    .card:hover .media img:not(.quant),
    .card:focus-within .media img:not(.quant) {
        filter: none;
        transform: none;
    }
}

/* A picture you must hover to read is a barrier, not an effect. The
   global reduced-motion rule crushes the transition to 0.01ms, which
   would leave the blur snapping on and off instead of removing it — so
   this states the resting state outright and the photograph is simply
   the photograph. */
@media (prefers-reduced-motion: reduce) {
    .media img:not(.quant) {
        filter: none;
        transform: none;
    }
}

.media img {
    width: 100%;
    object-fit: cover;
}

/* ============================================================
   THE QUANTIZE REVEAL — the hero arrives as the four inks it would be
   printed from, and resolves into the photograph.

   WHY THIS IS NOT A FILTER, WHICH IS THE ONLY REASON IT IS HERE.
   The product this shop sells turns a picture into DISCRETE BLOCKS OF
   PHYSICAL COLOUR: the app's quantizer reduces an image to a handful of
   inks and its geometry builds each one as a real inlay tile, because a
   P1S with an AMS has FOUR filament slots and a slicer prints per-face
   colour. A preview texture does not print. So a pixel, a colour tile and
   a printed layer line are the same object on this site, and the corner
   staircase above was already that shape — this makes it mean something.

   FOUR COLOURS BECAUSE THE AMS HAS FOUR SLOTS. Not three because it is
   tidier, not six because it looks better.

   HOW THE FILE WAS MADE, because "we ran a posterize filter" would be a
   lie and this is the difference. img/home/hero-q4.webp is the output of
   tools/clicker-wonderland/src/quantize.js — THE APP'S OWN QUANTIZER, run
   offline over the real photograph, not reimplemented. k-means++ seeding,
   Lloyd iterations, the blend-snap along centroid pairs, the merge, the
   speck drop, the mode filter, the island absorption: all of it. The
   generator is a63-quantize.mjs; the site imports nothing from that repo
   and never will, which is why the OUTPUT ships and the code does not.

   It returned exactly four clusters and they tile the picture with no
   pinholes (1,178,672 of 1,178,672 px):
       #fbfbf9  55.0%   #edb70b  20.7%   #464a4b  19.6%   #090a09  4.7%

   WHAT THIS MAY NEVER BE ALLOWED TO SAY. It is a quantization of a
   PHOTOGRAPH, not of the design that produced the object, so nothing on
   this page may frame it as a simulation of how that item printed or of
   how a customer's job will come out. There is no caption and there must
   not be one. The gallery on work.html is real customers' real jobs and
   this effect is deliberately NOT on it — see fleet/STATUS-A63.md §5.

   steps(4, end) AND NOT A SMOOTH FADE. Four inks, four steps: the layer
   goes 1 -> 0.75 -> 0.5 -> 0.25 -> 0 and each step is a visible hold.
   A Hollywood cross-fade would say "photo effect"; a stepped one says
   "this was made out of a countable number of things", which is the
   point being made and is also the same idea as --px.
   ============================================================ */

/* Scoped by :has(> .quant) so exactly the frames that carry a quantize
   layer become containing blocks. No other .media on the site is touched,
   and position:relative changes stacking, never layout. */
.media:has(> .quant) {
    position: relative;
}

/* inset:0 over the frame, so it contributes NOTHING to layout — the hero's
   height is the photograph's, before and after. The staircase clip from
   .media:has(img) > img applies to this element too, for free, because it
   is an img child of the same frame: the notches stay cut through both
   layers instead of the overlay squaring them off mid-reveal. */
.media > .quant {
    position: absolute;
    inset: 0;
    height: 100%;
    pointer-events: none;
    animation: quant-resolve 1.4s steps(4, end) 0.12s forwards;
}

@keyframes quant-resolve {
    from {
        opacity: 1;
    }

    to {
        opacity: 0;
    }
}

/* NONE, not shortened. The global reduced-motion rule near the top of this
   file would already crush the duration to 0.01ms, but "a one-frame flash
   of blocks" is still an animation happening to somebody who asked for
   none. display:none removes the element outright and the photograph is
   simply the photograph.

   It does NOT stop the 16 KB being fetched — a display:none <img> still
   loads in Chrome. Avoiding that needs either JavaScript or a CSS
   background, and a CSS background is discovered later than a preload-
   scanned <img>, which trades a small cost for reduced-motion users
   against a visible late flash for everybody. Stated rather than hidden. */
@media (prefers-reduced-motion: reduce) {
    .media > .quant {
        display: none;
    }
}

.ph {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 10px;
    padding: 24px;
    text-align: center;
    /* --pink-text: a 2px dashed border is a boundary, so it is held to
       WCAG 1.4.11's 3:1 and --pink is 2.44:1 on white. Same pigment
       problem as .todo, same fix. */
    border: 2px dashed var(--pink-text);
    border-radius: var(--radius);
    background: repeating-linear-gradient(
        -45deg,
        transparent,
        transparent 12px,
        var(--hairline-soft) 12px,
        var(--hairline-soft) 24px
    );
    color: var(--ink-2);
}

.ph--wide {
    aspect-ratio: 16 / 9;
}

.ph--square {
    aspect-ratio: 1 / 1;
}

.ph--portrait {
    aspect-ratio: 4 / 5;
}

/* --mono, same as .eyebrow: this is the same kind of thing, a tag naming a
   block. Colour unchanged — --pink-text, 5.73:1, which is the .ph border's
   measured figure too. */
.ph__label {
    font-family: var(--mono);
    font-size: 0.75rem;
    font-weight: 700;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--pink-text);
}

/* This was the site's first monospace and it is where --mono came from. */
.ph__path {
    font-family: var(--mono);
    font-size: 0.8125rem;
    word-break: break-all;
    color: var(--ink);
}

.ph__note {
    font-size: 0.75rem;
    max-width: 34ch;
}

/* Marks any text or link that is still a placeholder. Anything wearing
   this class is a thing the owner has to fill in before launch, and it
   is loud on purpose — an accidental deploy should look broken rather
   than look fine while pointing at nothing. Delete the class when you
   fill the value; `grep -rn "todo" site/` then finds what is left. */
.todo {
    /* The wash is the one place raw --pink is still correct: it is a
       tint behind type, not a boundary and not ink. Literal rgba first
       so a browser without color-mix() keeps the wash rather than losing
       it — same two-declaration pattern as .nav above. */
    background: rgba(255, 122, 168, 0.16);
    background: color-mix(in srgb, var(--pink) 16%, transparent);

    /* --pink-text, not --pink. This outline IS the device: it is the
       only thing that says "this value is not real yet". At --pink it
       measured 2.24:1 against --bg-soft, under the 3:1 at which a
       boundary is reliably noticed — so the safety mechanism was
       quietly not working. --pink-text is 5.73:1 on --bg-soft and
       6.24:1 on white. */
    box-shadow: 0 0 0 2px var(--pink-text);

    /* The token text itself was inheriting --ink-2, which is 4.08:1 on
       the wash — under the 4.5:1 bar. --ink is 13.55:1 in light mode and
       15.94:1 in dark, and it flips with the scheme, so it is right in
       both. */
    color: var(--ink);
    border-radius: 4px;
    padding: 0 4px;

    /* A placeholder token is one unbroken 28-character run of capitals and
       underscores. Dropped into an h2 at 52px it cannot break, so it sets the
       minimum width of the whole page: A5 measured scrollWidth 1287 in a
       1280 window on services.html, and the entire site scrolled sideways
       — caused by the very device that is supposed to make unfinished work
       obvious. `anywhere` rather than `break-word` because only `anywhere`
       also shrinks the min-content size, which is the half that stops the
       overflow rather than just hyphenating after the damage is done.
       Global rather than scoped to headings: the featured quote is 48px in
       a 22ch box and would be next. */
    overflow-wrap: anywhere;
}

.todo::after {
    content: " ⚠ TODO";
    font-size: 0.7em;
    font-weight: 700;
    letter-spacing: 0.06em;
    color: var(--pink-text);
    white-space: nowrap;
}

/* ============================================================
   CARD GRID — past work, services
   ============================================================ */

.grid {
    display: grid;
    gap: clamp(20px, 3vw, 32px);
    grid-template-columns: repeat(auto-fill, minmax(min(100%, 300px), 1fr));
}

.card {
    display: flex;
    flex-direction: column;
    gap: 14px;
}

.card__body {
    padding-inline: 2px;
}

.card__title {
    font-size: 1.0625rem;
    font-weight: 600;
    letter-spacing: -0.02em;
}

.card__meta {
    font-size: 0.875rem;
    color: var(--ink-2);
    margin-top: 3px;
}

/* ============================================================
   QUOTES

   A review is one voice at a time. The featured one is set large
   because a wall of small testimonials is read as decoration and a
   single large sentence is read as a person.
   ============================================================ */

/* 28-48px, well clear of the floor, and it is a display setting by its own
   argument above. Its only consumer was reviews.html, which was DELETED on
   2026-08-02 — so this rule and .quote-card__translation below are now dead
   code, kept deliberately rather than by oversight: the reviews themselves
   survive in ../assets/reviews/ with their permission notes, and the page is
   one `git show 53cf78a:reviews.html` away. Deleting the styles too would
   turn a one-command restore into a redesign. If the reviews are still gone
   in a few months, these go. */
.quote {
    font-family: var(--display-face);
    max-width: 22ch;
    margin-inline: auto;
    text-align: center;
    font-size: clamp(1.75rem, 4.5vw, 3rem);
    font-weight: 600;
    letter-spacing: -0.03em;
    line-height: 1.14;
    text-wrap: balance;
}

.quote__by {
    display: block;
    margin-top: 26px;
    font-size: 0.9375rem;
    font-weight: 400;
    letter-spacing: 0;
    color: var(--ink-2);
}

.quote-card {
    display: flex;
    flex-direction: column;
    padding: clamp(22px, 3vw, 32px);
    border: 1px solid var(--hairline-soft);
    border-radius: var(--radius);
    background: var(--bg);
}

.quote-card p {
    font-size: 1.0625rem;
    line-height: 1.45;
}

.quote-card footer {
    margin-top: auto;
    padding-top: 20px;
    font-size: 0.875rem;
    color: var(--ink-2);
}

/* The English line under a Malay review. A translation is a footnote, not
   part of what the customer said, and without this it carries the same
   weight as their own words — which reads as if we wrote both.

   This styled reviews.html, which the owner unlinked on 2026-08-01 and
   which was DELETED on 2026-08-02. Dead code, kept on purpose — see the
   .quote comment above for the reasoning and the restore command. */
.quote-card__translation {
    margin-top: 10px;
    font-size: 0.875rem;
    line-height: 1.45;
    color: var(--ink-2);
}

/* ============================================================
   WHO WE ARE — the logo used large, on services.html, once.

   The owner: "the logo of babah 3d have not been used to full potential,
   make one bigger one somewhere, maybe at the service explaining who we
   are." Before this it existed at 50 x 32 in the header and inside
   img/og.jpg, and nowhere else.

   ONE PLACEMENT AND ONLY ONE. A logo repeated down a page stops being a
   mark and becomes wallpaper, and this design has exactly one home for it:
   the block that says who the business is. Everywhere else the header mark
   is enough.

   THE MESH BEHIND IT IS THE ARGUMENT OF THE WHOLE BRAND PASS. The logo is
   not pixelated, it is a triangulated surface — and a triangulated surface
   is precisely what an STL file is. The mark is a mesh; so is every object
   this shop prints. So the mark sits ON a mesh, generated as inline SVG in
   services.html rather than shipped as a picture: it stays crisp at any
   size, costs one path and nine polygons, and adds no request to a page
   that currently makes two.
   ============================================================ */

.who {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: clamp(28px, 5vw, 64px);
}

/* No breakpoint here, on purpose, like the nav. min() against a viewport
   unit means the mark shrinks with the screen and the row wraps when the
   two halves stop fitting, whatever the words underneath turn out to be. */
.who__mark {
    position: relative;
    flex: 0 0 auto;
    width: min(280px, 58vw);
}

/* <picture> is inline by default and would sit on a text baseline, leaving
   a few pixels of descender gap under the mark. */
.who__mark picture {
    display: block;
}

/* position:relative is what puts the mark ABOVE the mesh — z-index does
   nothing to a static element. */
.who__mark img {
    position: relative;
    z-index: 1;
    width: 100%;
    height: auto;
}

/* max-width:none is REQUIRED, not tidiness: the global
   `img, svg, video { max-width: 100% }` near the top of this file would
   otherwise clamp the mesh back to the mark's own width and the bleed would
   silently disappear.

   EXPLICIT width AND height, not `inset` with auto sizing. That was the
   first version and it is a trap: an <svg> with a viewBox is a REPLACED
   ELEMENT with an intrinsic ratio, so with width and height auto the
   browser sizes it from that ratio instead of from the insets — measured
   462.9px tall where the insets asked for 439.2, and the mesh drifted off
   centre. Percentages here are of .who__mark's padding box, which is
   definite for an absolutely positioned child even though it is
   content-derived. preserveAspectRatio="slice" in the markup then crops the
   small ratio difference rather than distorting the triangles.

   The 12% horizontal bleed is bounded on purpose. --gutter is at least 20px
   and the widest the mesh can ever overhang is 12% of 280px = 33.6px, so at
   the one width where .who fills its row exactly the overhang lands inside
   the gutter and never reaches the viewport edge — measured at 768px, where
   it is the tightest: 4.8px of clearance. Re-measure all 16 page/width
   combinations if you raise it. */
.who__mesh {
    position: absolute;
    left: -12%;
    top: -8%;
    width: 124%;
    height: 116%;
    max-width: none;
    z-index: 0;
    pointer-events: none;
    color: var(--ink);
}

.who__body {
    flex: 1 1 340px;
    min-width: 0;
    max-width: var(--measure);
}

/* ============================================================
   CONTACT AND FOOTER
   ============================================================ */

.contact-row {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 14px;
    margin-top: 34px;
}

.contact-link {
    display: inline-flex;
    align-items: center;
    gap: 10px;
    min-height: 48px;
    padding: 12px 22px;
    border: 1px solid var(--hairline);
    border-radius: 999px;
    text-decoration: none;
    font-weight: 500;
    transition: background 0.15s ease, border-color 0.15s ease;
}

.contact-link:hover {
    background: var(--bg-soft);
    border-color: var(--ink-2);
}

/* The icons are drawn inline in the markup as plain strokes rather than
   pulled from an icon font or a CDN — one less request, and nothing to
   break when a third party moves a file. */
.contact-link svg {
    width: 20px;
    height: 20px;
    flex: none;
}

.foot {
    padding-block: clamp(40px, 7vh, 72px);
    border-top: 1px solid var(--hairline-soft);
    background: var(--bg-soft);
}

.foot__grid {
    display: flex;
    flex-wrap: wrap;
    gap: 28px 56px;
    justify-content: space-between;
}

/* --mono, third and last of the technical labels. Colour unchanged. These
   are headings, not targets, so the 24px floor below does not apply to
   them — it applies to the <a>s underneath. */
.foot h2 {
    font-family: var(--mono);
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.12em;
    text-transform: uppercase;
    color: var(--ink-2);
    margin-bottom: 12px;
}

.foot ul {
    list-style: none;
    display: flex;
    flex-direction: column;
    gap: 9px;
}

/* Same 24px floor as the nav links — see the note there. */
.foot a {
    display: inline-flex;
    align-items: center;
    min-height: 24px;
    font-size: 0.875rem;
    color: var(--ink-2);
    text-decoration: none;
}

.foot a:hover {
    color: var(--ink);
    text-decoration: underline;
}

.foot__legal {
    margin-top: 34px;
    padding-top: 22px;
    border-top: 1px solid var(--hairline-soft);
    font-size: 0.75rem;
    color: var(--ink-2);
}

/* ============================================================
   SCROLL REVEAL

   Deliberately opt-in from JavaScript: site.js adds .reveal-on to
   <html> before anything paints, and only then does the hidden state
   below apply. With JavaScript off or broken, every element is simply
   visible. A reveal animation that can leave the page blank is a bug
   dressed as a flourish.
   ============================================================ */

.reveal-on .reveal {
    opacity: 0;
    transform: translateY(22px);
    transition: opacity 0.7s ease, transform 0.7s ease;
}

.reveal-on .reveal.is-in {
    opacity: 1;
    transform: none;
}

@media (prefers-reduced-motion: reduce) {
    .reveal-on .reveal {
        opacity: 1;
        transform: none;
    }
}

/* ---------------------------------------------------------------------
   PHOTOGRAPHY IN DARK MODE
   ---------------------------------------------------------------------
   Every colour on this site is a token and every token has a dark value.
   The product photographs are the one thing that cannot flip: they are
   shot on white, and white stays white.

   Measured (A13, 2026-08-01): the hero's backdrop reads 20.81:1 against
   the dark page — BRIGHTER THAN THE PAGE'S OWN BRIGHTEST INK at 19.29:1.
   On a phone at night the hero is a light box.

   .92 is the largest reduction that is invisible on the product itself
   side-by-side and still lands the backdrop at 17.29:1, under the text.
   Do NOT push it lower to fix it harder — past about .85 the prints look
   grey and dull, which is a worse lie about the product than a bright
   background is. The real fix is upstream: shoot on mid-grey, then delete
   this whole block.

   This block lives at the end of the file so that a top-level at-rule is
   unambiguously top-level. Verified rather than assumed: brace depth at
   the `@media` below is 0, and the file balances at 91 open / 91 close. */

@media (prefers-color-scheme: dark) {
    .media img,
    .hero img {
        filter: brightness(.92);
    }
}

:root[data-theme="dark"] .media img,
:root[data-theme="dark"] .hero img {
    filter: brightness(.92);
}

:root[data-theme="light"] .media img,
:root[data-theme="light"] .hero img {
    filter: none;
}

/* ============================================================
   THE LOGO'S BACKING PLATE, DARK MODE ONLY

   Measured problem: the mark is the owner's real low-poly artwork and it
   has genuinely dark facets — deep purple and maroon. Composited at its
   display size against the dark bar (which is black: --bg is #000 and the
   nav sits at 82% of it), a large share of the mark's painted pixels fall
   below 3:1. Half the logo sinks into the bar.

   THE FIX IS THE GROUND, NOT THE MARK. Recolouring, lightening or
   re-drawing the artwork would make the site show a logo the owner did
   not design — and the same dark facets are exactly right in light mode.
   So the artwork is untouched at every width and in both schemes, and it
   simply gets something to sit on.

   Re-measured 2026-08-02 after the mark was RE-CROPPED (see the note on
   logo-mark.png below). Method: composite at the 50x32 display size, count
   a pixel as painted at alpha >= 0.5, WCAG relative luminance:

                          old crop        new crop
     painted pixels       849             794
     below 3:1 on #000    53.0%           56.9%
     below 3:1 on plate   25.4%           21.2%
     below 3:1 on #fff    22.9%           18.5%

   The old-crop 53.0% reproduces the 52.7% this comment used to carry, so
   the method matches the one that argued for the plate.

   Read the middle row, not the first: in dark mode this rule ALWAYS
   applies, so the mark is never composited on bare #000 in a shipped
   page. 56.9% is a counterfactual. What ships went 25.4% -> 21.2% in
   dark and 22.9% -> 18.5% in light. The new crop is better in both.

   box-shadow with spread and NO blur, plus a matching background, is what
   draws the plate. Deliberately not padding: padding on a fixed-size <img>
   either grows the box (content-box) and pushes the bar taller, or shrinks
   the picture inside it (border-box). A shadow paints outside the border
   box and takes up no layout at all, so the 50x32 measurements and the
   48px bar are unchanged — re-verified across all 16 page/width combinations.

   #f5f5f7 and not #fff: it is the same value as --ink in dark mode, so the
   plate reads as part of the page's own palette rather than a hole punched
   in the bar. Contrast against the mark is within a hair of white.

   Both the media query and the [data-theme] rule are here because this
   site supports an explicit toggle as well as the OS setting — same shape
   as the photo rules above. The light override is what makes the toggle
   work in BOTH directions.
   ============================================================ */

@media (prefers-color-scheme: dark) {
    .nav__brand img {
        background: #f5f5f7;
        border-radius: 3px;
        box-shadow: 0 0 0 3px #f5f5f7;
    }
}

:root[data-theme="dark"] .nav__brand img {
    background: #f5f5f7;
    border-radius: 3px;
    box-shadow: 0 0 0 3px #f5f5f7;
}

:root[data-theme="light"] .nav__brand img {
    background: none;
    box-shadow: none;
}

/* ============================================================
   THE GUIDE — guide.html, and nothing else on this site.

   The owner wrote "Clicker Wonderland — The Complete Guide" as a
   standalone A4 print document with its own inline stylesheet: Segoe UI,
   point sizes, #d6336c pink and #7048e8 purple. That stylesheet is gone
   and this block replaces it. The MARKUP is the author's, unchanged —
   every heading, id, table, callout and .pb page-break marker is
   byte-identical to the file they wrote. Only the shell and the paint
   are the site's. See fleet/STATUS-A53.md.

   THE ONE RULE AT THE TOP OF THIS FILE DOES NOT APPLY HERE, and that is
   deliberate rather than an oversight. "One idea per screen, the picture
   does the talking, adding copy is how this design dies" is a rule about
   four marketing pages that have to sell in one screen. A 22-chapter
   manual is the opposite object: the only thing a visitor wants from it
   is words, quickly findable. Everything below is scoped to .guide so
   none of it can reach index, work, services or 404.

   COLOUR. Three accents, and they are not new: they are the logo's own
   pigments, the values already in the mesh markup on services.html.
       note (default)  --pink-text
       warning         #ed5f2f   the mark's orange
       tip             #207a64   the mark's teal
   They appear ONLY as a 3px left border — a boundary, held to WCAG
   1.4.11's 3:1, measured against --bg-soft in both schemes:
       --pink-text  5.73 light / 5.99 dark   (was 9.95 — the dark value
                    moved to #de6896 on 2026-08-02; see the token block)
       #ed5f2f      3.07 light / 5.77 dark
       #207a64      4.79 light / 3.70 dark
   NONE of them is allowed near type. #ed5f2f is 3.07:1 on #f5f5f7 and
   #207a64 is 3.70:1 on #0e0e10, both under the 4.5:1 a label needs, so
   the callout's bold lead-in stays --ink. The original document coloured
   that text; it is the one visual thing here that was deliberately not
   carried over, and this paragraph is why. Raw #90194c is NOT used as a
   border either — 2.23:1 on #0e0e10. --pink-text is the scheme-aware
   token and it is what a boundary must use. Same rule as .ph and .todo.
   ============================================================ */

.guide {
    padding-block: clamp(40px, 7vh, 80px) var(--chapter-y);
}

/* The cover. The author's markup is a .cover div holding h1, two .sub
   lines, a .rule and four .badge pills. Their version centred it under
   120px of padding for a title page; on screen it is the top of a web
   page, so it keeps the centring and loses the title-page air. */
.guide .cover {
    text-align: center;
    padding-bottom: 8px;
}

/* THE COVER TITLE GETS THE DISPLAY FACE. THE 22 CHAPTER HEADS DO NOT, AND
   THAT LINE IS DRAWN ON PURPOSE.

   .guide h1 is one string, 35-54px, on the cover of the document — a
   display position by any reading, and the guide would otherwise be the
   only page on the site still opening in the old face.

   .guide h2/h3/h4 are 22 chapter heads, 22 sub-heads and their sub-heads
   INSIDE a 25,000px reading document. A display face on 60-odd signposts
   through a manual is where an aesthetic starts charging the reader rent,
   and h3 is 17px, which is under the measured floor anyway. They keep the
   body stack. See the DISPLAY FACE block for the floor and how it was
   measured.

   Print is handled separately at the foot of this file: the PDF at
   downloads/ is generated FROM this page, and it is not being reprinted. */
.guide h1 {
    font-family: var(--display-face);
    font-size: clamp(2.2rem, 6vw, 3.4rem);
    font-weight: 700;
    letter-spacing: -0.04em;
    line-height: 1.05;
}

.guide .sub {
    font-size: clamp(1rem, 2vw, 1.1875rem);
    color: var(--ink-2);
    margin-top: 10px;
    max-width: var(--measure);
    margin-inline: auto;
}

/* The one place the brand palette appears as itself. The mark is a
   low-poly sweep from orange through magenta to teal; this is that sweep,
   120px wide. It is decoration with no text on it and no job as a
   boundary — which is the only reason these three raw values are allowed
   in a gradient at all. */
.guide .rule {
    width: 120px;
    height: 4px;
    margin: 22px auto;
    border-radius: 2px;
    background: linear-gradient(90deg, #ed5f2f, #90194c, #207a64);
}

.guide .badge {
    display: inline-block;
    margin: 3px 2px;
    padding: 4px 13px;
    border: 1px solid var(--hairline);
    border-radius: 999px;
    font-family: var(--mono);
    font-size: 0.6875rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    color: var(--ink-2);
}

/* The download. This block is the only markup on the page that is not the
   author's — see a53-build-guide.mjs in the scratchpad. .btn--quiet and
   not .btn: the site allows one filled call to action per screen and on
   this page it is not this one. */
.guide__dl {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 10px 18px;
    margin: 26px 0 8px;
}

/* Contents. Two columns is the author's; it is kept because 22 links in
   one column is a screen of its own, and dropped below 620px where two
   columns would give about 20 characters each. */
.guide .toc {
    margin-top: 14px;
    columns: 2;
    column-gap: 34px;
}

@media (max-width: 620px) {
    .guide .toc {
        columns: 1;
    }
}

.guide .toc a {
    display: block;
    padding: 5px 0;
    color: var(--ink);
    text-decoration: none;
    break-inside: avoid;
    /* 24px is WCAG 2.5.8's floor for a target and these are 22px lines.
       Same fix, same reason, as .nav__links a. */
    min-height: 24px;
}

.guide .toc a:hover {
    color: var(--pink-text);
}

.guide .toc .num {
    display: inline-block;
    width: 28px;
    font-family: var(--mono);
    font-size: 0.8125rem;
    font-weight: 600;
    color: var(--pink-text);
}

/* Chapter heads. A hairline above rather than the author's coloured
   underline: on a page of 22 of them, a rule above is the thing that says
   "new chapter" without adding a colour. */
.guide h2 {
    margin-top: 56px;
    padding-top: 22px;
    border-top: 1px solid var(--hairline);
    font-size: clamp(1.375rem, 3vw, 1.75rem);
    font-weight: 650;
    letter-spacing: -0.025em;
    line-height: 1.2;
}

.guide h2#toc {
    margin-top: 34px;
}

.guide h3 {
    margin-top: 30px;
    font-size: 1.0625rem;
    font-weight: 650;
    letter-spacing: -0.015em;
}

.guide h4 {
    margin-top: 20px;
    font-size: 0.9375rem;
    font-weight: 650;
    color: var(--ink-2);
}

.guide p,
.guide li {
    margin-top: 12px;
    line-height: 1.62;
}

.guide ul,
.guide ol {
    margin-top: 12px;
    padding-left: 22px;
}

.guide li {
    margin-top: 6px;
}

.guide b {
    font-weight: 650;
}

/* Tables. 18 of them, some five columns wide, on a page that is also read
   on a phone. display:block with overflow-x:auto below 700px makes the
   TABLE scroll rather than the document — the site's own rule is that
   nothing pushes the page sideways, and this is what keeps it true
   without touching the author's markup. */
.guide table {
    width: 100%;
    margin-top: 18px;
    border-collapse: collapse;
    font-size: 0.9375rem;
    break-inside: avoid;
}

.guide th,
.guide td {
    padding: 9px 12px;
    border: 1px solid var(--hairline);
    text-align: left;
    vertical-align: top;
    line-height: 1.5;
}

.guide th {
    background: var(--bg-soft);
    font-weight: 650;
}

@media (max-width: 700px) {
    .guide table {
        display: block;
        overflow-x: auto;
    }
}

/* Callouts. Colour lives on the border and nowhere else — see the block
   comment at the top of this section for the measured reason. */
.guide .callout {
    margin-top: 18px;
    padding: 12px 16px;
    background: var(--bg-soft);
    border-left: 3px solid var(--pink-text);
    border-radius: 0 var(--radius) var(--radius) 0;
    break-inside: avoid;
}

.guide .callout > b:first-child {
    color: var(--ink);
}

.guide .callout.warn {
    border-left-color: #ed5f2f;
}

.guide .callout.tip {
    border-left-color: #207a64;
}

/* Inline code. The hairline is not decoration: the background is
   --bg-soft and a callout's background is ALSO --bg-soft, so without a
   border every code span inside a callout would vanish. There are nine of
   them and several are in callouts. */
.guide code,
.guide .k {
    padding: 1px 6px;
    background: var(--bg-soft);
    border: 1px solid var(--hairline-soft);
    border-radius: 4px;
    font-family: var(--mono);
    font-size: 0.875em;
    color: var(--ink);
}

.guide .small {
    font-size: 0.8125rem;
    color: var(--ink-2);
}

.guide__colophon {
    margin-top: 48px;
    padding-top: 16px;
    border-top: 1px solid var(--hairline);
    font-size: 0.8125rem;
    color: var(--ink-2);
    text-align: center;
}

/* ============================================================
   PRINT — the PDF is this page.

   Clicker-Wonderland-Complete-Guide.pdf is produced by running
   Page.printToPDF over guide.html in headless Chrome, so there is exactly
   ONE source of truth for the text. There is no separate export to drift
   out of date, and re-printing after an edit is one command.

   .pb is the author's own page-break marker and it survives here — eight
   of them, on the chapters they wanted a fresh page for.

   Colours are forced light: prefers-color-scheme follows the machine, and
   a visitor in dark mode printing this would otherwise get thirty pages
   of black.
   ============================================================ */

@page {
    size: A4;
    margin: 18mm 16mm;
}

@media print {
    :root {
        --bg: #ffffff;
        --bg-soft: #f5f5f7;
        --ink: #1d1d1f;
        --ink-2: #55555a;
        --hairline: rgba(0, 0, 0, 0.28);
        --hairline-soft: rgba(0, 0, 0, 0.14);
        --pink-text: #b3275d;
        color-scheme: light;
    }

    html,
    body {
        background: #ffffff;
        color: #1d1d1f;
        font-size: 10.5pt;
    }

    /* The site chrome is not part of the document. The download button is
       the loudest of these: a PDF with a "Download the PDF" button
       printed on page one is the kind of thing that gets noticed after it
       has been sent to a customer. */
    .nav,
    .foot,
    .skip-link,
    .guide__dl {
        display: none !important;
    }

    .guide {
        padding: 0;
    }

    .wrap,
    .wrap--narrow {
        max-width: none;
        padding-inline: 0;
    }

    /* The cover gets its title page back. This is the one place the
       screen edition and the print edition are meant to differ. */
    .guide .cover {
        padding: 46mm 0 0;
        break-after: page;
    }

    .guide .pb {
        break-before: page;
    }

    /* A chapter head at the foot of a page with its first paragraph
       overleaf is the commonest way a printed manual looks careless. */
    .guide h2,
    .guide h3,
    .guide h4 {
        break-after: avoid;
        break-inside: avoid;
    }

    .guide h2 {
        margin-top: 22px;
        padding-top: 10px;
    }

    .guide table,
    .guide .callout {
        break-inside: avoid;
    }

    /* Tables must NOT be scrolling blocks on paper. */
    .guide table {
        display: table;
        overflow: visible;
    }

    .guide .toc {
        columns: 2;
    }

    /* The contents anchors are internal. On paper a link is not clickable
       and 22 underlined blue lines read as damage, so they print as plain
       text — which is what the author's own stylesheet did. */
    .guide a,
    .guide .toc a {
        color: inherit;
        text-decoration: none;
    }

    /* NO DISPLAY FACE ON PAPER, AND THIS IS THE LOAD-BEARING LINE OF THIS
       BLOCK, added 2026-08-02 with the face itself.

       downloads/Clicker-Wonderland-Complete-Guide.pdf is PRINTED FROM
       guide.html — that is the whole reason the page and the download
       cannot drift apart (fleet/STATUS-A53.md §5.3). So a screen-only type
       change to .guide h1 is a change to a customer-facing artefact unless
       this rule exists.

       THE FILE WAS REGENERATED ON 2026-08-02, and the sentence that stood
       here — "the PDF on disk … has NOT been regenerated: it is the same
       470,099 bytes it was at 01:24 on 2026-08-01" — was false by then. It
       is now 423,532 bytes over 27 pages, down from 470,099 over 30, and the
       difference is not this rule: the shipped file had never been a print
       of the stylesheet it was committed with. It carried the site nav, the
       footer and the download button into the PDF and set its type at screen
       size, 17/15pt against this sheet's 14/13.1pt. Reprinting the tree at
       the commit that first introduced the PDF also gives 27 pages, which is
       how that was established rather than assumed.

       There is a generator again and it is checked in this time:
       tools/build-guide-pdf.mjs, with check-guide-pdf.mjs beside it. The
       checker holds no list of phrases — that is exactly what went stale
       before — it loads guide.html under emulated print media and compares
       word multisets against the PDF's own extracted text, so the page and
       the download move together or it goes red.

       The general case matters more than the guide. A pixel face is a
       SCREEN idea: its whole argument is that one module lands on one
       device pixel, and paper has no device pixels — at 600dpi the modules
       resolve into small hard squares and the face reads as a fax of
       itself. Everything on this list is display type, so anything added
       to --display-face on screen should be added here too.

       Checked, not assumed: .quote and .subhead have no user in any
       document, and .display and .headline do not appear on guide.html.
       They are listed anyway, because the reason this rule exists is that
       the print edition must not follow the screen edition by accident. */
    .display,
    .headline,
    .subhead,
    .quote,
    .guide h1 {
        font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
            "Helvetica Neue", Arial, sans-serif;
    }
}
