/* =========================================================================
   PELLMELL-STYLE INTRO — PART IV project-screen sequence  v=70
   REVISED beat sequence (2026-07-16) — supersedes Figma 1566:1530 (that node has no
   v3.5 and its order is stale). New loop: hold → corner peel-back + icon scale-up in
   sync → icon reaches thumbnail size as the dark fill (#183027) meets its edges →
   icon→image swap → apt Directors rise → corner frame peels in → thumb+frame push to
   full bleed → scan ring stroke circumnavigates. Old dress-in (tint/blueprint/lines/
   scan-head) is DORMANT, not animated.
   (v=2: explicit .hero-card pointer-events:auto — the card is the pause-on-hover
         surface; the scan layers above are pointer-events:none.)
   (v=3: dark fill #183027; .card-scan-ring added for v3.5.)
   (v=4: thumbnail corners = filled #183027 wedges (.card-apt-corner, corner-N-med.svg),
         replacing the hand-drawn line-work brackets (.card-apt-brackets removed —
         wrong primitive / errant lines). scan-ring removed; dress-in stays live.)
   (v=5: opening lens is now BUILT from the same corner wedges (.card-lens-corner) over
         an #AEF5B7 centre fill (.card-lens-fill) — lens.svg dropped, no apt behind.)
   (v=6: WRONG — resized wedges to 18.44%, producing a plain green square. Reverted.)
   (v=7: lens wedges back to 50% quadrants per Figma 1595:1471 (corners 255.74/511.47
         ≈ 50%). The wedges tiling into the aperture (like lens.svg) IS the lens — correct.)
   (v=8: the lens now reuses the SAME .card-apt-corner wedges that render on the apt
         thumbnail (the .card-lens-corner class would not paint). Same shape, no apt
         image, aperture filled #AEF5B7. Sized 50% in-lens via `.card-lens .card-apt-corner`.)
   (v=9: the opening lens is now ONE supplied asset — assets/lens-sml.svg (Figma
         1546:1441), 128×129, #AEF5B7 rounded-rect + four #183027 wedges, no strokes.
         Retires the assembled ellipse-&-frame element (1560:1895): .card-lens-fill and
         the borrowed `.card-lens .card-apt-corner` sizing are both dropped. The apt
         thumbnail's own wedges (#cardAptCorners) and every shared .card-apt-corner rule
         are UNCHANGED — the corner shape transitions are not in this change's path.)
   (v=10: TWO lens icons, each on its correct screen — they are NOT interchangeable.
         • start (.card-lens)        = Icon-lens-start.svg — OPAQUE #E6F8E9 plate +
           #183027 wedges. Reads as a solid object on the bright green shader field.
         • v3.6 blueprint (.card-scanhead-icon) = Icon-lens-lite.svg (Figma 1563:2086) —
           TRANSLUCENT #E6F8E9 wedges, NO plate, authored for the dark tinted screen.
         `.card-scanhead-icon`'s `background: #E6F8E9` is REMOVED: it backed the opaque
         Icon-lens.svg, but the lite wedges ARE #E6F8E9, so the plate would erase them.
         Its aspect-ratio goes 95.558/96.691 → 1/1 (the lite asset is exactly square).
         Icon-lens.svg and lens-sml.svg are now unreferenced. Timing untouched; the
         corner shape transitions are not in this change's path.)
   (v=11: Icon-lens-lite.svg re-supplied, simplified — the two stroke paths and the
         outer hairline border rect are gone, and the four wedges are now a uniform
         #E6F8E9 @ .75 (was .75/.85). Still 96×96, rx=18, still PLATE-LESS, so v=10's
         `background` removal stands and is still required. Comment-only here; the
         asset ref is bumped to ?v=2 in the html so the new file actually loads.)
   (v=12: .card-scanhead-title ("SCAN YOUR SPACE") font-weight 700 → 400 to match the
         Figma "Sans Regular" (node 1566:1479). Inter is self-hosted variable (100–900),
         so 400 is a true Regular, not a synthesized fallback. Title only; nothing else.)

   INTRO-1 ONLY. This file is loaded solely by index.html, AFTER the
   shared base css/pellmell-intro.css. The shared base (v=19) is OFF LIMITS and
   is byte-identical to ab7256f; v2 (pellmell-intro-rhythm.html) and v3
   (pellmell-intro-v3.html) do NOT include this file, so nothing here reaches
   them by cascade. All Part IV styling lives in this v1-only stylesheet.

   Five beats play INSIDE the settled card, AUTOPLAYED (Q2) when Part III's rise
   completes — see js/pellmell-intro-scan.js. Media (per MEDIUM-BEFORE-APPEARANCE):
     • card fill      = the base's Colir WebGL <canvas.intro-bg> (untouched)
     • corners/peel   = CSS border-radius over a dark layer (a rounding reveal)
     • lens/blueprint = SVG assets, GSAP-driven
     • apt crop       = raster (assets/APT-Lg 1290x602.webp), one asset for thumb + fill
     • line glow      = feGaussianBlur + mix-blend:screen, blur DERIVED (--bp-glow-blur)

   DEFAULT-STATE CONTRACT: every layer starts hidden / at its v3.0 value and is
   revealed only by js. During the rise, and under reduced-motion / no-JS (the js
   early-returns), the card shows the v3.0 green rest EXACTLY as the base did.
   ========================================================================= */

/* The card becomes the positioned stage for the layer stack (base .hero-card
   already clips: overflow hidden + radius 52.8 — unchanged; this only adds the
   containing block, and only in v1). It is ALSO the hover surface for the scan's
   pause-on-hover: every .card-* layer below is pointer-events:none, so a real
   cursor falls through to #heroCard — keep it explicitly hittable so
   mouseenter/mouseleave reliably fire on it. */
