/* ============================================================================
   FDE-ELEMENTS.CSS — the 20 elements catalogue
   ----------------------------------------------------------------------------
   Source of the twenty: RESEARCH-apple-and-20-elements.md §"20 SHIPPABLE
   ELEMENTS" (lines 984–1850), cross-checked against IMPLEMENTATION-PACK-v4.md
   §4. This file BUILDS them; it does not re-argue them.

   WHAT THIS FILE IS
   A catalogue. Every block is (a) a named element, (b) the one-line reason a
   practitioner reaches for it, (c) the exact markup when markup is required,
   (d) complete, working, dependency-free CSS.

   INERTNESS CONTRACT — the reason this file is safe to add to any page
   • Every rule is scoped to an `.fde-*` class. There is not one bare-element
     selector at the head of a rule. Adding the <link> changes NOTHING until a
     class is used.
   • The only non-class selector in the file is the `:root` block immediately
     below. It declares custom properties and nothing else — custom properties
     paint no pixels until something references them.
   • `::view-transition-*` rules (#18) name only `fde-`-prefixed transition
     names, so they are dead unless one of this file's classes is applied AND
     the page opts into a transition.
   • Nothing here edits, overrides, or depends on for-employers.html,
     nav.css, or chaptered-scroll.css. Another process owns those.

   TYPE LADDER — fixed, closed, six rungs + one label rung
     --t6 80px display · --t5 56px section title · --t4 24px sub-head
     --t3 17px body    · --t2 16px small / reading floor
     --t1 13px         · --t-label 14px label-only
   THE 13px RUNG IS NEVER USED IN THIS FILE. The brief's floor ("nothing may
   render below 14px, ever") and `--t1: 13px` are in direct conflict; the floor
   wins. Every label, eyebrow, caption and micro-line here resolves to
   `var(--t-label, 14px)` or `var(--t2)`. No new size is invented.

   COLOUR — brand tokens only, no new hue
   Read from for-employers.html :root. Every pair below was computed with the
   WCAG 2.x relative-luminance formula (sRGB, 8-bit), not estimated:
     --ink      #1A1410 on --bg #FAF6EC ............ 16.90:1  AA/AAA
     --gold-ink #6F5729 on --bg .................... 6.34:1   AA body
     --gold     #8B6F3A on --bg .................... 4.39:1   FAILS body AA,
                                                      passes 3:1 large-text —
                                                      so --gold is used for
                                                      rules/fills/numerals at
                                                      >=24px only, never body.
     --gold-warm #C8A96E on --bg ................... 2.08:1   FAILS everything
                                                      on cream. Used ONLY on an
                                                      --ink fill (8.13:1).
     --mute rgba(26,20,16,.62) = #6F6A64 on --bg ... 4.96:1   AA body
     --green #3a7a3a on --bg ....................... 4.83:1   AA body
     --red   #8E2419 on --bg ....................... 8.05:1   AA/AAA
     pane surface rgba(255,255,255,.55) over --bg = #FDFBF6:
       --ink 17.64 · --gold-ink 6.61 · --green 5.05 · --red 8.40 — all AA.
   Decorative-only values are labelled as such at their point of use.

   MOTION — compositor properties only (transform / opacity / filter / clip-path
   / mask). Every animated rule is inside `@media (prefers-reduced-motion:
   no-preference)`, and §R at the foot of the file re-neutralises each one under
   `reduce` as a belt-and-braces second gate.

   DEPENDENCIES — none. No CDN, no npm, no icon font, no external image, no JS
   required for any of the twenty (#9's optional pointer-lerp is called out
   honestly as the one place JS buys something CSS cannot).

   LINK IT (last, after the shared sheets, before the page's inline <style>):
     <link rel="stylesheet" href="./shared/fde-elements.css">
   ========================================================================== */


/* --- tokens: custom properties only. Paints nothing. ---------------------- */
:root{
  /* the label rung the ladder is missing on the page */
  --t-label:14px;

  /* geometry, ported from the page's own measured Apple values */
  --fde-pad:clamp(4.5rem, 8vw, 9rem);
  --fde-gut:clamp(1.25rem, 4vw, 3rem);
  --fde-nav-h:72px;

  /* easing */
  --fde-ease:cubic-bezier(.16,1,.3,1);
  --fde-spring:cubic-bezier(.16,1,.3,1);                 /* fallback */
  --fde-spring:linear(0, .006, .026 2.5%, .107 5%, .443 12.5%, .775 20%,
                      .978 27.5%, 1.056 32.5%, 1.062 37.5%, 1.031 45%,
                      .995 55%, .991 70%, 1);            /* real overshoot */

  /* derived surfaces — tints of existing tokens, no new hue.
     Each is declared twice: plain rgba first, color-mix second, so engines
     without color-mix() still get the exact same colour. */
  --fde-hair:rgba(26,20,16,.13);
  --fde-hair:color-mix(in oklab, var(--ink) 13%, transparent);
  --fde-hair-strong:rgba(26,20,16,.22);
  --fde-hair-strong:color-mix(in oklab, var(--ink) 22%, transparent);
  --fde-hair-w:1px;

  --fde-surface:rgba(255,255,255,.55);      /* = the page's pane, #FDFBF6 */
  --fde-tint:rgba(139,111,58,.08);          /* gold @8% = #F1EBDE           */
  --fde-select:rgba(139,111,58,.30);        /* gold @30% = #D9CEB7          */
  --fde-select-hi:rgba(139,111,58,.38);     /* gold @38% = #D0C3A8          */
}
@media (min-resolution:2dppx){
  :root{ --fde-hair-w:.5px }   /* a 1px rule on a 2x panel is a 2px rule */
}


