/* =========================================================================
   INTRO-1 INNER-CARD CONTENT SCALE — the project title (left) and the title rail
   (right) rest at a REDUCED SCALE, close to the central card stack, and SCALE UP
   into their shipped poses as the card's clip reveal opens.  INTRO-1 ONLY.  v=6

   THE PROBLEM THIS SOLVES (measured 2026-07-27, headless @1440×900 and @1848×1053):
   at rest the card is clipped to a BAND 70.618% of its width (--lip-h 14.6909% per
   side, css/pellmell-intro-lipscale.css). The inner content is inset 9.87% per side
   (--proj-content-inset). The deficit is a clean

       14.6909% − 9.87% = 4.8209% of card width, BOTH sides
       = 62.2px @1440 (card 1290)   |   71.9px @1848 (card 1492, capped)

   so the Matey logo rendered as "atey" @1440 and "tey" @1848, and the rail's widest
   row lost ~58px @1440. The content was laid out in FULL-FOOTPRINT coordinates while
   the visible window was the rest band.

   WHAT THIS FILE DOES NOT DO. The reveal is untouched and stays a PIXEL reveal: the
   clip-path model, --lip-h, --lip-v, REVEAL_LOCK, the ease and the timeline are all
   byte-identical. The scale introduced here is on the INNER CONTENT only — a different
   subject on different elements. The one edit in the reveal file is @property
   --lip-reveal inherits:false → true, so these descendant rules can read the progress.

   ── THE MODEL ─────────────────────────────────────────────────────────────
   Both elements scale from --cs-rest-scale (rest) to 1 (full), driven off --lip-reveal
   (1 = rest, 0 = full) — the SAME progress, the SAME power2.out, the SAME scrub as the
   clip. One source; they cannot desync.

   VERTICAL — ONE SHARED PIN LINE (user 2026-07-27). Both elements scale about the SAME
   horizontal line: the RAIL's full-pose TOP EDGE. Two elements unfolding from one datum
   reads as a single gesture; two elements each pinning their own top reads as two things
   happening near each other. The rail's top therefore holds still and its rows fan
   DOWNWARD — a legible motion with an obvious cause (lists grow downward) — while the
   title, whose box is shorter, settles down 50→84px from the same line (at s = 0.65).

     THE FAN IS ORIGIN-INDEPENDENT. Under scale(s) about origin O a point P rests at
     O + s(P − O), so its travel is (1−s)(P − O) and the DIFFERENTIAL between any two
     points is (1−s)(P₁ − P₂) — O cancels. Moving the pin cannot change the fan; it only
     adds a rigid translation. At s = 0.65 the rail's rows go from 62.4px apart to 96px
     apart (+33.6 per gap), spreading row 1 → row 4 by 100.8px (three gaps × 33.6; NOT
     134.4 — that is the box-top-to-box-bottom figure, and the box has no visible edges).

     Pinning the rail's top is the least-travel option that still clears the fold. The
     rejected alternative placed O ~215px ABOVE the rail box to lift the rest pose into
     view: it delivered the IDENTICAL 100.8px fan under 193px of slide, i.e. the most
     movement of any option and no compensating gain.

   HORIZONTAL — INWARD-FACING ORIGINS. The title scales about its RIGHT edge, the rail
   about its LEFT: the card-facing edge of each. Shrinking therefore pulls each toward
   the stack on its own, and growing pushes it back out. On top of that each gets an
   inward translate (--cs-dx-*) so its card-facing edge rests GAP (40px, see the js) clear of the
   WIDEST object the loop shows — the `thumb` box, half-width 13.465% of the card, NOT the
   narrower app-card stack; measuring to the stack put both elements 22.6px INSIDE the lens —
   this is the "close to the central card" half of the brief, which the origin alone
   does not deliver (a right-origin title would shrink in place, gap unchanged).

   ── WHY GSAP NEVER TOUCHES THESE TRANSFORMS ───────────────────────────────
   Both elements already spend `transform: translateY(-50%)` for their vertical centring.
   GSAP's `scale:`/`x:` rewrite the whole matrix in a FIXED order (translate → rotate →
   scale), so `yPercent:-50` would resolve against the element's UNTRANSFORMED height and
   land OUTSIDE the scale — the element would not be centred at s ≠ 1. Keeping the whole
   transform in CSS, driven by a custom property, sidesteps that entirely: the centring
   stays declarative and the only animated input is --lip-reveal.
   It also keeps this file clear of js/pellmell-intro-scan.js, which owns OPACITY on
   both elements from a separate, LOOPING, non-scroll timeline with per-iteration resets.
   Transform here, opacity there — no shared property, no fight.

   ── THE NUMBERS ARE MEASURED, NOT ASSUMED ─────────────────────────────────
   Everything below is computed in js/pellmell-intro-content-scale.js from live
   getBoundingClientRect geometry and rewritten on resize. This file only spends them.
   See that file for the derivation of --cs-rest-scale and for the three fixed values.
   ========================================================================= */