.hero-card {
  position: relative;
  pointer-events: auto;

  /* ---- --band-clear: minimum clearance from content's OUTER edge to the rest BAND edge ----
     THE SHARED TOKEN. This is one design quantity spent in two places: here (the scan-head's
     inset below) and in js/pellmell-intro-content-scale.js, which reads it off this element
     as its `MARGIN` — the project screen's guard on how close the title / rail may come to
     the band edge. It was a fixed `20` in that JS and a would-be second literal here; one
     token instead, because a sync hazard you have to remember is worse than a shared value.

     FIXED, NOT DERIVED (per derived-over-fixed). Nothing in the layout implies it: it is a
     comfort judgement about how close content may sit to a cut edge, the same class of value
     as PAD / GAP in the content-scale js, and it inherits their named-fixed status.

     A PERCENTAGE, NOT PX, AND THAT IS THE LOAD-BEARING PART. The band is a percentage of the
     card (--lip-h) and the card is fluid. A px clearance against a percentage band makes the
     resulting inset drift with card width (16.24% @1290 but 16.03% @1492) — which is exactly
     the px-island pattern that produced the title rail's balance failure (see
     --proj-content-inset). One coordinate system. 1.55% == 20px at the 1290 design width. */
  --band-clear: 1.55%;                 /* 20 / 1290 — shared with content-scale.js MARGIN */

  /* ---- --stage-side-inset: the LEFT side element's inset (Part 1's scan-head) ----
     COMMENT CORRECTED 2026-07-27. This block used to claim the token was "read from BOTH
     edges … spent by the LEFT side element (Part 1's scan-head) and the RIGHT one (Part 2's
     title rail)". The rail has not spent it for some time — it is anchored off the card STACK
     (`left: calc(50% + … + var(--rail-gap))`) with a content-driven right edge, which the
     .card-titles block below already records as "an old aspiration the code no longer holds".
     A grep confirms .card-scanhead is now the ONLY consumer. The "one value read twice"
     symmetry described here never survived that change, so it is removed rather than restated.

     DERIVED OFF THE BAND ABOVE 768 (2026-07-27). The base value is the scan-head's measured
     Figma inset, 99/1290 = 7.674% of the card (node 1566:1469) — but that is an inset from the
     CARD edge, and at rest the card is clipped to a band starting at --lip-h (14.6909%). The
     scan-head therefore sat 7.0169% of the card width OUTSIDE the visible window: 90.5px @1440,
     104.7px @1848. "SCAN YOUR SPACE" rendered as "AN / UR / CE", and the 95.558px lens icon was
     94.7% clipped — effectively absent.
     Expressed as a RELATIONSHIP to the band rather than a second literal, so it tracks --lip-h
     if the reveal is ever retuned:

         --stage-side-inset = --lip-h + --band-clear = 16.24% (209.5px @1290)

     FALLBACK 6.124% = 7.674% − 1.55%: if --lip-h is ever absent (no lipscale, so no band and
     nothing to clear) the expression collapses back to exactly the shipped 7.674% Figma inset.

     ACCEPTED FIGMA DEVIATION (user, 2026-07-27): this moves the scan-head ~110px further in
     than node 1566:1469 at the FULL pose too, not only at rest. That is deliberate. The
     alternative — driving the inset off --lip-reveal so the full pose stays exactly on Figma —
     works and would have avoided the deviation, but only by converting the same 110px into
     110px of TRAVEL. There is no motion requirement on this screen (the goal is simply the
     title in view at default), so a static offset was chosen over acquiring motion nobody
     asked for. See Docs/RULES.md. */
  --stage-side-inset: 7.674%;          /* base / ≤768 — 99 / 1290, Figma 1566:1469 */

  /* ---- --rail-gap: THE ONE FREE NUMBER for the title rail ----------------------------
     The clear distance between the Part-2 card stack's right edge and the start of the
     icon+title column. Everything else about the rail's placement is derived (see
     .card-titles); this is the single design value and it is spent once.
     89.7px is what the previous right-anchored placement resolved to at the 1290 design
     width, so the composition is unchanged — the same gap, now named and PRESERVED rather
     than left to close as the card shrinks. */
  --rail-gap: 89.7px;

  /* Composition scale at the small breakpoints — see the media queries. 1 above them. */
  --stage-scale: 1;
}

/* ---- THE BAND-DERIVED INSET, >768 ONLY ------------------------------------------------
   SCOPED BECAUSE ≤768 HAS NO PROBLEM — NOT BECAUSE IT FAILS THERE. Measured 2026-07-27 at
   768×1024 and 480×900: "SCAN YOUR SPACE" and the lens icon both read IN FULL, unclipped.
   At 768 the scaled stage spans viewport 144→608 while the band spans 141→627 — the stage
   sits entirely inside the band, clearing by 46.3px. Applying the derived inset there would
   push the scan-head inward TWICE and break a screen that is currently correct.

   WHY ≤768 IS ALREADY CLEAR, AND WHY THAT IS FRAGILE. --stage-scale shrinks .card-stage about
   the CARD'S CENTRE, so it pulls every stage child inward by (1 − stage-scale) × distance-from-
   centre: 94.6px at 768, against the 48.3px the band needs. The clearance is therefore
   INCIDENTAL — it falls out of a --stage-scale value derived for a completely unrelated reason
   (the measured ceiling that keeps the RAIL's labels inside the card; see the note above the
   ≤768 query near the bottom of this file). Nothing connects the two on screen.
   *** Anyone retuning --stage-scale could silently re-break the blueprint screen at these
   widths, with no visible link back to this rule. Re-check "SCAN YOUR SPACE" at 768 and 480
   after any change to it. *** */
@media (min-width: 769px) {
  .hero-card {
    /* INTERPOLATED off --lip-reveal (2026-07-27) — WAS a static 16.2409%.
       The static value cleared the rest band but cost the composition permanently: it sat
       ~110px inward of Figma at the FULL pose too, and forced the title down to 28.8px. Both
       ends are now spent where they are actually needed:
         reveal 1 (rest) → --sh-inset-rest  16.2409%  — clears the band
         reveal 0 (full) → --sh-inset-full   7.674%   — Figma 1566:1469, exactly
       so the ~110px becomes TRAVEL rather than a permanent offset, and the authored inset is
       restored at the pose the composition is actually read at.

       WHY THE INSET ALONE IS NOT ENOUGH — the trap this nearly shipped wrong. The band and
       blueprint constraints are SIMULTANEOUS at rest, not alternating: the blueprint is inside
       the card, so at rest it is visible within the band and both edges bind at once. At 1440
       clearing the band needs inset ≥ 16.241% while clearing the ink needs ≤ 11.368% — no
       static inset satisfies both, and no interpolation of the inset alone changes that. The
       TITLE SCALE must interpolate on the same channel (see the content-scale css), which is
       what makes the rest end feasible.

       RUNS ON `left`, NOT `transform` — js/pellmell-intro-scan.js owns #cardScanhead's
       transform and rewrites it every loop iteration, so this channel is free. --lip-reveal
       already inherits. COST: `left` is a layout property, so this reflows the scan-head's
       subtree (one icon, three text lines) on each scrubbed frame — contained, but real, and
       unlike the composited transforms the project screen uses.
       NO-LIPSCALE FALLBACK: --lip-h absent → --sh-inset-rest resolves to 6.124% + 1.55% =
       7.674% = --sh-inset-full, so both ends collapse to the authored Figma inset and the
       reveal factor becomes irrelevant. */
    --sh-inset-full: 7.674%;                                          /* Figma 1566:1469 */
    --sh-inset-rest: calc(var(--lip-h, 6.124%) + var(--band-clear));  /* 16.2409% */
    --stage-side-inset: calc(var(--sh-inset-full)
                             + var(--lip-reveal, 0)
                               * (var(--sh-inset-rest) - var(--sh-inset-full)));
  }
}