/* ============================================================================
   01 — CONTENT-SIZED SECTION                                  .fde-section
   Practitioners kill the `min-height:100svh` per section because a viewport
   floor parks dead air above and below short content — that is the exact
   "so much white space / feels empty" symptom. Height comes from content;
   rhythm comes from a padding token whose VARIANCE the reader feels as cadence.
   Apple ships 160/128/96px section padding and no vh floor on any content
   section. Ported here as three stepped values, not a clamp, so the padding
   changes at the same breakpoints the type does.

   HTML: <section class="fde-section">…</section>
         <section class="fde-section fde-section--tight">…</section>
         <section class="fde-section fde-section--open">…</section>
   ========================================================================== */
.fde-section{
  display:block;
  min-height:0;
  overflow:visible;
  padding-block:160px;
  padding-inline:var(--fde-gut);
}
@media (max-width:1068px){ .fde-section{ padding-block:128px } }
@media (max-width:734px) { .fde-section{ padding-block:96px  } }

/* consecutive sections do not double their seam */
.fde-section + .fde-section{ padding-block-start:calc(160px * .72) }
@media (max-width:1068px){ .fde-section + .fde-section{ padding-block-start:calc(128px * .72) } }
@media (max-width:734px) { .fde-section + .fde-section{ padding-block-start:calc(96px  * .72) } }

.fde-section--tight{ padding-block:96px }
.fde-section--open { padding-block:224px }
@media (max-width:734px){
  .fde-section--tight{ padding-block:64px }
  .fde-section--open { padding-block:120px }
}

/* the ONE viewport floor a page is allowed, and in px like Apple's hero */
.fde-section--hero{
  min-height:760px;
  padding-block-start:calc(160px + var(--fde-nav-h));
}
@media (max-width:734px){ .fde-section--hero{ min-height:0; padding-block-start:96px } }


/* ============================================================================
   02 — SCROLL-DRIVEN DEPTH                     .fde-depth / .fde-depth__layer
   The document itself moves in Z, not a decorative background. Practitioners
   use it because differential layer velocity is the cheapest possible signal
   that a real person art-directed the page — and since CSS scroll-driven
   animations run off the main thread, it costs zero JS and zero jank.
   Compositor-only: transform, nothing else.

   HTML: <div class="fde-depth">
           <div class="fde-depth__layer fde-depth__layer--far">…</div>
           <div class="fde-depth__layer fde-depth__layer--near">…</div>
         </div>
   ========================================================================== */
.fde-depth{
  position:relative;
  perspective:1200px;
  perspective-origin:50% 40%;
}
.fde-depth__layer{ transform-style:preserve-3d }

@keyframes fdeDepth{
  from{ transform:translate3d(0, var(--fde-depth-from, 6%), 0) }
  to  { transform:translate3d(0, var(--fde-depth-to,  -6%), 0) }
}
@supports (animation-timeline: view()){
  @media (prefers-reduced-motion: no-preference){
    .fde-depth__layer{
      animation-name:fdeDepth;
      animation-fill-mode:both;
      animation-timing-function:linear;
      animation-timeline:view();
      animation-range:cover 0% cover 100%;
      will-change:transform;
    }
    .fde-depth__layer--far { --fde-depth-from: 3%;  --fde-depth-to: -3%  }
    .fde-depth__layer--near{ --fde-depth-from:11%;  --fde-depth-to:-11%  }
  }
}
/* Honest limit: the layers parallax, they do not literally translateZ. A live
   translateZ would need a scale compensation the timeline cannot supply in the
   same transform, and the visual delta at these magnitudes is nil. */


/* ============================================================================
   03 — PINNED SCRUB, ONE EARNED MOMENT                            .fde-pin
   The single mechanic Apple actually ships (verified on macbook-pro,
   airpods-pro, iphone-17-pro): a sticky stage whose beats cross-fade as the
   track scrolls past. Practitioners use it exactly once per page — used twice
   it becomes the sticky card-deck trope. Built here with `view-timeline` on
   the track and `animation-timeline` on the beats, so descendants inherit the
   timeline by name and NO JS and no library is involved.

   The un-enhanced state is the readable one: beats stack in normal flow. The
   pin is added only where scroll-driven animation is supported AND motion is
   not reduced. That ordering is the whole trick — the page is complete first.

   HTML: <div class="fde-pin" style="--fde-beats:3">
           <div class="fde-pin__stage">
             <div class="fde-beat">…</div>
             <div class="fde-beat">…</div>
             <div class="fde-beat">…</div>
           </div>
         </div>
   ========================================================================== */
.fde-pin{ --fde-beats:3; position:relative }
.fde-pin__stage{ display:block }
.fde-beat{ margin-block-end:var(--sp-4, 2.2rem) }
.fde-beat:last-child{ margin-block-end:0 }

@keyframes fdeBeat{
  0%           { opacity:0; transform:translate3d(0,14px,0) }
  14%, 74%     { opacity:1; transform:translate3d(0,0,0)    }
  100%         { opacity:0; transform:translate3d(0,-14px,0)}
}
@supports (animation-timeline: view()) and (animation-range: contain 0%){
  @media (prefers-reduced-motion: no-preference){
    .fde-pin{
      view-timeline-name:--fde-pin;
      view-timeline-axis:block;
      min-height:calc(var(--fde-beats) * 100svh);
    }
    .fde-pin__stage{
      position:sticky;
      inset-block-start:0;
      height:100svh;
      display:grid;
      place-items:center;
      overflow:clip;
    }
    .fde-beat{
      grid-area:1 / 1;
      margin-block-end:0;
      opacity:0;
      animation-name:fdeBeat;
      animation-fill-mode:both;
      animation-timing-function:linear;
      animation-timeline:--fde-pin;
      will-change:opacity, transform;
    }
    .fde-beat:nth-child(1){ animation-range:contain 0%   contain 34%  }
    .fde-beat:nth-child(2){ animation-range:contain 33%  contain 67%  }
    .fde-beat:nth-child(3){ animation-range:contain 66%  contain 100% }
    .fde-beat:nth-child(4){ animation-range:contain 74%  contain 100% }
  }
}