/* Registered so they interpolate/parse predictably inside calc(). --cs-rest-scale is the
   DERIVED rest scale (see the js); the two --cs-dx-* are the rest inward offsets in the
   element's OWN parent space (already divided by --stage-scale where that applies). */
@property --cs-rest-scale {
  syntax: "<number>";
  inherits: true;
  initial-value: 1;          /* 1 = no scale — the no-JS / reduced-motion pose */
}
@property --cs-dx-title {
  syntax: "<length>";
  inherits: true;
  initial-value: 0px;
}
@property --cs-dx-rail {
  syntax: "<length>";
  inherits: true;
  initial-value: 0px;
}
@property --cs-origin-y-title {
  syntax: "<length>";
  inherits: true;
  initial-value: 0px;        /* 0 = the title's own top; the js lifts it to the rail's */
}

/* THE SHARED DRIVER. --lip-reveal is 1 at rest and 0 at full, so:
       scale : 1 at full, --cs-rest-scale at rest
       dx    : 0 at full, the full inward offset at rest
   Both read the reveal's own progress, so both carry its power2.out and its scrub:1.
   FALLBACK: if js/pellmell-intro-lipscale.js early-returns (reduced motion, no GSAP)
   --lip-reveal keeps its initial 0 → scale 1, dx 0 → the shipped full pose, unchanged. */
.hero-card {
  --cs-scale: calc(1 - var(--lip-reveal) * (1 - var(--cs-rest-scale)));
}

/* ---- PROJECT TITLE (left) ------------------------------------------------------------
   transform-origin X = right   → the card-facing edge; shrinking pulls it toward the stack.
   transform-origin Y = --cs-origin-y-title, a NEGATIVE length that lifts the origin ABOVE
   this element's own top edge onto the SHARED PIN LINE (the rail's full-pose top). It is
   (railHeight − titleHeight)/2 in this element's own coordinate space — measured, because
   .card-projtitle sits INSIDE .card-stage, which carries scale(--stage-scale) at ≤768/≤480.

   translateY(-50%) is the element's EXISTING centring and stays exactly as it was.
   translateX is divided by --stage-scale for the same reason: this element's parent space
   is the SCALED stage, while .card-titles below is a sibling of that stage in unscaled card
   space. An identical px offset would otherwise render 0.675× / 0.54× here and 1× there —
   the one half of the --stage-scale asymmetry that genuinely bites. (The SCALE itself is
   transparent to it: each element's full pose already bakes in its own --stage-scale, and a
   multiplicative rest factor preserves that — title renders 0.675·s, rail s, and they were
   already 0.675 and 1 at full. That is shipped design, not new drift.) */
/* SCOPED >768 — see the block above .card-titles for the measured reason. */
@media (min-width: 769px) {
.card-projtitle {
  transform-origin: right var(--cs-origin-y-title);
  transform: translateY(-50%)
             translateX(calc(var(--lip-reveal) * var(--cs-dx-title) / var(--stage-scale)))
             scale(var(--cs-scale));
}
}

/* ---- TITLE RAIL (right) --------------------------------------------------------------
   transform-origin X = left → the card-facing edge, mirroring the title.
   transform-origin Y = 0    → this element's own top edge IS the shared pin line (the pin
                               is DEFINED as the rail's full-pose top), so no lift is needed
                               and the rail's top holds still through the whole spread.
   No --stage-scale divisor: .card-titles is a SIBLING of .card-stage, not a child, so its
   parent space is the card's own unscaled space. */