/* SCAN STAGE — the wrapper the whole lens/apt/wedge composition lives in. Unscaled here;
   the small breakpoints scale it. inset:0 makes it the card's own box, so every child's 50%
   resolves exactly as before and js keeps deriving every value from the UNSCALED card: the
   grow/rise/peel keep their proportions and only the final box is smaller. */
.card-stage { position: absolute; inset: 0; }

.card-dark, .card-green, .card-lens, .card-apt,
.card-tint, .card-blueprint, .card-lines, .card-scanhead {
  position: absolute;
  pointer-events: none;
}

/* v3.1 GROUND — the dark fill the peel reveals. #183027 (user-specified, supersedes
   the earlier #14231D guess) — the same dark as the lens/Icon-lens asset, so the
   icon and the fill share one ink and the fill-meets-icon-edge moment reads as a
   continuous surface. */
.card-dark {
  inset: 0;
  background: #183027;
  opacity: 0;
}

/* v3.0/v3.1 GREEN — wraps the base WebGL canvas; PEELS by rounding its radius up
   (js) into a horizontal stadium, so .card-dark shows through the corners. */
.card-green {
  inset: 0;
  border-radius: 10px;                 /* == the card's rest radius (52.8 -> 10, user 2026-08-24); js grows it on peel */
  overflow: hidden;
  background: #7ba587;                  /* same fallback as the base card */
  will-change: border-radius;
}
.card-green .intro-bg { display: block; width: 100%; height: 100%; }

/* v3.2 LENS — the container. 9.877% of card width == 127.41 / 1290 (Figma lens
   1566:1540); js sizes it to lensSize and fades it in at full scale (no grow). The
   graphic is one self-contained asset (see below). */
.card-lens {
  left: 50%; top: 50%;
  width: 9.877%;
  aspect-ratio: 1 / 1;
  transform: translate(-50%, -50%);
  opacity: 0;
  will-change: opacity;
}
/* Lens graphic = assets/Icon-lens-start.svg: a single 96×96 SVG carrying an OPAQUE
   #E6F8E9 rounded-rect (rx=18 baked in, so no CSS radius) and its four #183027 wedges.
   Nothing assembled here — no .card-lens-fill, no borrowed wedges. Do NOT swap the
   v3.6 Icon-lens-lite.svg in here: that one is the translucent, plate-less treatment
   for the dark blueprint screen, and it would wash out on this bright green field.
   The .card-apt-corner rules further down are the APT THUMBNAIL's and stay untouched. */
/* The lens artwork is a GENERATED inline <svg> (js buildLensSvg) — one petal rotated
   90·n, congruent by construction — not an <img> asset. It fills the lens box exactly;
   the viewBox does the scaling. The apt corner wedges are untouched and stay <img>. */
.card-lens svg { display: block; width: 100%; height: 100%; }

/* THE MATEY MARK INSIDE THE LENS (2026-07-28) — the closing lens's payload.
   PORTED from INTRO-3's .feed-tlens-mark, same asset (assets/Logo-mark-matey.svg) and the
   same 40% proportion: Figma puts mark 1723:1472 (128) dead centre of Icon-lens-lg 1715:2128
   (320), so 128/320 = 40% of whatever box the lens occupies. INTRO-1's lens box is `thumb`
   (js), so the mark tracks the lens at every size with no second measurement.
   Being a CHILD is the whole point — it inherits the lens's scale AND its opacity, so it
   grows with the closing lens and leaves with it when the lens crossfades under the landing
   apt. That is what answers "what happens to the mark before the apt slides on": nothing of
   its own; it rides the lens out. The only motion it owns is the spin (js MARK_SPIN_*).
   Its own opacity is gated in js, from timeline CALLBACKS — #cardLens is reused for the
   cold-start prologue lens and the mid-sequence SEAM lens too, and the mark belongs to the
   CLOSING lens alone. See THE MARK'S VISIBILITY in js/pellmell-intro-scan.js.
   DOM order puts it after the generated <svg>, so it paints over the petals. */
.card-lens-mark {
  position: absolute;
  left: 50%; top: 50%;
  width: 40%;                          /* 128 / 320 — Figma 1723:1472 in 1715:2128 */
  aspect-ratio: 1 / 1;
  transform: translate(-50%, -50%);    /* no-JS fallback centring; js takes over via
                                          xPercent/yPercent + explicit x/y — the same
                                          double-centring guard every other layer here uses */
  will-change: transform;              /* it carries its own rotationY spin */
}
.card-lens-mark img { display: block; width: 100%; height: 100%; }

/* v3.3/v3.4 APT — assets/APT-Lg 1290x602.webp, object-fit cover. ONE raster serves the
   risen thumbnail AND the full-card fill. js measures the live card and drives
   width/height/clip/y between the two states. Centred via translate. */
.card-apt {
  left: 50%; top: 50%;
  transform: translate(-50%, -50%);
  overflow: hidden;
  border-radius: 16px;
  opacity: 0;
  will-change: width, height, transform, opacity;
}
.card-apt img { display: block; width: 100%; height: 100%; object-fit: cover; }
/* v3.3 viewfinder brackets on the thumbnail (Figma Image>corners, 63.9px).
   Authored inline (no asset exported); fade out as the thumb expands to fill. */
/* v3.4 corner WEDGES — filled #183027 quadrant-with-concave-arc (assets/corner-N-med.svg),
   the REAL primitive (was hand-drawn line-work). 64px each → 18.44% of the thumbnail
   (64 / 347.4). Each sits AT its corner; js scales it in from that outer corner. The
   SVG's #183027 is the same value as the peel fill (.card-dark). */
.card-apt-corners { position: absolute; inset: 0; }
.card-apt-corner  { position: absolute; width: 18.44%; height: 18.44%; display: block; }
.card-apt-corner--1 { top: 0;    left: 0;  }   /* TL */
.card-apt-corner--2 { top: 0;    right: 0; }   /* TR */
.card-apt-corner--3 { bottom: 0; right: 0; }   /* BR */
.card-apt-corner--4 { bottom: 0; left: 0;  }   /* BL */