/* ============================================================================
   04 — OPTICAL ALIGNMENT + LEADING TRIM             .fde-optical / --label
   The highest-signal "a designer touched this" in the whole catalogue. A
   heading carries ~0.28em of phantom leading above its cap-height, so it can
   never optically align with the rule or the number beside it. Metric
   alignment is not optical alignment. `text-box` trims the phantom; the em
   nudge on mono labels corrects the cap-to-baseline offset the trim does not
   cover; `hanging-punctuation` pulls an opening quote out of the measure so
   the left edge stays a straight line.

   HTML: <h2 class="fde-optical">…</h2>
         <p class="fde-optical-label">…</p>
   ========================================================================== */
@supports (text-box: trim-both cap alphabetic){
  .fde-optical{ text-box:trim-both cap alphabetic }
}
@supports not (text-box: trim-both cap alphabetic){
  .fde-optical{ margin-block-start:-.13em }   /* Fraunces 300 cap shim */
}
.fde-optical-label{ transform:translateY(-.055em) }   /* static, not motion */
.fde-optical-hang{ hanging-punctuation:first last }


/* ============================================================================
   05 — MEASURE + BALANCED / PRETTY WRAP                      .fde-measure
   A 90-character line is the single loudest reason generated pages "feel
   wrong": past ~75 characters the eye loses the return sweep. `balance` is
   capped at 6 lines in Chromium, which is precisely why it belongs on
   headings and `pretty` belongs on body — pretty only fixes the last line
   (orphans) and has no line cap.

   HTML: <h2 class="fde-measure-head">…</h2>
         <p  class="fde-measure-lede">…</p>
         <div class="fde-measure"><p>…</p><ul><li>…</li></ul></div>
   ========================================================================== */
.fde-measure-head{ text-wrap:balance; max-width:20ch }
.fde-measure-lede{ text-wrap:pretty;  max-width:46ch; font-size:var(--t4) }
.fde-measure{ max-width:64ch }
.fde-measure p,
.fde-measure li{ text-wrap:pretty; max-width:64ch; overflow-wrap:break-word }
.fde-measure-wide{ max-width:none }
@media (max-width:40rem){
  .fde-measure p{ hyphens:auto; -webkit-hyphens:auto; hyphenate-limit-chars:8 3 3 }
}


/* ============================================================================
   06 — TABULAR FIGURES + THE 16px FLOOR       .fde-figures / .fde-micro
   Proportional digits make a column of numbers jitter; on an evidence page
   whose whole job is numbers, that jitter is the difference between "record"
   and "decoration". `slashed-zero` removes the O/0 ambiguity in mono. Negative
   tracking below about -0.02em on a display italic destroys the alignment the
   tabular figures just bought, so it is capped here.

   The floor half of this element: `.fde-micro` is the smallest reading class
   in the file and it resolves to --t2 (16px). Nothing in this catalogue emits
   a size below --t-label (14px), and no reading text below --t2.

   HTML: <div class="fde-figures"> … any table, list or number stack … </div>
         <p class="fde-micro">…</p>
   ========================================================================== */
.fde-figures{
  font-variant-numeric:tabular-nums lining-nums slashed-zero;
  font-feature-settings:"tnum" 1,"lnum" 1,"zero" 1;
  font-variant-ligatures:none;
}
.fde-figures__num{
  font-size:var(--t5);              /* 56px — --gold at 4.39:1 clears the 3:1
                                       large-text threshold at this rung */
  letter-spacing:-.018em;
  color:var(--gold);
  font-variant-numeric:tabular-nums lining-nums slashed-zero;
}
.fde-micro{
  font-size:var(--t2);              /* 16px reading floor */
  line-height:1.5;
  letter-spacing:.03em;
  color:var(--mute);                /* #6F6A64 on --bg = 4.96:1, AA body */
}
.fde-label{
  font-size:var(--t-label, 14px);   /* 14px, label-only, never a reading block */
  letter-spacing:.16em;
  line-height:1.2;
  text-transform:uppercase;
  color:var(--gold-ink);            /* 6.34:1 on --bg, AA body */
}


/* ============================================================================
   07 — HAIRLINE SYSTEM                     .fde-hair / .fde-hairbox / --fade
   A full-strength 1px rule running edge to edge is a wireframe artifact. Two
   fixes practitioners apply together: drop to 0.5px on 2x panels (a 1px rule
   is physically 2px there), and fade the ends so the rule reads engraved
   rather than drawn. This is how you build hierarchy on cream without the
   shadows the brand bans.

   CONTRAST NOTE, stated rather than assumed: --fde-hair (--ink @13%) computes
   to #DDD9CF, which is 1.31:1 against --bg. That is DECORATIVE ONLY and is
   never used as the boundary of an interactive control. Interactive borders
   use --line-ctl #8B6F3A = 4.39:1, clearing the 3:1 non-text requirement.

   HTML: <hr class="fde-hair fde-hair--fade">
         <div class="fde-hairbox">…</div>
   ========================================================================== */
.fde-hair{
  border:0;
  block-size:var(--fde-hair-w);
  margin:0;
  background:var(--fde-hair-strong);
}
.fde-hair--fade{
  block-size:1px;
  background:linear-gradient(90deg,
    transparent 0,
    var(--fde-hair) 9%,
    var(--fde-hair-strong) 50%,
    var(--fde-hair) 91%,
    transparent 100%);
}
.fde-hairbox{
  border-radius:2px;
  box-shadow:inset 0 0 0 var(--fde-hair-w) var(--fde-hair);
}
.fde-hairbox--ctl{                 /* interactive boundary: opaque, 4.39:1 */
  box-shadow:none;
  border:1px solid var(--line-ctl);
  border-radius:2px;
}
@media (forced-colors: active){
  .fde-hair, .fde-hair--fade{ background:none; border-block-start:1px solid CanvasText }
  .fde-hairbox{ box-shadow:none; border:1px solid CanvasText }
}