/* ---- SCOPED TO >768 — MEASURED, NOT CAUTION (2026-07-27) ------------------------------
   This whole feature is scoped above 768 because the render pass found it MISBEHAVING below
   it, in the exact place the pre-build report flagged as unverified. Measured at rest:

       viewport   s_band(title)   dx(title)      --stage-scale
       768×1024      0.677         −97.4px          0.675
       480× 900      0.361        −137.5px          0.54
       430× 932      0.277        −140.2px          0.54

   TWO FAILURES, both at ≤768 only:
   1. dx(title) is NEGATIVE, i.e. the title's full pose ALREADY sits closer to the stack than
      GAP, so the rest rule pushes it OUTWARD and it would travel INWARD on scroll — the exact
      inversion this model exists to avoid. Cause: .card-projtitle is inside .card-stage
      (scaled 0.675/0.54) but its content is fixed px (148.5 logo, 40px tagline), so against a
      688 / 417px card it is proportionally huge and already crowds the stack.
   2. s_band(title) binds BELOW S_MIN, so the floor overrides a genuine fit constraint rather
      than a comfort one — the title would not actually fit its rest budget.
   Compounding both: at ≤768 the rail is deliberately RE-ANCHORED into the space --stage-scale
   frees (left: calc(50% + 50%*--stage-scale + 8px)) rather than balanced against the card, so
   deriving the title's inset from the rail's edge there pushes it further into the crowding.

   NOT CLAMPED, NOT PAPERED OVER. Clamping dx to zero would hide a composition that does not
   fit behind a motion that merely stops being wrong. Above 769 every number is in range
   (s_band title 1.069 @1440, 1.348 @1848) and the feature is verified; below it the shipped
   ≤768 composition is left exactly as it was. TODO carries the finding with these numbers —
   and note that TODO 07-24 already lists ≤1024/≤768/≤430 as unverified for the REVEAL itself,
   so this is consistent with the state of the rest of the work, not a new gap. */
@media (min-width: 769px) {
.card-titles {
  transform-origin: left 0;
  transform: translateY(-50%)
             translateX(calc(var(--lip-reveal) * var(--cs-dx-rail)))
             scale(var(--cs-scale));
}
}

/* ===================== BLUEPRINT SCREEN — SCAN-HEAD TITLE SCALE =====================
   A SECOND SCREEN in this file (2026-07-27). The file is named for the project screen, but
   this rule spends a value from the same js at the same >768 scope, so a third file would
   have split one mechanism across two homes for no gain.

   WHY THE TITLE AND NOT THE BLOCK. js/pellmell-intro-scan.js OWNS #cardScanhead's transform:
   `gsap.set(cardHead, { yPercent: -50, opacity: 0, y: 12, scale: 0.92 })` at :851 and
   `p4.to(cardHead, { y: 0, scale: 1, ... })` at :999 — re-set and re-animated on EVERY loop
   iteration. Any CSS transform on the container would be overwritten each frame. It writes
   NOTHING to .card-scanhead-title (verified: no JS references it, and the span carries no
   transform of its own), so the child is free. That is the only conflict-free option.

   WHY A SCALE AND NOT A SMALLER font-size. `font-size: 48px` is Figma-authored (node
   1566:1479) and TODO 0b records it as resisting derivation. Overwriting 48 → 42 would erase
   the authored value; a named scale factor keeps 48px legible in the file and states the
   deviation AS a deviation, with its reason attached.

   WHAT IT IS FIXING. The scan-head's inset moved ~110px inward (see --stage-side-inset in
   css/pellmell-intro-scan.css) to clear the rest band. That 110px came out of the space
   between the block and the blueprint: the widest line ("SPACE", ~148px) ended 2.5px from the
   "Room height 3.6m" caption. The fold hid the lower lines at rest, so it read as two separate
   faults — "SCAN squashed" at rest, "YOUR SPACE overlapping" once the card rose — but it is
   one collision, revealed in stages.

   transform-origin: left center — the block is `left`-anchored, so shrinking toward the left
   edge is what moves the RIGHT edge off the blueprint. The scale is DERIVED per viewport in
   the js (it tracks --lip-h through --stage-side-inset); the fallback 1 is the authored pose. */
@media (min-width: 769px) {
  .card-scanhead-title {
    transform-origin: left center;
    /* INTERPOLATED off --lip-reveal (2026-07-27) — WAS a static var(--sh-title-scale).
         reveal 0 (full) → 1        = the authored 48px, Figma 1566:1479
         reveal 1 (rest) → --sh-title-rest (0.600 @1440), the derived rest value
       Same channel and the same calc shape as --cs-scale, so the type grows as the inset
       travels outward and the two read as one move. This is what retires the icon-to-type
       deviation: the icon holds at its RENDERED 45.8px (it DECLARES width: 95.558px, but
       max-width: 32% binds — 32% of the flex column's 143.3px title width), and at the FULL
       pose the title is 48px again, so
       the authored 1.99 ratio is restored and the shift is transient rather than permanent. */
    transform: scale(calc(1 - var(--lip-reveal, 0) * (1 - var(--sh-title-rest, 1))));
  }
}

/* Both elements are animated every frame of the reveal scrub. They already carry their own
   opacity animation from the scan loop; promote them so the scale composites rather than
   re-rasterising the rail's four rows of type on each frame. Same promotion .ht-char and
   the tagline lines already carry (css/pellmell-intro-lipscale.css). */
.card-projtitle,
.card-titles { will-change: transform; }