/* v3.4 TINT — flat dark wash over the apt (Figma TINT = rgba(0,0,0,0.75)). */
.card-tint {
  inset: 0;
  background: rgba(0, 0, 0, 0.75);
  opacity: 0;
  will-change: opacity;
}

/* v3.4 BLUEPRINT — assets/blueprint-intro.svg (1290×573). Rendered TWICE, mirroring
   the phone's scan-blueprint-outlines: a screen-blended BLUR copy underneath (the
   glow) + a sharp copy on top. Fills the card; alignment-to-apt is an on-device
   confirm item. */
.card-blueprint { inset: 0; opacity: 0; will-change: opacity; }
/* ONE FIT RULE, SHARED. All three dress assets are re-exported on the SAME 1290x602 frame
   (Figma 1676:1569), so giving them the same box and the same fit reproduces the apt photo's
   mapping exactly — same scale, same crop, same origin. No offsets anywhere.

   COVER — RESTORED 2026-07-28, reverting a same-day pass to `contain`. That pass removed the
   9.6% zoom by fitting the whole 1290x602 frame inside the card, which was BACKWARDS for this
   beat: the intent has always been to CLIP LEFT AND RIGHT and scale to fill the HEIGHT, and the
   side crop is accepted. cover does exactly that — at the corrected 1203.973x660 card it is
   height-driven, scale 1.09635, filling the height and taking 105.16px off each side.
   (contain would have scaled 0.93331 and left 40px bands left and right instead.) */
.card-blueprint img { display: block; width: 100%; height: 100%; object-fit: cover; }
/* GLOW copy. --bp-glow-blur is set by buildScan() to 0.345052 × (card-width /
   234.5) — the phone's #bp-glow stdDeviation re-derived from the MEASURED card,
   so it tracks the card rather than freezing at the 1440 value (1.898px, the
   fallback if js hasn't run). Screen-blend per pinned 664:2103. */
.card-blueprint--glow img { filter: blur(var(--bp-glow-blur, 1.898px)); }
.card-blueprint--glow { mix-blend-mode: screen; }

/* v3.4 MEASUREMENTS — assets/Measurements-intro.svg (818×571): the six pale lines
   + baked glow (Figma stdDeviation 1.52297) + outlined-vector labels. Placed at
   the Figma centre offset (≈47.9% / 52.0%), width 62.99% of card, natural aspect.
   NOTE: the asset exports the lines as FILLED outlines, not stroked paths, so the
   phone's strokeDashoffset per-line draw cannot attach — this reveals the whole
   overlay (js). Per-line draw is held pending a stroked re-export. */
/* SAME BOX, SAME FIT as the apt and the blueprint — the third layer of the shared frame.
   The re-export is a true 1290x602 (clip-content on, so the arrowhead glow no longer grows
   the canvas), which retires two things at once: the old hand placement (left 47.9%,
   top 52.0%, width 62.99%, aspect 818/571 — a tight crop of just the line cluster), and the
   667/602 frame workaround the 607-tall export needed. Identical intrinsic size + identical
   box + identical object-fit = identical mapping, with nothing per-layer left to drift. */
.card-lines {
  inset: 0;
  opacity: 0;
  will-change: opacity, clip-path;
}
/* The lines are a GENERATED inline <svg> (js buildMeasureSvg), not the asset. Its
   preserveAspectRatio="xMidYMid slice" is the SVG equivalent of object-fit: cover, so it keeps
   the same 1290x602 mapping as the apt and blueprint. */
.card-lines svg { display: block; width: 100%; height: 100%; }

/* v3.4 SCAN-HEAD — Icon-lens.svg + "SCAN YOUR SPACE" (Figma Icon+title 1566:1469).
   Left = --stage-side-inset (99/1290; see .hero-card), vertically centred — the same
   token the Part-2 title rail spends on the right. Icon guarded with max-width so it
   can't overrun a narrow card. Font: Inter (base's only self-hosted face) stands
   in for Figma's Nunito. */
.card-scanhead {
  left: var(--stage-side-inset); top: 50%;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  gap: 24px;
  opacity: 0;
  will-change: opacity, transform;
}
/* v3.6 blueprint-screen lens = assets/Icon-lens-lite.svg. NO background here, by
   necessity: the lite icon's wedges are themselves #E6F8E9, so the old
   `background: #E6F8E9` plate (which existed to back the OPAQUE Icon-lens.svg) would
   paint the icon in its own colour and erase it. The lite asset is authored for dark
   ground — .card-tint's rgba(0,0,0,.75) over the apt — and is four bare translucent
   wedges: no plate, no strokes, carrying only its own rx=18 clip.
   border-radius/overflow stay as a clip guard. */
.card-scanhead-icon {
  width: 95.558px;                     /* Figma 1566:1518 */
  max-width: 32%;                      /* structural guard against a small card */
  aspect-ratio: 1 / 1;                 /* Icon-lens-lite is exactly 96×96 (was 96×97) */
  border-radius: 18px;
  overflow: hidden;
}
.card-scanhead-icon img { display: block; width: 100%; height: 100%; }
.card-scanhead-title {
  font-weight: 400;                    /* Figma "Sans Regular" (node 1566:1479) — was 700.
                                          Inter is self-hosted variable (100–900), so 400 is a
                                          true Regular weight, not a synthesized fallback. */
  font-size: 48px;                     /* Figma 48px; responsive-phase item */
  line-height: 1.0;
  letter-spacing: 0.01em;
  color: #E6F8E9;
  text-transform: uppercase;
}

/* ===================== PROJECT TITLE BLOCK (Matey logo + tagline) =====================
   Figma 1709:1942 "title" — the Matey wordmark over "DIY made easy", left side of the card.
   INTRO-1 ONLY (scan.css is loaded only by index.html), so this @font-face and the
   block below never reach v1/v2/v3.

   Nunito is self-hosted here for the SAME CDN-block robustness the base uses for Inter and
   Satoshi — the main site loads Nunito via Google Fonts, but the intro prototypes deliberately
   do not touch an external font host. Only the Light (300) latin subset is vendored (the
   tagline is all basic latin); assets/fonts/nunito-light.woff2 is the exact Google Fonts
   latin block. */
@font-face {
  font-family: 'Nunito';
  font-style: normal;
  font-weight: 300;
  font-display: swap;
  src: url('../assets/fonts/nunito-light.woff2') format('woff2');
}