/* ============================================================================
   08 — A FOCUS RING SOMEONE DESIGNED       .fde-focus / .fde-focus-scope
   `outline` paints a square halo around a 2px-radius control — the loudest
   browser-default tell on an otherwise crafted page. A two-layer box-shadow
   respects the radius, and the inner --bg layer buys a 1px breathing gap so
   the ring never touches a glyph.

   CONTRAST, computed: ring --gold-ink #6F5729 vs --bg = 6.34:1. Against an
   --ink-filled button the ring alone would be only 2.67:1 — which is exactly
   why the 2px --bg gap layer is MANDATORY, not decorative: it separates ring
   from fill (--bg vs --ink = 16.90:1) so both adjacencies clear 3:1.

   HTML (per control):  <button class="fde-focus">…</button>
   HTML (whole region): <form class="fde-focus-scope">…</form>
   ========================================================================== */
.fde-focus,
.fde-focus-scope :is(a, button, summary, input, select, textarea, [tabindex]:not([tabindex="-1"])){
  border-radius:2px;
}
.fde-focus:focus,
.fde-focus-scope :is(a, button, summary, input, select, textarea, [tabindex]:not([tabindex="-1"])):focus{
  outline:none;
}
.fde-focus:focus-visible,
.fde-focus-scope :is(a, button, summary, input, select, textarea, [tabindex]:not([tabindex="-1"])):focus-visible{
  outline:none;
  box-shadow:0 0 0 2px var(--bg), 0 0 0 3.5px var(--gold-ink);
  transition:box-shadow .12s var(--fde-ease);
}
@supports not selector(:focus-visible){
  .fde-focus:focus,
  .fde-focus-scope :is(a, button, summary, [tabindex]):focus{
    box-shadow:0 0 0 2px var(--bg), 0 0 0 3.5px var(--gold-ink);
  }
}
@media (forced-colors: active){
  .fde-focus:focus-visible,
  .fde-focus-scope :is(a, button, summary, input, select, textarea, [tabindex]):focus-visible{
    outline:3px solid Highlight; outline-offset:2px; box-shadow:none;
  }
}


/* ============================================================================
   09 — HOVER WITH REAL WEIGHT                                   .fde-lift
   A CSS transition is mechanically linear, so the hand reads it as a slide.
   Real weight needs overshoot, and `linear()` gives you a genuine spring curve
   with zero JS — this is the CSS-only half of the "physics hover" the research
   writes as a pointer lerp. Double-gated on (hover:hover) AND (pointer:fine),
   per Rauno's rule: a hover state must never fire on a touch press.

   HONEST LIMIT: pointer-position tilt (rotateX/rotateY tracking the cursor)
   genuinely does need JS — CSS cannot read pointer coordinates. What CSS gives
   you for free is the velocity, which is most of the perceived weight. The
   `translateZ` on children below is the parallax layer that JS tilt would
   drive; on its own it still buys real depth on hover.

   HTML: <article class="fde-lift"><div class="fde-lift__face">…</div></article>
   ========================================================================== */
.fde-lift{ transform:translate3d(0,0,0) }
.fde-lift__face{ transform:translateZ(14px) }

@media (hover:hover) and (pointer:fine){
  @media (prefers-reduced-motion: no-preference){
    .fde-lift{
      transform-style:preserve-3d;
      will-change:transform;
      transition:transform .42s var(--fde-spring), box-shadow .18s var(--fde-ease);
    }
    .fde-lift:hover{ transform:translate3d(0,-5px,0) }
    .fde-lift:active{ transform:translate3d(0,-1px,0); transition-duration:.09s }
  }
  .fde-lift:hover{ box-shadow:inset 0 0 0 var(--fde-hair-w) var(--fde-hair-strong) }
}
@media (hover:none){
  .fde-lift{ will-change:auto }
  .fde-lift__face{ transform:none }
}


/* ============================================================================
   10 — SELECTION STYLING                                    .fde-selection
   A hiring manager WILL drag-select your numbers to paste them somewhere. The
   OS default blue is then the loudest un-designed pixel on a cream page. This
   is two lines of CSS that nobody ships and everybody notices.

   CONTRAST, computed: --gold @30% over --bg composites to #D9CEB7; --ink on
   that is 11.69:1 (AAA). The high-emphasis variant --gold @38% = #D0C3A8,
   --ink on it 10.47:1. The inverse chip (--ink fill, --bg text) is 16.90:1.
   --gold-ink on the selection fill would be only 4.38:1, so selected text is
   forced to --ink, never gold.

   HTML: <main class="fde-selection">…</main>     (scope, not per-element)
         <span class="fde-selection-invert">…</span>
   ========================================================================== */
.fde-selection ::selection,
.fde-selection::selection{ background:var(--fde-select); color:var(--ink); text-shadow:none }
.fde-selection ::-moz-selection,
.fde-selection::-moz-selection{ background:var(--fde-select); color:var(--ink) }
.fde-selection ::target-text{ background:var(--fde-select-hi); color:var(--ink) }

.fde-selection-invert::selection,
.fde-selection-invert ::selection{ background:var(--ink); color:var(--bg) }
.fde-selection-invert::-moz-selection,
.fde-selection-invert ::-moz-selection{ background:var(--ink); color:var(--bg) }

@media (forced-colors: active){
  .fde-selection ::selection, .fde-selection::selection{ background:Highlight; color:HighlightText }
}


