/* ============================================================
   LIZ SOLMS — DESIGN TOKENS
   ============================================================ */

/* ---------- WEBFONT ----------
   Black-Paint ships only as a 40 MB bitmap OTF that no browser can use
   as a webfont. This WOFF2 was generated by tools/trace_font.py, which
   traces the font's embedded bitmaps into real outlines and keeps the
   original advance widths. Optical weight matches the source within 1.5%.
   font-display:swap => text is readable in the fallback face while the
   105 KB font downloads, rather than showing nothing. */
@font-face {
  font-family: "Black Paint Web";
  src: url("../assets/fonts/black-paint-web.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* ---------- BODY WEBFONT: EB Garamond, self-hosted ----------
   These are the exact binaries Google Fonts serves, taken from the
   @fontsource/eb-garamond package (which repackages the google/fonts
   repo), not from a distro package. That distinction matters: the
   Debian `fonts-ebgaramond` OTFs are Duffner's v0.016 with an hhea
   ascent of 710 against Google's 1007, so anything measured against
   them is measured against a different box.

   WHY SELF-HOST. It is not really about bytes — the browser fetches
   the same ~49 KB either way. It is about the waterfall and the third
   party. From the CDN the chain is HTML -> CSS on fonts.googleapis.com
   -> font on fonts.gstatic.com: three round trips across two new
   origins, each needing DNS, TCP and TLS, and the CSS request blocks
   the font request behind it. Self-hosted it is HTML -> font, same
   origin, already warm, and preloadable so it starts immediately
   rather than after the CSS parses. It also stops every visitor's IP
   going to Google, which is the question the developer would
   otherwise have to answer for an EU audience.

   ONLY WEIGHT 400. The old CDN request asked for 400, 500 and 400
   italic. Nothing on this page uses 500 — the two `font-weight: 400`
   declarations in style.css exist to hold headings AT 400 — so the
   500 faces are simply not shipped.

   BOTH SUBSETS, LOADED ON DEMAND. latin covers everything currently
   on the page (including the em dash and ellipsis); latin-ext carries
   the accented characters a CMS will eventually introduce. The
   unicode-range descriptors are copied from Google's own CSS, so the
   browser downloads latin-ext only if a character actually needs it —
   it costs nothing until it is used. Do NOT subset these files to the
   glyphs currently on the page: the text is CMS-driven and the first
   unusual character would fall back to Times.

   font-display: swap — text renders immediately in the fallback and
   reflows when the face arrives, rather than showing nothing. */

@font-face {
  font-family: "EB Garamond";
  src: url("../assets/fonts/eb-garamond-latin-400-normal.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  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;
}
@font-face {
  font-family: "EB Garamond";
  src: url("../assets/fonts/eb-garamond-latin-ext-400-normal.woff2") format("woff2");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
    U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F,
    U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
    U+A720-A7FF;
}
@font-face {
  font-family: "EB Garamond";
  src: url("../assets/fonts/eb-garamond-latin-400-italic.woff2") format("woff2");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
  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;
}
@font-face {
  font-family: "EB Garamond";
  src: url("../assets/fonts/eb-garamond-latin-ext-400-italic.woff2") format("woff2");
  font-weight: 400;
  font-style: italic;
  font-display: swap;
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7,
    U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F,
    U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F,
    U+A720-A7FF;
}

/* Licence: SIL Open Font License 1.1. The OFL requires the licence
   and copyright notice to travel with the font, which is what
   assets/fonts/EB-Garamond-OFL.txt is for. Do not delete it. */


/* The single source of truth for the visual system.
   Every colour below was sampled directly from "Liz Solms Page.pdf". */

:root {

  /* ---------- COLOUR: surfaces ---------- */
  --c-ground:        #F2F1E7;  /* page background — warm off-white */
  --c-ink:           #000000;  /* body + headings, 18.5:1 on ground */

  /* ---------- COLOUR: collage accents ---------- */
  /* Sampled from the artwork. Used for UI that must match the collage. */
  --c-green:         #8FD337;  /* PURCHASE blob */
  --c-blue:          #607CCC;  /* diagonal + strip */
  --c-yellow:        #DCBE4F;  /* vertical torn strips */
  --c-sage:          #91B1A0;  /* contact panel */
  --c-pink-pale:     #E0C4C1;  /* horizontal strip */

  /* ---------- COLOUR: interactive ---------- */
  /* The comp uses pure magenta #FF00FF for links. That is only 2.76:1
     against the ground — it fails WCAG AA (4.5:1 required for body text).
     --c-link is a darkened magenta that holds the same hue and passes
     at 4.58:1. --c-link-comp preserves the original for reference. */
  --c-link:          #C100C1;  /* 4.58:1 on ground — PASSES AA      */
  --c-link-hover:    #8F008F;  /* deeper on hover, 8.1:1            */
  --c-link-comp:     #FF00FF;  /* original comp value — fails AA    */
  --c-focus:         #000000;  /* focus ring, high contrast on all  */

  /* ---------- COLOUR: text selection ---------- */
  /* Sage was chosen over the pink, yellow and green: at 2.06:1 against the
     ground it is the only palette colour far enough from the page to read
     unmistakably as "selected". Black text on it is 9.0:1.
     --c-selection-text forces selected text to black — magenta links would
     otherwise sit at 2.2:1 on the highlight and turn to mud. */
  --c-selection:      #91B1A0;
  --c-selection-text: #000000;

  /* ---------- TYPE: families ---------- */
  /* Display = the traced Black Paint webfont (see @font-face above).
     Body = a Garamond. The comp uses Apple Garamond, which is Apple
     proprietary and cannot be licensed for web use. EB Garamond is the
     closest open substitute, and is now self-hosted from
     assets/fonts/ rather than pulled from Google's CDN — see the
     @font-face block above. */
  --font-display: "Black Paint Web", "EB Garamond", Georgia, serif;
  --font-body:    "EB Garamond", "Apple Garamond", Garamond, Georgia,
                  "Times New Roman", serif;

  /* ---------- TYPE: fluid scale ----------
     Max values are MEASURED off the comp at 1366px — found by scanning the
     rendered PDF for text rows and reading ink height and line spacing,
     not estimated by eye:

        wordmark      ~58px   line spacing 65px
        bio prose     ~27px   line spacing 43px  (1.57)
        work list     ~22px   line spacing 37px  (1.64)
        contact       ~34px   line spacing 45px  (1.28 — deliberately tight)

     This is large for web body copy, and it is the whole reason the comp
     breathes: fewer words per line, more room above the text for the
     paint. Ratio ~1.22 between steps.

     Minimums are set for a 390px phone, where 27px prose would leave about
     six words per line. clamp() rides between the two.

     Each slope is solved so the preferred value equals the MAXIMUM exactly
     at 1366 — the design width. The first pass eyeballed the coefficients
     and every step landed 5-8% short there (list text 20.8px against the
     comp's 22, prose 25.5 against 27), which read as the whole page being
     slightly too small. Formula: slope = (max - min) / (1366 - 390),
     intercept = min - slope x 390. */
  --step--1:  clamp(0.94rem, 0.863rem + 0.307vw, 1.13rem);  /* 15 → 18px */
  --step-0:   clamp(1.06rem, 0.938rem + 0.512vw, 1.38rem);  /* 17 → 22px  lists */
  --step-1:   clamp(1.19rem, 0.988rem + 0.820vw, 1.69rem);  /* 19 → 27px  prose */
  --step-2:   clamp(1.31rem, 1.013rem + 1.230vw, 2.06rem);  /* 21 → 33px  contact */
  --step-3:   clamp(1.50rem, 1.100rem + 1.639vw, 2.50rem);  /* 24 → 40px */
  --step-4:   clamp(2.00rem, 1.351rem + 2.664vw, 3.63rem);  /* 32 → 58px  wordmark */

  /* Leading tightens as type grows — a 3rem wordmark needs proportionally
     less room between lines than 1rem body copy. */
  --leading-tight:   1.12;   /* wordmark — comp measures 65/58 */
  --leading-heading: 1.18;
  --leading-body:    1.57;   /* comp: 43px on 27px prose */
  --leading-list:    1.64;   /* comp: 37px on 22px list items */
  --leading-contact: 1.36;   /* comp sets the contact block tight */

  /* Solved from the comp, per role, by fitting rendered string widths at a
     size pinned from cap height (tracking does not affect cap height, so
     the two can be separated).

     The wordmark is the outlier and was badly under-tracked at 0.05em:
     "LIZ" fitted 0.367em and "SOLMS" 0.373em independently. That wide
     spacing IS the wordmark — at 0.05em it read as a heading, not a name.

     Section headings went the other way: the comp measures 0.022-0.031em
     where I had 0.06em. */
  --tracking-wordmark: 0.37em;
  --tracking-display:  0.03em;   /* section headings */
  --tracking-cta:      0.05em;   /* SMALL HOT ROOMS on the blob */

  /* ---------- SPACING: 8px base, fluid at the large end ---------- */
  /* Small increments stay fixed (they sit inside components, where a
     shifting value just looks unstable). The large ones — the gaps
     between sections — scale with the viewport so the page doesn't feel
     cavernous on a phone or cramped on a desktop. */
  --space-2xs: 0.25rem;   /*  4px */
  --space-xs:  0.5rem;    /*  8px */
  --space-s:   1rem;      /* 16px */
  --space-m:   1.5rem;    /* 24px */
  --space-l:   clamp(1.75rem, 1.35rem + 1.7vw, 2.5rem);   /* 28 → 40px */
  --space-xl:  clamp(2.5rem,  1.70rem + 3.4vw, 4rem);     /* 40 → 64px */
  --space-2xl: clamp(3.5rem,  2.17rem + 5.7vw, 6rem);     /* 56 → 96px */

  /* ---------- LAYERS ----------
     A named stacking scale. The collage is not one flat plane behind the
     content — some art sits under the writing, some on top of it.

     The order is read off the comp material by material, which is how the
     collage was physically assembled: torn paper down first, painted marks
     onto the paper, coloured strips over those, photographs on top.

     Every decorative element declares its layer explicitly. Left implicit,
     DOM order decides the stacking by accident — which is what put the
     cream paper over the blue strip and washed it out. */
  --z-paper:       1;   /* torn paper panels — the base of the collage    */
  --z-paint:       2;   /* painted marks laid onto the paper              */
  --z-strip:       3;   /* coloured strips, over the papers               */
  --z-photo-back:  4;   /* the frond photograph                           */
  --z-splatter:    5;   /* wet paint flicked over the photographs          */
  --z-content:    20;   /* text, and images that ARE content              */
  --z-art-front:  30;   /* reserved: art over the content. Nothing uses it
                           — in the comp the splatter stops at Liz's
                           silhouette, so it sits below her, not above.  */
  --z-ui:         50;   /* skip link, focus affordances                 */

  /* Kept as an alias so nothing silently loses its layer. */
  --z-backdrop:    1;

  /* ---------- DEPTH ----------
     How fast a layer travels outward as the window grows past the 1366
     design width. 0 = stays with the writing. 1 = welded to the window
     edge, so edge art keeps its crop no matter how wide the screen.
     Values between the two give parallax on resize: near layers drift a
     little, far layers a lot, and the collage reads as having depth
     instead of being one flat plane that stretches. */
  --depth-flat: 0;      /* text, and art that belongs to it   */
  --depth-near: 0.25;   /* backdrops just behind the writing  */
  --depth-mid:  0.5;    /* the middle ground                  */
  --depth-far:  1;      /* welded to the window edge          */

  /* ---------- LAYOUT ---------- */
  --design-width:  1366px;  /* artboard width of the original comp */
  /* Comp runs body copy to 758px. At 27px that is ~56 characters — well
     inside the comfortable 45–75ch range, just set larger than usual. */
  --measure:       56ch;
  /* Comp sets its left margin at x72–90 on a 1366 page; 3.5rem (56px) ran
     the whole page 16px tight against it. */
  --gutter:        clamp(1.25rem, 5.3vw, 4.5rem);

  /* ---------- MOTION ----------
     Every value the animation pass uses lives here, so the whole site's
     motion can be retuned — or switched off — from one place.

     One dial does almost all the work:
       --drift-max  distance the farthest collage layer travels on scroll.
                    Set to 0 for no parallax at all.

     There is deliberately no idle-motion dial — the collage moves only
     while the reader scrolls. See motion.css section 3. */

  /* The wordmark's easing was the first thing that went wrong. An
     asymmetric "fast out, slow settle" curve spends 80% of its time on the
     last 15% of the sweep, so a 1.1s paint-in finished visibly in about
     300ms and read as a flash. This curve is near-linear through the
     middle — the brush travels at a steady rate and only eases at the
     ends, which is how a hand actually moves. */
  --ease-brush:  cubic-bezier(0.42, 0.05, 0.42, 0.96);  /* near-linear middle */
  --ease-settle: cubic-bezier(0.16, 0.84, 0.24, 1);

  /* A gentle S rather than a pure ease-out. An ease-out starts at full
     speed, and on a fade that reads as a snap followed by a long tail — the
     eye catches the start and then waits. Easing in a little at both ends
     means the reveal has no hard edge anywhere in it, which is most of what
     "softer" means for opacity. */
  --ease-out:  cubic-bezier(0.38, 0.06, 0.24, 1);
  --ease-wipe: cubic-bezier(0.36, 0.08, 0.28, 1);

  /* SCROLLED-INTO-VIEW durations. Still brisk: a drawn-out fade on a text
     block means the reader is waiting to read something already in front of
     them. The wipe is longer than the line fade only because it has several
     lines to travel across: the bio is three paragraphs and about sixteen
     lines, and the edge has to cross all of them. Any single line inside it
     still resolves in roughly a third of a second, and the edge stays ahead
     of anyone reading top-down — which is the point of revealing in reading
     order rather than all at once. */
  --dur-line:   620ms;   /* a heading or one list item              */
  --dur-wipe:  1900ms;   /* a whole prose block, line by line       */
  --dur-settle: 800ms;   /* the purchase blob                       */

  /* FIRST-SCREEN durations, used only for what is already visible when the
     page loads. Everything above the fold arrives in one go, with no
     scrolling to space it out, so the same timings that feel considered
     one block at a time feel like a burst all at once. These are longer,
     and js/motion.js spaces the blocks much further apart. */
  --dur-line-load:  820ms;
  --dur-wipe-load: 2500ms;

  --dur-paint: 2400ms;   /* the wordmark being brushed on at load   */

  /* ---------- LINK RULE ----------
     The underline is a dashed rule drawn as a repeating background
     rather than a text-decoration, so it can travel on hover.

     ALL FOUR VALUES ARE IN em, so the rule scales with whatever type
     it sits under — the same rule serves 33px contact copy and 17px
     list items without a breakpoint.

     --rule-pitch IS THE CRITICAL ONE. The keyframe in style.css
     travels exactly this distance, so the loop closes on itself
     invisibly. If the two ever disagree the dashes visibly jump once
     per cycle. One token, used twice, so they cannot drift.

     --rule-drop is the gap below the text, and it has to be measured
     against the SHIPPING font, not a lookalike. It was first set to
     0.14em against Debian's fonts-ebgaramond, which is Duffner's
     v0.016; the face this site actually serves is Google's v1.003,
     whose g descends deeper. Same value, 0.7px less daylight.
     Re-measured over http against the real face at 27px:

         0.10em  1.33px      0.18em  3.33px
         0.14em  2.33px      0.20em  4.33px
         0.17em  ~2.8px      0.30em  8px    <- the first pass, floated

     0.17em reads as attached to the word and still clears the tail of
     a g at the 17px list size. Below about 0.12em it grazes.

     --dur-rule: about 8px/s. Deliberately slower than it seems it
     needs to be — a regular pattern travelling fast strobes, and the
     eye reads it as juddering or briefly running backwards. */
  --rule-pitch:  0.34em;   /* one dash + one gap                     */
  --rule-dash:   0.17em;   /* ink within that pitch — half, so 1:1   */
  --rule-weight: 0.065em;  /* rule thickness                         */
  --rule-drop:   0.17em;   /* gap below the deepest descender        */
  --dur-rule:    760ms;    /* one pitch travelled                    */

  /* Scroll parallax — the amplitude for a piece at --depth-y 1, in a band
     whose pass length is about twice the viewport. Per-band scales in
     layout.css divide out the rest.

     Two earlier passes were both invisible, for different reasons. 20px of
     total travel is simply below the threshold. Then 52px looked right on
     paper — 35–48px of movement per screen of scroll — but every piece was
     moving by the same amount, and parallax is perceived as the difference
     between layers, not as absolute displacement. Spreading --depth-y from
     0.25 to 1.0 is what actually made it read; the amplitude here only sets
     how far the fastest layer goes.

     The target is roughly a 12% differential against the text, which is
     where the effect becomes legible without announcing itself.

     6.8rem was estimated, not measured, and it undershot: measured at
     1366 it put the fastest layer at 87px per screen, a 9.6%
     differential. That gap only became obvious after the idle float was
     removed — the float had been quietly making up the difference, and
     the collage read as flatter without it even though the scroll-linked
     travel was byte-for-byte identical (A/B'd against the pre-removal
     commit: same page height, same 243px on the fastest layer, same
     layer-to-layer spread; the only delta was 6.32px of idle drift
     going to zero).

     Measured sweep at 1366:

         6.8rem   87px/screen    9.6%   <- was here
         8.0rem  102px/screen   11.3%
         8.8rem  112px/screen   12.5%   <- the documented intent
         9.6rem  122px/screen   13.6%
        11.0rem  140px/screen   15.6%

     This is the dial. Turn it up if the effect wants to be louder;
     nothing else needs to change. */
  --drift-max: 8.8rem;

  /* Distance a line travels as it fades in. Deliberately small — the effect
     should read as a fade that happens to settle, not as a slide. */
  --rise: 0.55rem;
}