/* LEFT ↔ RIGHT BALANCE. --proj-content-inset is the shared card inset the left title spends so
   its left edge sits the SAME distance in from the card's left as the RAIL's right edge sits in
   from the card's right — the two balanced against the card.
   VALUE IS THE RAIL'S REAL RIGHT EDGE, not the scan-head's --stage-side-inset (7.674% / 99px):
   the rail is anchored off the card STACK and sized max-content, so its right edge is
   content-driven, not a nominal inset. The title is matched to it per the user's balance call
   (2026-07-22).

   NOW DERIVED AT RUNTIME, NOT A LITERAL (2026-07-27). This was `9.87%`, a constant measured
   once on a 1276.5-wide card. It could only ever be right at that one width, because the two
   sides are in different coordinate systems: the TITLE's inset is a percentage (it tracks the
   card) while the RAIL's is px-derived (--rail-gap 89.7px + a px-wide column). The card is now
   fluid — min(89.58vw, 1492px) — so at 1492 the rail's real inset is ~215px against the title's
   147px: a 68px imbalance, visible on any display past ~1440.
   js/pellmell-intro-content-scale.js measures the rail's actual right edge and writes this
   property ABOVE 769px, so the balance holds at every card width in that range and the token
   finally does what this comment always claimed. Live values: 9.871% @1440 (card 1290),
   14.813% @1848 (card 1492) — the drift the constant could not express.
   THE LITERAL BELOW IS UNCHANGED AT 9.87% ON PURPOSE. It is now the fallback for the two
   cases the js does not write: no-JS, and ≤768 (where deriving off the rail is wrong — the
   rail is re-anchored into the --stage-scale freed space there rather than balanced against
   the card; see css/pellmell-intro-content-scale.css). Keeping the old literal leaves both of
   those poses byte-identical to what shipped, rather than moving them ~2px as a side effect.

   ROOT CAUSE, DEFERRED: the rail is the only px island in a composition authored in
   percentages of the card, and that one fact drives this imbalance, the 2026-07-20 clipping,
   and the two-coordinate-systems problem in the content-scale tween. Removing it means
   card-relative type scaling above 768 — which puts the rail's type at 27.8px on a 4K display,
   a visible change to a shipped composition. Its own ticket; logged to TODO. --rail-gap stays
   px deliberately: holding that gap is exactly what the 2026-07-20 pass bought. */
.hero-card { --proj-content-inset: 9.87%; }    /* fallback only: no-JS, and ≤768 */

.card-projtitle {
  position: absolute;
  left: var(--proj-content-inset);     /* balances the rail's right inset */
  top: 50%;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  align-items: flex-start;             /* LEFT-ALIGNED — logo + tagline share one left edge
                                          (user 2026-07-22, overrides the comp's centred stack) */
  gap: 10px;                           /* Figma gap-[10px] */
  pointer-events: none;
  opacity: 0;                          /* DEFAULT-STATE CONTRACT — the scan timeline fades it in
                                          with the first rail item (see the js DRESS_IN reveal) */
}
.card-projtitle-logo {
  display: block;
  width: 148.548px;                    /* Figma Logo 148.548 wide */
  height: 48px;                        /* the wordmark's own height (assets/Logo-matey-lite.svg
                                          is 149x48; the node's 68 frame is 48 glyph + 10 pad
                                          top/bottom, and the pad is absorbed by the gap) */
}
.card-projtitle-tag {
  font-family: 'Nunito', system-ui, sans-serif;
  font-weight: 300;                    /* Nunito Light */
  font-size: 40px;                     /* Figma 40px */
  line-height: 1.0;
  color: #EBFDED;                      /* Figma #EBFDED */
  white-space: nowrap;                 /* Figma nowrap */
}

/* ---- MATEY MARK, stacked on the title block (user 2026-07-28) -------------------------
   WAS a free-standing .card-brand at the card's top-left corner (ported from INTRO-3's
   .feed-brand, Figma 1736:1513). It is now the first item in .card-projtitle's column, on
   the "SCAN YOUR SPACE" pattern — the same stacked relationship .card-scanhead-icon has to
   .card-scanhead-title. The corner rules are retired; INTRO-3's .feed-brand is UNTOUCHED,
   the two pages no longer share this pose.

   SIZE IS UNCHANGED at 61.703px, the corner pose's rendered width (4.7826% of the 1290
   design card), so the glyph still lands at the node's 34.24. It is px here, not a
   percentage, because everything else in this block is px (148.548 wordmark, 48/40 type) —
   the block scales as a unit via --cs-scale and --stage-scale, so nothing is lost.

   BOTH OFFSETS CANCEL THE ASSET'S OWN PADDING. The GLYPH, not the element box, is what has to
   align and what the gap has to be measured from — the same correction the corner pose made
   with its 14.03 pull-back; only the two edges it applies to have changed.

   *** THE ASSET IS NOT SYMMETRIC. *** MEASURED with getBBox on the real file, not transcribed:
   ink x 29.102 y 29.867 w 68.490 h 70.282 in the 128 box, so
       left 22.7356%   right 23.7567%   top 23.3338%   bottom 21.7586%
   — four different numbers. The long-standing note "glyph 71.05 of a 128 box, inset 29.10"
   describes only the LEFT inset correctly; 29.10 is not the bottom one, and 71.05 is neither
   the measured width (68.49) nor the height (70.28). Assuming one padding for all four edges
   costs 0.6px on the vertical gap, which is exactly the number this block exists to control,
   so each edge below spends its OWN measured inset.
   The element WIDTH stays 61.703px regardless — that is the corner pose's rendered size and
   the brief moves the mark, it does not resize it. (Its glyph therefore renders 33.02 × 33.88,
   not the 34.24 the corner comment claimed; that discrepancy is inherited, not introduced.) */