/* ============================================================================
   11 — CARET + SCROLLBAR                    .fde-scrollskin / .fde-field
   The scrollbar is a 10px-wide strip of un-designed chrome running the full
   height of the page, and `scrollbar-gutter:stable` is a real CLS fix, not a
   flourish. `scroll-padding-block-start` is what stops every #anchor landing
   underneath a fixed nav — the most common broken thing on a chaptered page.

   CONTRAST: the thumb is --gold mixed 45% into --bg = #C8B99C, 2.05:1 against
   the track. That is a UA scrollbar part, explicitly out of scope for WCAG
   1.4.11 non-text contrast; the native scrollbar remains available at full
   contrast for anyone who has not opted into this class.

   HTML — for the document scrollbar the class goes on <html>:
     <html lang="en" class="fde-scrollskin">
   HTML — for any inner scroller:
     <div class="fde-scrollskin" style="overflow:auto">…</div>
   HTML — for text inputs: <input class="fde-field" type="text">
   ========================================================================== */
.fde-scrollskin{
  scrollbar-width:thin;
  scrollbar-gutter:stable;
  scrollbar-color:color-mix(in oklab, var(--gold) 55%, var(--bg)) var(--bg);
  scroll-padding-block-start:calc(var(--fde-nav-h) + 1rem);
  scroll-behavior:auto;
}
.fde-scrollskin::-webkit-scrollbar{ width:10px; height:10px }
.fde-scrollskin::-webkit-scrollbar-track{ background:var(--bg) }
.fde-scrollskin::-webkit-scrollbar-thumb{
  background:color-mix(in oklab, var(--gold) 45%, var(--bg));
  border:3px solid var(--bg);
  border-radius:0;
}
.fde-scrollskin::-webkit-scrollbar-thumb:hover{ background:var(--gold-ink) }

.fde-field{ caret-color:var(--gold-ink) }   /* 6.34:1 — the caret is findable */

@media (prefers-reduced-motion: no-preference){
  .fde-scrollskin--smooth{ scroll-behavior:smooth }
}


/* ============================================================================
   12 — CONTAINER QUERIES ON THE CARD             .fde-cq / .fde-card
   The component stops caring about the viewport and starts caring about the
   space it was actually given. This is the difference between a card that
   works in a sidebar AND a full-bleed row, and a card that only works at the
   breakpoint it was designed at. Practitioners moved here because viewport
   media queries cannot express "I am 30rem wide inside a 1440px page".

   HTML: <div class="fde-cq">
           <article class="fde-card">
             <figure class="fde-card__media">…</figure>
             <div class="fde-card__body">
               <h3 class="fde-card__title">…</h3>
               <p class="fde-micro">…</p>
             </div>
             <div class="fde-card__meta">…</div>
           </article>
         </div>
   ========================================================================== */
.fde-cq{ container-type:inline-size; container-name:fdecard }

.fde-card{
  display:grid;
  gap:var(--sp-2, .9rem);
  background:var(--fde-surface);        /* #FDFBF6 — --ink on it 17.64:1 */
  padding:var(--sp-3, 1.4rem);
  border-radius:2px;
  box-shadow:inset 0 0 0 var(--fde-hair-w) var(--fde-hair);
}
.fde-card__media{ margin:0 }
.fde-card__media img{ display:block; inline-size:100%; block-size:auto; aspect-ratio:16/10; object-fit:cover }
.fde-card__title{ font-size:var(--t3); line-height:1.3; margin:0 }
.fde-card__meta{ font-size:var(--t-label, 14px); letter-spacing:.06em; color:var(--gold-ink) }

@container fdecard (min-width: 30rem){
  .fde-card{ grid-template-columns:minmax(9rem, 14rem) 1fr; align-items:start }
  .fde-card__meta{ grid-column:2 }
}
@container fdecard (min-width: 48rem){
  .fde-card{ grid-template-columns:minmax(12rem, 18rem) 1fr auto; align-items:center }
  .fde-card__meta{ grid-column:3; text-align:end }
  .fde-card__title{ font-size:var(--t4) }
}


/* ============================================================================
   13 — :has() LAYOUT LOGIC                                     .fde-panel
   Where :has() genuinely earns its place: the layout reacts to what the author
   actually put in the panel, so the markup stops carrying layout classes. A
   panel with media becomes a split; a panel without one gets the full reading
   measure; a panel containing a focused control raises its own hairline so the
   focus ring reads against a defined edge instead of a vague one.

   HTML: <section class="fde-panel">
           <div class="fde-panel__media">…</div>   <!-- optional -->
           <div class="fde-panel__body">…</div>
         </section>
   ========================================================================== */
.fde-panel{ display:grid; gap:var(--sp-3, 1.4rem) }
.fde-panel__media{ margin:0 }
.fde-panel__media img{ display:block; inline-size:100%; block-size:auto }

@supports selector(:has(*)){
  /* has media → split, media first on the reading axis */
  @media (min-width:900px){
    .fde-panel:has(> .fde-panel__media){ grid-template-columns:1.15fr 1fr; align-items:start }
  }
  /* no media → give the prose its real measure back */
  .fde-panel:not(:has(> .fde-panel__media)) .fde-panel__body{ max-width:64ch }

  /* a panel whose control is focused defines its own edge */
  .fde-panel:has(:focus-visible){ box-shadow:inset 0 0 0 var(--fde-hair-w) var(--fde-hair-strong) }

  /* a dense list stops trying to be airy */
  .fde-spec:has(> .fde-spec__row:nth-child(7)) .fde-spec__row{ padding-block:.45rem }

  /* a grid that was given an aside stops centring on the main column */
  .fde-grid:has(> .fde-col-aside) .fde-col-main{ max-width:none }
}