.card-projtitle-mark {
  display: block;
  width: 61.703px;                     /* == the corner pose's 4.7826% of the 1290 design card */
  aspect-ratio: 1 / 1;
  /* COLOUR (user 2026-07-28): #EBFDED — the tagline's own colour, so the three items in this
     block finally share one ink. The asset paints #1E2E29 and cannot be recoloured through an
     <img>, so the element is a MASKED SPAN: the svg supplies the alpha, the background supplies
     the colour. The file is untouched and stays the single source (INTRO-3 loads the same one).
     Alpha masking flattens the asset's two fills into one — see the note below.
     Bounds are unaffected: the mask uses the same box the <img> did, so every measured offset
     above still holds. */
  background-color: #EBFDED;
  -webkit-mask-image: url('../assets/Logo-mark-matey.svg?v=1');
          mask-image: url('../assets/Logo-mark-matey.svg?v=1');
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  /* glyph's LEFT edge onto the wordmark's, against .card-projtitle's align-items: flex-start */
  margin-left: -14.029px;              /* 61.703 × 0.227356 (measured left inset) */
  /* GLYPH BOTTOM → WORDMARK TOP = 24px, .card-scanhead's gap — the stacked relationship the
     lens icon has to "SCAN YOUR SPACE". The column's own 10px gap and the asset's 13.426
     bottom pad supply most of it; this is the remainder. Written as the subtraction, not as
     the resulting 0.574, so the 24px survives a change to the gap or the box size. */
  margin-bottom: calc(24px - 10px - 13.426px);   /* 61.703 × 0.217586 (measured bottom inset) */
}
/* THE ASSET IS TWO-COLOUR, AND THE MASK FLATTENS IT. Logo-mark-matey.svg is three #1E2E29
   paths plus ONE `fill="white"` path — a highlight dot punched into the tape-measure detail at
   93.5,88.8, r≈1.85 of the 128 box. An alpha mask keeps every painted pixel regardless of its
   fill, so that dot now renders #EBFDED with the rest and the highlight is gone.
   That is a real loss and it is accepted deliberately: at this element's 61.703px the dot is
   0.89px across — below a device pixel at 1× and invisible at the rest scale (0.65) the block
   actually renders at. Preserving it would mean inlining a recoloured copy of the svg into the
   markup and forking it from the file INTRO-3 shares. If the mark is ever enlarged enough for
   0.89px to matter, that trade has to be revisited — inline it then, and give the dot its own
   fill. Measured from the file, not transcribed. */

/* ===================== PART 2 — card sequence =====================
   The Matey app cards (card-*.svg, 330×516) at FULL card size, centred on the green —
   the Part-2 equivalent of the Part-1 lens icon. Sized by HEIGHT (85% of the card) with
   the SVG's own 330/516 aspect, so it's the biggest portrait card that fits with margin.
   js grows it from a dot to scale 1 (the "small thumbnail → full card" open) and centres
   it via translate. pointer-events off like the other scan layers. */
.card-p2 {
  position: absolute;
  left: 50%; top: 50%;
  height: 100%;                        /* "full size" = fills the card height; js opens the
                                          Part-2 shop card at thumb/ch of this (the lens's
                                          scaled-up resting height, aspect preserved — see
                                          scan js cardStart). */
  aspect-ratio: 330 / 516;
  transform: translate(-50%, -50%);
  opacity: 0;
  pointer-events: none;
  will-change: transform, opacity;
  /* CORNERS + STACK COVER (Figma 1629:3282): CSS owns the silhouette so all four cards
     are identical. Every card is the SAME 330×516 box at the SAME slot + scale, so a
     single uniform radius + clip makes the four corners equal AND makes the front card
     exactly cover the ones behind — no card peeks past another at the rounded corners
     (the per-SVG rx varied: make bled dark-green to a hard corner while shop/save/plan
     carried a light-green frame). Radius is in the element's own box; the GSAP scale
     carries it down proportionally, so it reads the same through the open + at rest.
     10px here ≈ 5px at the resting scale — comfortably past any per-SVG rx so the clip,
     not the art, defines the corner. Tunable on device. */
  border-radius: 10px;
  overflow: hidden;
}
.card-p2 img { display: block; width: 100%; height: 100%; }

/* ===================== PART 2 — side title rail =====================
   Persistent 4-row menu at the right of the card stage (Figma 1629:3282). Sits in
   #heroCard's coordinate space, vertically centred. Rows are 96px tall; each is a flex
   box with a 24px gap: NAV ICON FIRST, then the label stack. Labels: Satoshi 24px #ebfded.
   Both labels (short + active) stack in one grid cell and crossfade — the grid track
   sizes to the widest, and z-order is irrelevant since only opacity moves. The rail
   paints above the card layers but takes no pointer events.

   LEFT-ANCHORED OFF THE CARD STACK (see the .card-titles rule below) — the earlier
   right-edge anchoring is retired. So:

     width  = max-content  → the rail's box IS its widest row (icon + the longest active
                             label), rather than a nominal 144px every active label
                             overflowed unmeasured
     right edge = CONTENT-DRIVEN → because the box is left-anchored and sized max-content, its
                             right edge lands wherever the widest label ends: 131.6px (10.20%)
                             in from the card's right at 1440 — NOT --stage-side-inset's 99px
                             (an old aspiration the code no longer holds). Being content-driven
                             it does NOT track the card: at 1492 it is ~215px. That is why
                             --proj-content-inset is measured off this edge at runtime rather
                             than copied as a constant — see its block above.

   That is also where the PADDING BETWEEN THE CARDS AND THE LABELS comes from: the rail is
   pushed off the right edge by the stage inset, and the card stack is centred, so the gap
   between the two is derived at every viewport instead of being a literal left offset that
   only held at 1290. Rows stay left-aligned to each other, so the block moves as a unit. */
.card-titles {
  position: absolute;
  /* ANCHORED OFF THE CARD STACK, NOT THE CARD'S EDGE, so the icon-to-card gap is the
     quantity that HOLDS as the viewport narrows. Right-anchoring kept the rail a fixed %
     off the card's right edge, but the rail's width is mostly fixed px (40 icon + 24 gap +
     16 slot padding + 24px type) while the stack scales with the card — so the gap closed
     and the column crowded the cards.
     The stack's half-width is DERIVED: each Part-2 card renders at height `thumb`
     (cw x 0.2693) with the card art's 330/516 aspect, so its width is cw x 0.2693 x 330/516
     and its half-width is 8.611% of the card. That chain is written out below so it tracks
     any change to thumb or the card aspect. At the 1290 design width this lands within
     ~0.2px of where the right-anchor put it — same composition, different invariant. */
  left: calc(50% + (26.93% * 330 / 516 / 2) + var(--rail-gap));
  top: 50%;
  transform: translateY(-50%);
  width: max-content;                  /* the rail IS icon + its longest label */
  z-index: 5;                          /* above the Part-2 cards */
  pointer-events: none;
}
/* Nav icon slot (Figma nav set, 40×40) — the first child of each row, filling the slot the
   24px gap was already reserving. shop/save carry their own glyph; plan and make both carry
   the play glyph (the supplied set has no dedicated plan or make icon). flex: 0 0 40px keeps
   the icon column a fixed track that a long title can't squeeze, so all four labels share
   one left edge. Default (translucent #AEF5B7 @ .25 plate) variant only — the -active
   (solid #42604D) files are in the repo but UNREFERENCED here on purpose: wiring a
   default↔active state would be new motion, which this change deliberately has none of. */
.ct-icon {
  flex: 0 0 40px;
  width: 40px;
  height: 40px;
  opacity: 0;                          /* NEVER ICON-ALONE — see below */
  display: grid;                       /* both variants share ONE cell — see below */
}
/* FULL ASSET SWAP, NOT A RECOLOR. Each row carries BOTH supplied files:
     .ct-icon--default  translucent plate  #AEF5B7 @ .25   — row is not the primary card
     .ct-icon--active   solid plate        #42604D         — row IS the primary card
   Same glyph, same 40×40 box, only the plate differs, so the swap reads as the pill
   lighting up with no shift. They stack in one grid cell and crossfade in place: the cell
   can't reflow, z-order is irrelevant (only opacity moves), and the slot stays 40px wide
   whichever variant is up. The variants' opacities are INSIDE the slot, whose own opacity
   is the icon's paired-with-its-title reveal — the two multiply, so a row that hasn't
   entered yet shows neither variant. */
.ct-icon-img {
  grid-area: 1 / 1;
  display: block;
  width: 100%;
  height: 100%;
}
.ct-icon-img > img { display: block; width: 100%; height: 100%; }
/* Default up, active down at rest. The scan timeline owns both from here, so reduced-motion
   / no-JS shows the default pills under an empty rail and never a half-lit one. */
.ct-icon--default { opacity: 1; }
.ct-icon--active  { opacity: 0; }
/* THE ICON IS PAIRED WITH ITS TITLE. It starts hidden here for the same reason the labels
   do (the DEFAULT-STATE CONTRACT: js owns every reveal), and the scan timeline fades it in
   on its row's OWN entrance beat, with the same duration and ease as that row's active
   title — so the two arrive together and the icon is never up on its own. Without this
   `opacity: 0` the icon painted from first frame while its label was still hidden, which
   showed the whole rail as four bare icons until the cards started landing. */
.card-title-row {
  --ct-gap: 24px;                      /* icon → label, read again by .ct-alt */
  display: flex;
  align-items: center;
  gap: var(--ct-gap);                  /* icon → label — token, so .ct-alt tracks it */
  height: 96px;
  padding-left: 16px;
}
/* ONE title per row, static. The two-label rail (a short default crossfading to an
   expanded active as each card entered, then demoting on the next card's beat) is gone —
   second label set, the grid cell they shared, and the swap all removed. A row's text is
   fixed at authoring time; only its opacity moves. */
/* LONG vs SHORT TITLES. Both strings live in the row; the breakpoint chooses which one is
   in flow. They are children of .ct-label, NOT siblings of it, so the timeline still fades a
   single .ct-label per row and nothing about the motion changes — see the media query at the
   foot of this file for the breakpoint and how it was derived. */
.ct-t--short { display: none; }
.ct-alt      { display: none; }   /* only in flow at <=768 — see the marker block */

.ct-label,
.ct-alt {
  font-family: 'Satoshi', 'Inter', system-ui, sans-serif;
  font-weight: 300;                    /* only the Light weight is self-hosted */
  font-size: 24px;
  line-height: 1;
  letter-spacing: 0.02em;
  color: #ebfded;
  white-space: nowrap;
  opacity: 0;                          /* starts HIDDEN — see below */
}
/* The rail BUILDS UP progressively (Figma 1641:1638): a row's title only appears once
   its card has entered — the newest card's row shows its ACTIVE (expanded) label, rows
   whose cards already landed drop to their DEFAULT (short) label, and rows whose cards
   haven't entered show nothing. The scan timeline owns every opacity from here (gsap.set
   at rest + per-card tweens). Reduced-motion / no-JS therefore shows an EMPTY rail, which
   matches the card-less static hero those paths fall back to. */


/* ===================== RAIL: SHORT TITLES BELOW THE CLIP POINT =====================
   The rail holds its gap to the card stack (see --rail-gap), which means its RIGHT edge is
   what gives way as the viewport narrows: past a certain width the column runs off the card
   and .hero-card's overflow:hidden cuts the titles. The long titles are what make it wide, so
   below that point the rail swaps to the short set and fits again.

   WHERE 1100px COMES FROM — measured, not chosen:
     rail width, long titles ............ 312.6px   (16 padding + 12 bullet + 24 gap + type)
     rail width, short titles ........... 116.5px
     rail's left anchor ................. 58.611% of the card + --rail-gap
     so it fits while ................... gap + railWidth <= (1 - 0.586113) * cardWidth
     min card width, long ............... (89.7 + 312.6) / 0.413887 = 971.98px
     card is --content-width (89.58%) of the hero, and the hero is the viewport less the
     15px scrollbar-gutter reserve, so ... 971.98 / 0.8958 + 15 = 1100.2px viewport
   1100px is that clip point rounded down to the whole pixel; the query fires at 1099px and
   below. The same arithmetic puts the SHORT set's clip point at 571.2px, so the short titles
   have ~528px of headroom below the swap — they are not themselves at risk here.

   CORRECTED 2026-07-27. These widths costed a 40px ICON, but the >768 block below
   (min-width:769px) replaces that plate with a 12px BULLET — so every long-title width here
   was 28px too wide and the clip point 76px too early: the rail was swapping to short labels
   at 1175px while the long set still fit down to 1100px. The 40px figures were correct when
   written and drifted when the bullet shipped; the bullet rule itself is right and untouched.
   Re-derive if --rail-gap, the type size, or the icon column changes. */
@media (max-width: 1099px) {
  .ct-t--long  { display: none; }
  .ct-t--short { display: inline; }
}

/* ===================== DESKTOP RAIL BULLET (>768) =====================
   Replaces each row's 40×40 nav-icon plate with the feed-style square marker ported from
   INTRO-3's .fr-marker: 12×12, radius 2, #42604D (the active-plate colour — NOT feed's
   #1e2e29). Scoped to min-width:769px so the ≤768 block (which draws its own #42604D
   background square) is untouched and the two never stack.

   REVEAL is INHERITED, no JS change: the bullet is a ::before on .ct-icon--active, whose
   opacity the scan timeline already animates (set to 1 on the row's entrance beat tIn,
   crossfaded to 0 on the next row's beat tDim), nested inside .ct-icon whose opacity is
   also tweened up on tIn. The two multiply, so the bullet appears exactly when the active
   pill would have and hands off on the same beat. Non-primary rows show no marker (active
   is opacity 0), matching the feed model.

   The 40×40 SVGs stay in the DOM (markup untouched) but are hidden. The slot shrinks to the
   bullet's 12px so the row's own --ct-gap (24px) lands the label 24px from the bullet — the
   rail's rhythm, not feed's 8px. Row height (96px) untouched. */