/* ============================================================================
   14 — GRAIN / NOISE OVERLAY                                    .fde-grain
   Flat cream is the tell; a 4% noise field is what makes a digital surface
   read as paper. Practitioners use it because it is the cheapest possible
   texture — one inline SVG data URI, no request, no asset pipeline.

   GOVERNANCE, which is the part everyone skips: grain must never erode text
   contrast for a low-vision reader and must never print. Both are gated below.
   Do NOT stack this on a page that already ships a grain layer (for-employers
   already renders one at chaptered-scroll.css:99–101) — two layers double the
   density and the darkening becomes measurable.

   HTML: <div class="fde-grain">…</div>       (any positioned-context wrapper)
   ========================================================================== */
.fde-grain{ position:relative; isolation:isolate }
.fde-grain::after{
  content:"";
  position:absolute;
  inset:0;
  z-index:2;
  pointer-events:none;
  opacity:.045;
  mix-blend-mode:multiply;
  background-repeat:repeat;
  background-size:180px 180px;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)' opacity='.5'/%3E%3C/svg%3E");
}
.fde-grain--fixed::after{ position:fixed }
@media (prefers-contrast: more), (forced-colors: active), print{
  .fde-grain::after{ display:none }
}


/* ============================================================================
   15 — TWO-AXIS GRADIENT MASK                                 .fde-feather
   The locked brand media treatment, done correctly. A 2-stop linear ramp
   BANDS visibly on an 8-bit display against cream — the stair-steps are the
   artifact you see on the hero portrait. Five eased stops per axis, composited
   with `intersect`, kills it. Practitioners reach for a mask instead of a
   gradient overlay because a mask carries the alpha into the compositor and
   survives any background behind it.

   CONTRAST: cannot be computed. The colour under a feather is whatever image
   the author supplies, which is not a token. Never place text over a feathered
   region; the caption class below sits outside the mask, on --bg.

   HTML: <figure class="fde-band">
           <div class="fde-feather">
             <picture><img src="…" width="1248" height="702" alt="…"></picture>
           </div>
           <figcaption class="fde-band__cap">…</figcaption>
         </figure>
   ========================================================================== */
.fde-feather{
  -webkit-mask-image:
    linear-gradient(to bottom, transparent 0, rgba(0,0,0,.19) 3%, rgba(0,0,0,.63) 7%, #000 13%, #000 100%),
    linear-gradient(to right,  transparent 0, rgba(0,0,0,.19) 4%, rgba(0,0,0,.63) 9%, #000 17%, #000 100%);
  -webkit-mask-composite:source-in;
  mask-image:
    linear-gradient(to bottom, transparent 0, rgba(0,0,0,.19) 3%, rgba(0,0,0,.63) 7%, #000 13%, #000 100%),
    linear-gradient(to right,  transparent 0, rgba(0,0,0,.19) 4%, rgba(0,0,0,.63) 9%, #000 17%, #000 100%);
  mask-composite:intersect;
  mask-size:100% 100%;
  mask-repeat:no-repeat;
}
.fde-feather--edges{
  -webkit-mask-image:
    linear-gradient(to bottom, transparent 0, rgba(0,0,0,.19) 3%, rgba(0,0,0,.63) 7%, #000 13%, #000 87%, rgba(0,0,0,.63) 93%, rgba(0,0,0,.19) 97%, transparent 100%),
    linear-gradient(to right,  transparent 0, rgba(0,0,0,.19) 4%, rgba(0,0,0,.63) 9%, #000 17%, #000 83%, rgba(0,0,0,.63) 91%, rgba(0,0,0,.19) 96%, transparent 100%);
  mask-image:
    linear-gradient(to bottom, transparent 0, rgba(0,0,0,.19) 3%, rgba(0,0,0,.63) 7%, #000 13%, #000 87%, rgba(0,0,0,.63) 93%, rgba(0,0,0,.19) 97%, transparent 100%),
    linear-gradient(to right,  transparent 0, rgba(0,0,0,.19) 4%, rgba(0,0,0,.63) 9%, #000 17%, #000 83%, rgba(0,0,0,.63) 91%, rgba(0,0,0,.19) 96%, transparent 100%);
}

/* the band that carries it, plus its caption on --bg where contrast is real */
.fde-band{ margin-block:calc(var(--fde-pad) * .55); margin-inline:0 }
.fde-band img{ display:block; inline-size:100%; block-size:clamp(320px, 44svh, 540px); object-fit:cover }
.fde-band__cap{
  margin:.9rem var(--fde-gut) 0;
  font-size:var(--t2);              /* 16px */
  letter-spacing:.06em;
  color:var(--mute);                /* 4.96:1 on --bg, AA body */
}

/* the reveal, clipping the INNER img so the mask layer is never re-rasterised */
@keyframes fdeWipe{
  from{ clip-path:inset(0 0 78% 0); transform:scale(1.05) }
  to  { clip-path:inset(0 0 0%  0); transform:scale(1)    }
}
.fde-feather img{ clip-path:inset(0 0 0 0) }    /* default state = revealed */
@supports (animation-timeline: view()){
  @media (prefers-reduced-motion: no-preference){
    .fde-feather img{
      animation-name:fdeWipe;
      animation-fill-mode:both;
      animation-timing-function:linear;
      animation-timeline:view();
      animation-range:entry 8% cover 36%;
    }
  }
}


/* ============================================================================
   16 — SCROLL PROGRESS, ZERO JS                              .fde-progress
   Every agency page ships a progress bar and almost all of them ship it with a
   scroll listener that fires hundreds of times a second on the main thread.
   `animation-timeline: scroll(root block)` does the same job off-thread, in
   four declarations, and cannot jank.

   CONTRAST: bar is --gold-ink on --bg = 6.34:1. It is a decorative status
   indicator, not the sole carrier of any information.
   REDUCED MOTION: removed outright rather than frozen — a frozen bar would
   read as "100% scrolled" and lie. The native scrollbar still communicates
   position.

   HTML (first child of <body>):
     <div class="fde-progress" aria-hidden="true"><i class="fde-progress__bar"></i></div>
   ========================================================================== */
.fde-progress{
  position:fixed;
  inset-block-start:0;
  inset-inline:0;
  block-size:2px;
  z-index:60;
  pointer-events:none;
  background:transparent;
}
.fde-progress__bar{
  display:block;
  block-size:100%;
  inline-size:100%;
  transform:scaleX(0);
  transform-origin:0 50%;
  background:var(--gold-ink);
}
@keyframes fdeProgress{ from{ transform:scaleX(0) } to{ transform:scaleX(1) } }
@supports (animation-timeline: scroll()){
  @media (prefers-reduced-motion: no-preference){
    .fde-progress__bar{
      animation-name:fdeProgress;
      animation-fill-mode:both;
      animation-timing-function:linear;
      animation-timeline:scroll(root block);
    }
  }
}
@media (forced-colors: active){ .fde-progress__bar{ background:CanvasText } }


/* ============================================================================
   17 — STICKY CONTEXT HEADER                              .fde-stickyhead
   On a long chaptered document the reader loses the answer to "what am I
   looking at". A sticky section header keeps the label present without adding
   a second navigation. The craft detail practitioners get right: the header
   has NO rule until it is actually stuck — an always-on rule floating over
   content is the artifact. Detected here with a `view-timeline` on the section
   (an ancestor, so no `timeline-scope` and no JS sentinel is needed).

   CONTRAST: label is --gold-ink on --bg = 6.34:1. The header sits on an opaque
   --bg fill so text under it never bleeds through.

   HTML: <section class="fde-stickyhead-scope">
           <header class="fde-stickyhead">
             <p class="fde-label fde-optical-label">the record</p>
             <i class="fde-stickyhead__rule" aria-hidden="true"></i>
           </header>
           …
         </section>
   ========================================================================== */
.fde-stickyhead-scope{ position:relative }
.fde-stickyhead{
  position:sticky;
  inset-block-start:0;
  z-index:30;
  background:var(--bg);
  padding-block:.75rem;
  display:block;
}
.fde-stickyhead__rule{
  display:block;
  block-size:1px;
  inline-size:100%;
  margin-block-start:.6rem;
  background:linear-gradient(90deg, var(--fde-hair-strong), transparent 88%);
  opacity:1;
}
@keyframes fdeStuck{ from{ opacity:0 } to{ opacity:1 } }
@supports (animation-timeline: view()){
  @media (prefers-reduced-motion: no-preference){
    .fde-stickyhead-scope{ view-timeline-name:--fde-head; view-timeline-axis:block }
    .fde-stickyhead__rule{
      opacity:0;
      animation-name:fdeStuck;
      animation-fill-mode:both;
      animation-timing-function:linear;
      animation-timeline:--fde-head;
      animation-range:entry 100% contain 2%;
    }
  }
}
@media (forced-colors: active){
  .fde-stickyhead__rule{ background:none; border-block-start:1px solid CanvasText; opacity:1 }
}


/* ============================================================================
   18 — VIEW TRANSITIONS                                         .fde-vt-*
   Two pages that share a headline and a portrait should morph between them,
   not blink. Practitioners adopted this the moment it went cross-document
   because it is the only navigation polish that costs literally nothing at
   runtime and degrades to a hard cut everywhere it is unsupported.

   INERT BY DESIGN: assigning a `view-transition-name` does nothing until a
   transition actually runs, and this file deliberately does NOT ship the
   opt-in. To activate, paste this ONE line into the page's own stylesheet:
       @view-transition { navigation: auto; }
   Only `fde-`-prefixed names are styled below, so nothing here can capture or
   alter a transition the page sets up for its own elements.

   CONTRAST: not computable. The intermediate frames are a UA-interpolated
   cross-fade between two snapshots, not a colour pair I can name. Both END
   states are ordinary --ink/--bg text and are covered by the ratios above.

   HTML (identical class on BOTH pages):
     <h1 class="fde-vt-title">…</h1>
     <figure class="fde-vt-hero">…</figure>
   ========================================================================== */
@supports (view-transition-name: none){
  .fde-vt-title{ view-transition-name:fde-title; contain:layout }
  .fde-vt-hero { view-transition-name:fde-hero;  contain:layout }
}
::view-transition-group(fde-title),
::view-transition-group(fde-hero){
  animation-duration:.42s;
  animation-timing-function:var(--fde-ease);
}
::view-transition-old(fde-title),
::view-transition-new(fde-title),
::view-transition-old(fde-hero),
::view-transition-new(fde-hero){
  animation-duration:.34s;
  animation-timing-function:var(--fde-ease);
  mix-blend-mode:normal;
}
@media (prefers-reduced-motion: reduce){
  ::view-transition-group(fde-title),
  ::view-transition-group(fde-hero),
  ::view-transition-old(fde-title),
  ::view-transition-new(fde-title),
  ::view-transition-old(fde-hero),
  ::view-transition-new(fde-hero){ animation:none !important }
}


/* ============================================================================
   19 — DENSE SPEC LIST, NO CARD CHROME                           .fde-spec
   The card is a container for people who have not decided what the hierarchy
   is. A definition list on a subgrid gives you a real second column that stays
   aligned across every row — including rows whose term wraps — with one
   hairline per row and no box, no shadow, no padding tax. This is the single
   densest way to present evidence a manager is scanning in 90 seconds.

   CONTRAST: term --gold-ink 6.34:1, definition --ink 16.90:1, row rule is
   decorative at 1.31:1 and carries no information the text does not.

   HTML: <dl class="fde-spec">
           <div class="fde-spec__row"><dt>deployed</dt><dd>…</dd></div>
           <div class="fde-spec__row"><dt>stack</dt><dd>…</dd></div>
         </dl>
   ========================================================================== */
.fde-spec{
  display:grid;
  grid-template-columns:minmax(6rem, 11rem) 1fr;
  margin:0;
  padding:0;
  font-variant-numeric:tabular-nums lining-nums slashed-zero;
}
.fde-spec__row{
  display:grid;
  grid-column:1 / -1;
  grid-template-columns:subgrid;
  column-gap:var(--sp-3, 1.4rem);
  padding-block:.7rem;
  border-block-end:var(--fde-hair-w) solid var(--fde-hair);
  align-items:baseline;
}
.fde-spec__row:last-child{ border-block-end:0 }
.fde-spec dt{
  font-size:var(--t-label, 14px);
  letter-spacing:.1em;
  text-transform:uppercase;
  color:var(--gold-ink);
  transform:translateY(-.055em);
}
.fde-spec dd{
  margin:0;
  font-size:var(--t3);
  line-height:1.47;
  color:var(--ink);
  text-wrap:pretty;
}
@supports not (grid-template-columns: subgrid){
  .fde-spec__row{ grid-template-columns:minmax(6rem, 11rem) 1fr }
}
@media (max-width:734px){
  .fde-spec{ grid-template-columns:1fr }
  .fde-spec__row{ grid-template-columns:1fr; row-gap:.2rem }
  @supports not (grid-template-columns: subgrid){
    .fde-spec__row{ grid-template-columns:1fr }
  }
}
@media (forced-colors: active){
  .fde-spec__row{ border-block-end:1px solid CanvasText }
}


/* ============================================================================
   20 — ASYMMETRIC EDITORIAL GRID WITH NAMED BLEED LINES        .fde-grid
   The last and highest-leverage of the twenty. A symmetric 12-column grid
   produces symmetric layouts, and symmetric layouts are what "generated" looks
   like. Named lines let a child declare its intent — `grid-column: main`,
   `grid-column: bleed` — instead of counting columns, which is why the same
   grid can carry a full-bleed band and a narrow aside without a single
   layout-only wrapper div.

   The track ratios are deliberately non-round (1.1 / 6.2 / 3.4). A reader
   cannot name why it feels composed; that is the point.

   HTML: <div class="fde-grid">
           <h2   class="fde-col-main">…</h2>
           <p    class="fde-col-main fde-measure">…</p>
           <aside class="fde-col-aside">…</aside>
           <figure class="fde-col-bleed">…</figure>
           <div  class="fde-col-wide">…</div>
         </div>
   ========================================================================== */
.fde-grid{
  display:grid;
  column-gap:var(--fde-gut);
  row-gap:var(--sp-4, 2.2rem);
  grid-template-columns:
    [bleed-start] minmax(var(--fde-gut), 1fr)
    [wide-start]  minmax(0, 1.1fr)
    [main-start]  minmax(0, 6.2fr)
    [main-end]    minmax(0, 3.4fr)
    [wide-end]    minmax(var(--fde-gut), 1fr)
    [bleed-end];
}
.fde-grid > *{ grid-column:main; min-width:0 }
.fde-col-main { grid-column:main }
.fde-col-wide { grid-column:wide }
.fde-col-bleed{ grid-column:bleed }
.fde-col-aside{ grid-column:main-end / wide-end }
.fde-col-lead { grid-column:wide-start / main-end }

/* the bleed child ignores the grid's own gutter */
.fde-col-bleed{ margin-inline:0 }

@media (max-width:900px){
  .fde-col-aside{ grid-column:main }
  .fde-col-lead { grid-column:main }
}
@media (max-width:734px){
  .fde-grid{
    column-gap:0;
    grid-template-columns:
      [bleed-start] var(--fde-gut)
      [wide-start main-start] minmax(0, 1fr)
      [main-end wide-end] var(--fde-gut)
      [bleed-end];
  }
}


/* ============================================================================
   §R — REDUCED-MOTION NEUTRALISATION (second gate)
   Every animated rule above already sits inside
   `@media (prefers-reduced-motion: no-preference)`. This block is the
   belt-and-braces: it names each animated class again and hard-stops it, so a
   future edit that forgets the positive gate still cannot ship motion to a
   reader who asked for none. Deliberately TARGETED — no
   `*{animation:none!important}`, which would also kill the focus-ring
   transition (#8) and every non-motion state in the file.
   ========================================================================== */
@media (prefers-reduced-motion: reduce){

  /* 02 depth */
  .fde-depth__layer{ animation:none !important; transform:none !important; will-change:auto }

  /* 03 pinned scrub — falls all the way back to stacked, readable flow */
  .fde-pin{ min-height:0 !important; view-timeline-name:none }
  .fde-pin__stage{ position:static !important; height:auto !important; display:block !important }
  .fde-beat{
    animation:none !important;
    opacity:1 !important;
    transform:none !important;
    grid-area:auto !important;
    margin-block-end:var(--sp-4, 2.2rem);
    will-change:auto;
  }

  /* 08 focus ring — the ring stays, the fade goes */
  .fde-focus:focus-visible,
  .fde-focus-scope :is(a, button, summary, input, select, textarea, [tabindex]):focus-visible{
    transition:none !important;
  }

  /* 09 hover weight */
  .fde-lift{ transition:none !important; transform:none !important; will-change:auto }
  .fde-lift:hover, .fde-lift:active{ transform:none !important }
  .fde-lift__face{ transform:none !important }

  /* 11 smooth scrolling */
  .fde-scrollskin, .fde-scrollskin--smooth{ scroll-behavior:auto !important }

  /* 15 media wipe */
  .fde-feather img{ animation:none !important; clip-path:inset(0 0 0 0) !important; transform:none !important }

  /* 16 progress — removed, not frozen: a frozen bar would read as 100% */
  .fde-progress{ display:none !important }

  /* 17 sticky header rule — present, never fading in */
  .fde-stickyhead-scope{ view-timeline-name:none }
  .fde-stickyhead__rule{ animation:none !important; opacity:1 !important }
}

/* --- end of catalogue. 20 elements, 0 dependencies, 0 bare-element rules. -- */