@media (min-width: 769px) {
  .ct-icon {
    flex: 0 0 12px;                      /* was 40 — slot is now the bullet, so --ct-gap
                                            (24px) measures bullet→label directly */
    width: 12px;
    height: 12px;
  }
  .ct-icon-img > img { display: none; }  /* hide the 40×40 nav-icon plates (kept in DOM) */
  .ct-icon--active {
    display: grid;
    place-items: center;                 /* centre the ::before in the 12px box */
  }
  .ct-icon--active::before {
    content: '';
    width: 12px;                         /* node 1693:1998 — size-[12px] */
    height: 12px;
    border-radius: 2px;                  /* node 1693:1998 — rounded-[2px] */
    background: #42604D;                 /* active-plate colour (per user; not feed's #1e2e29) */
  }
}


/* ===================== RAIL AT <=768 / <=480: MARKER + LABEL STATE =====================
   Figma 1692:1634 (square: node 1693:1998 — 12x12, radius 2px, x=16; node fill #1e2e29,
   OVERRIDDEN to #42604D per user 2026-07-22).
   TWO STATES PER ROW, and every row shows a label at all times:

     inactive   label #EBFDED, no square
     active     label #1E2E29, with the 12x12 #42604D square beside it

   THE TWO STATES ARE MUTUALLY EXCLUSIVE, NOT STACKED. Each icon variant carries its own copy
   of the label: the default variant holds the #EBFDED one and no square, the active variant
   holds the #1E2E29 one and the square. The timeline already crossfades those two elements —
   the active one is opacity-1 exactly while its card is primary and hands back on the next
   card's entrance beat (84693b5) — so the label ink, the square's arrival and its departure
   all ride that one existing beat. No new tween.

   WHY NOT OVERLAY THE LIT COPY ON .ct-label: tried in 23ac8d8/v=44. With the light copy still
   painting underneath, the dark copy cannot cover it — at 18px Light the glyph strokes are
   thin enough that the light ink bleeds through the anti-aliased edges and the active label
   renders a washed grey instead of #1E2E29. Verified by render, not by reasoning.

   .ct-label is out of flow at these widths BECAUSE the variants carry the visible label —
   the row is never label-less; which ELEMENT paints it changes, not whether one is shown. */
/* --stage-scale CEILINGS ARE MEASURED, NOT CHOSEN. The composition shrinks about the card's
   centre, so its right edge sits at 50% + 50%*scale; the rail re-anchors 8px past that edge,
   and the rail's visible extent (dot + gap + label, the label being out of flow) must still
   land inside the card. Solving for the largest scale that leaves the label inside:
       768:  (674.6 - 8 margin - 92.3 rail extent - 8 gap) / (0.5 * 674.6) - 1 = 0.679
       480:  (416.6 - 8 margin - 80.3 rail extent - 8 gap) / (0.5 * 416.6) - 1 = 0.538
   0.675 and 0.54 are those ceilings, rounded in. ~0.75 was the brief's target but does not
   fit: at 0.75 the label overhangs the card's right edge by 16px at 768 and 36px at 480, and
   .hero-card's overflow:hidden cuts it. Re-derive if the type, the gap or the labels change. */
@media (max-width: 768px) {
  .hero-card { --stage-scale: 0.675; }  /* measured ceiling — see the note above the query */

  .card-title-row {
    --ct-gap: 16px;                           /* tightened with the type */
    height: 64px;                             /* was 96 — closes the vertical rhythm */
    padding-left: 16px;                       /* = the node's x=16, unchanged */
  }
  /* COMPOSITION AT --stage-scale, RAIL INTO THE FREED SPACE. The composition stays centred
     and shrinks about the card's centre, so its right edge lands at 50% + 50%*scale. The rail
     then re-anchors just past that edge — into the dead space the stack anchor used to leave
     — instead of sitting over the composition. Both read the SAME --stage-scale, so the two
     cannot drift apart if the scale is retuned. */
  .card-stage { transform: scale(var(--stage-scale)); }
  .card-titles {
    left: calc(50% + (50% * var(--stage-scale)) + 8px);
  }

  .ct-label { display: none; }                /* the variants below carry the visible label */
  .ct-label,
  .ct-alt { font-size: 18px; }                /* was 24 */

  .ct-icon {
    flex: 0 0 12px;
    width: 12px;
    height: 12px;
    position: relative;
    z-index: 1;
  }
  .ct-icon--default,
  .ct-icon--active {
    position: relative;                       /* anchors each label copy */
    overflow: visible;                        /* the label sits outside the 12px box */
  }
  .ct-icon--default > img,
  .ct-icon--active  > img { display: none; }  /* no glyphs at this breakpoint */
  .ct-icon--default { background: none; }     /* inactive: label only, no square */
  .ct-icon--active {
    background: #42604D;                      /* node 1693:1998 is #1e2e29; overridden to
                                                  #42604D per user 2026-07-22 — the active
                                                  rail plate's colour. Fill only; size,
                                                  offset and rhythm unchanged. */
    border-radius: 2px;                       /* node 1693:1998 radius */
  }

  .ct-alt {
    display: block;
    opacity: 1;                               /* clears the rest state shared with .ct-label */
    position: absolute;
    left: calc(100% + var(--ct-gap));         /* slot edge + the row's own gap token */
    top: 50%;
    transform: translateY(-50%);
    white-space: nowrap;
  }
  .ct-icon--default .ct-alt { color: #EBFDED; }
  .ct-icon--active  .ct-alt { color: #1E2E29; }
}

/* Tighter again at the smallest widths. Same two states, same mechanism — only the rhythm
   and the type step down, so the four rows keep their proportions as the card shrinks. */
@media (max-width: 480px) {
  .hero-card { --stage-scale: 0.54; }        /* the rail takes a larger share of a 417px card */

  .card-title-row {
    --ct-gap: 12px;
    height: 48px;
  }
  .ct-label,
  .ct-alt { font-size: 15px; }
}

/* =========================================================================
   INTRO-1 OVERRIDE (scan.css v=56) — hide the .hero-caption dashed review marker.
   The shared base (css/pellmell-intro.css) draws a 1px dashed border on the empty
   reserved caption slot below the Matey card as a review marker; it renders on v2/v3
   too, so it is NOT edited there. This id-specificity override (1,0,0,0 > .hero-caption
   0,0,1,0) removes only the visible marker on INTRO-1 — the reserved slot stays. */
#heroCaption {
  border: none;
}
