/* ============================================================================
   delEat — module pages + product index
   New file (see tools/build-nav.py concurrency note). Everything here layers
   on top of css/site.css and css/product-ui.css without editing either: the
   page head gets a photograph + drawn-object collision instead of a charcoal
   band, every module gets a real device with its product-ui screen instead of
   a flat shot, and the old .modnav chip strip is gone in favour of a mega
   menu (css/nav-menu.css) and, on product.html, a sticky index.
   ========================================================================= */

/* ---------------------------------------------------------- the page head --
   Client Option 2: a full-bleed photograph band, edge to edge under the
   floating nav pill (the nav's own `.mod-hero { margin-top: calc(-1 * ...) }`
   spacer rule in css/site.css is untouched -- this only restyles what sits
   inside the section it already tucks under), with the copy on a
   liquid-glass card anchored to the band's bottom-left. The card is the
   sign-in page's own recipe (css/site.css `.lg-glass`: 28px blur, 1.2
   saturate, a bone-14% border, a bone-22% inset top highlight) with one
   number changed -- bone at ~72% fill rather than ~9%, because this glass
   sits over a photograph rather than the login's charcoal ground, and needs
   to read as paper, not smoke, so `--ink`'s charcoal (n-900, the page's
   ordinary light-mode ink -- this section never sets `.on-dark`) stays
   legible on any of the nineteen photographs. `--lift-4` at its default
   (non-`.on-dark`) value is already charcoal-tinted (rgba(0,0,0,...)),
   so the card's own lift needs no separate colour. */
.mod-hero {
  position: relative; isolation: isolate; overflow: hidden;
  min-height: clamp(520px, 78vh, 860px);
  background: var(--n-900);   /* shows for a beat before the photo decodes */
}
.mh2-band { position: absolute; inset: 0; z-index: 0; overflow: hidden; }
.mh2-photo {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: block;
  /* scaled up 14% -- a 7% overscan margin either side, enough headroom for
     the parallax's own 6% max translate (js/modules.js) to never uncover
     the band's true edge. */
  transform: translateY(var(--mhy, 0px)) scale(1.14); will-change: transform;
}
/* the same noise data-URI as the site's own `.grain` (css/site.css), tiled
   the same size -- fine, not heavy, so the photograph still reads as a
   photograph rather than a texture. */
.mh2-grain {
  position: absolute; inset: 0; z-index: 1; pointer-events: none;
  opacity: .14; mix-blend-mode: overlay;
  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='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='180' height='180' filter='url(%23n)' opacity='.6'/%3E%3C/svg%3E");
}
/* the bottom-30% wash the card sits against -- capped at 22% alpha so the
   band still reads as a photograph, not a charcoal panel with a picture
   showing through it. */
.mh2-scrim {
  position: absolute; left: 0; right: 0; bottom: 0; height: 30%; z-index: 1;
  pointer-events: none;
  background: linear-gradient(to bottom, transparent, rgba(0,0,0,.22));
}
.mh2-in {
  position: absolute; inset: 0; z-index: 2;
  display: flex; align-items: flex-end;
  padding-bottom: 40px;
}
.mh2-card {
  max-width: 640px;
  padding: 36px 40px;
  border-radius: var(--r-5);
  background: rgba(247,243,238,.72);
  border: 1px solid rgba(247,243,238,.14);
  -webkit-backdrop-filter: blur(28px) saturate(1.2);
  backdrop-filter: blur(28px) saturate(1.2);
  box-shadow: inset 0 1px 0 rgba(247,243,238,.22), var(--lift-4);
  color: var(--n-900);
}
@media (prefers-reduced-transparency: reduce), (prefers-contrast: more) {
  .mh2-card { background: var(--bone); backdrop-filter: none; -webkit-backdrop-filter: none; }
}
@media (prefers-contrast: more) {
  .mh2-card { border: 2px solid var(--n-900); }
}
.mh2-card h1 {
  margin-top: var(--s-3); font-size: clamp(40px, 4.6vw, 72px);
  line-height: var(--lh-tight); letter-spacing: var(--tr-tight);
  max-width: 12ch; text-wrap: balance; color: var(--n-900);
}
.mh2-card .lede { margin-top: var(--s-4); max-width: 44ch; font-size: var(--t-md); color: var(--ink-2); }
.mh2-card .hero-actions { margin-top: var(--s-6); }

@media (max-width: 760px) {
  .mod-hero { min-height: 62vh; }
  .mh2-in { padding: 0 16px 20px; }
  .mh2-card { max-width: none; width: 100%; padding: 24px 20px; }
  .mh2-card h1 { font-size: clamp(32px, 9vw, 44px); }
}

/* the demoted in-section H2 stays as site.css set it (var(--t-lg) !important) */

/* ------------------------------------------------------- device screens --
   One shared anatomy across four device types: a rig (centers + reserves
   height), a box (the thing fitScreen() scales via --s), the device itself
   (data-turn reads --sry/--srx from js/site.js's existing turners loop; a
   pointer adds --pry/--prx the same way), and a screen that hosts the exact
   .ui markup product-ui.css already knows how to lay out and reflow via
   @container queries — the same markup regardless of which device it is
   dropped into. */
.dev-rig { display: grid; justify-items: center; padding-block: var(--s-4); min-width: 0; }
.dev-frame-wrap { position: relative; }

/* ============================================================================
   DEVICE RECIPE -- the single source for all four device families.

   Extracted from the iPad (.pad-box/.pad/.pad-frame/.pad-glass/.pad-screen,
   css/site.css) and the iPhone (.ph-box/.phone/.ph-frame/.ph-glass/
   .ph-screen, css/site.css), the two frames that already read as real
   hardware, before the iMac/MacBook below were drawn to match:

   - scale pattern: a `--s` custom property, native pixel size times var(--s)
     on the outer `-box`, the device itself absolutely centred inside it at
     its true native size (`left:50%; top:50%; margin: -halfH 0 0 -halfW`)
     and `transform: scale(var(--s)) rotateY(...) rotateX(...)`. The box is
     what `fitBoxes()`/the pad's own clamp() scale; the device inside never
     changes shape, only size.
   - metal gradient: a shallow diagonal, ~150-155deg, 4-5 colour stops that
     go light -> dark -> light so a flat CSS box reads as a brushed,
     cylindrical edge catching one light source. iPad (space grey):
     `linear-gradient(150deg, #5A5A5F 0%, #303033 38%, #232326 68%,
     #4A4A4F 100%)`. iPhone (natural titanium/silver): `linear-gradient(
     155deg, #EDEDF0 0%, #C3C3C9 28%, #8F8F96 52%, #B9B9BF 74%,
     #E3E3E7 100%)` -- this is the gradient the iMac's silver shell reuses.
   - double inset edge: two stacked inset box-shadows on the frame, a light
     one closer to the edge and a dark one just inside it, so the bevel
     reads as metal catching light on one lip and falling into shadow on
     the next: iPad `inset 0 0 0 1px rgba(255,255,255,.18), inset 0 0 0
     2.5px rgba(0,0,0,.35)`; iPhone `inset 0 0 0 1px rgba(0,0,0,.28),
     inset 0 0 0 3px rgba(255,255,255,.35)` (iPhone's is dark-then-light
     since titanium catches a colder highlight further in). Both also carry
     a soft spread shadow on the frame itself, `0 50-60px 80-90px -30px
     rgba(0,0,0,.6-.75)`, for depth before any tilt is applied.
   - bezel-to-radius ratio: the visible black/screen bezel sits well inside
     the frame's own outer radius, and the screen's own corner radius is a
     fraction of the frame's -- iPad: frame 28px / glass inset 4px / screen
     inset 20px more / screen radius 10px (outer 3.8%, screen radius 1.35%
     of the 740px native width). iPhone: frame 62px radius / glass inset 4px
     / screen inset 9px more / screen radius 50px (outer 15.8%, screen
     12.7%). The iMac/MacBook below use the percentages the brief specifies
     (3%/1.5% outer/inner for the iMac, 4%/2% for the MacBook) rather than
     copying either ratio verbatim, since a 24" display and a phone are not
     the same object -- but the SHAPE of the ratio (screen radius roughly
     half the outer radius) is the same move both existing frames make.
   - screen glass reflection: one soft diagonal catch-light on every screen,
     identical formula everywhere so all four families read as the same
     glass: `linear-gradient(122deg, rgba(255,255,255,.06) 0%,
     rgba(255,255,255,.02) 22%, transparent 42%)` on a `::after`,
     `z-index:5; pointer-events:none; border-radius:inherit`.
   - contact shadow: not a box-shadow (which would draw a flat rectangle
     behind a tilted 3D object) but `filter: drop-shadow(...)` on the
     transformed device element itself, so it traces the device's real
     rendered silhouette. iPad/iPhone use `0 34-60px 40-90px rgba(0,0,0,
     .22-.75)` depending on how much of that shadow the frame's own
     box-shadow already carries. The iMac and MacBook below both use
     `drop-shadow(0 30px 34px rgba(0,0,0,.24))` on the whole device --
     one shared value, so all four devices sit on the page with the same
     weight of contact shadow.
   - antenna lines / buttons: thin `position:absolute` bars with their own
     short gradient (`linear-gradient(90deg, dark, light 50%, dark)`) so a
     button reads as a rounded metal nub, not a flat rectangle. Kept for the
     iPad/iPhone; the iMac has no buttons to draw, the MacBook's analogue is
     the hinge line and the lighter front lip below.

   ---- iMac 24" (silver) -- refined against two Sketchfab reference renders
   (Izvekov iMac / jackbaeten MacBook Pro 16; captures + measurements in the
   scratchpad). Proportions as a fraction of the screen's own native size
   (screen 720x480, kept at 3:2 for our UI regardless of the reference's own
   panel -- everything below is shell only):
     bezel            9px  = 1.25% of screen width  (ref: ~1.2%, hairline)
     outer radius    16px  = 2.22% of screen width
     inner radius     8px  = 1.11% of screen width
     chin height      77px = 16.04% of screen height (ref: ~16%)
     foot width      526px = 73.06% of screen width  (ref: ~73%)
     foot thickness    10px = 2.08% of screen height (ref: ~2%)
   Head (bezel + screen + chin) is 738x566. The chin is its own cool silver,
   #C5CDD0, with a faint vertical gradient lighter at the bottom edge -- no
   centred logo dot. The bezel itself is no longer a lit metal frame; it is
   the same near-black as a glass edge, .imac-chin overlaps its bottom 77px
   completely, and the "hairline" reads off the double-inset edge on
   .imac-head rather than any visible silver width on the sides.
   The neck and the foot are ONE element (.imac-neck), a `clip-path` hexagon
   that tapers from a 110px neck top down to the full 526px foot width by
   88.1% of its own height, then runs flat for the remaining foot slice --
   a continuous flare instead of two stacked shapes with a seam. A lighter
   band is painted into the background gradient right at that flare-to-flat
   seam (the foot's "lighter top face") and a 1px dark line closes the very
   bottom (the foot's front edge). .imac-foot is repurposed as the contact
   shadow the brief asks for: a blurred, flattened radial ellipse sitting
   just below the foot, doing no structural work of its own.
   Whole device is 738x662 native, scaled by `--s` exactly like the iPad.
   Finish is still the iPhone's own titanium gradient on the head (silver
   reads correctly against this site's light bone ground; the iPad's
   charcoal would not).

   ---- MacBook Pro 16" (space black) -- screen kept at 760x475 (16:10) for
   our UI, same reasoning as the iMac's 3:2: the reference sheet's own lid
   ratio (~1.41:1) was read off a 3/4-angle render and is not the flat,
   head-on aspect this CSS box draws, so it is left as context rather than
   forced onto the box:
     bezel            15px  = 1.97% of screen width (ref: ~2%)
     outer radius     16px  = 2.10% of screen width (ref: ~2%)
     inner radius      8px  = 1.05% of screen width
     notch          106x8px = 13.95% x 1.68% of screen (ref: 14% x 1.6%)
   Lid is screen + bezel on all four sides = 790x505. The deck is a
   `clip-path` trapezoid, 837px wide at the front -- 6% wider than the
   790px lid, per the brief -- and 152px tall, 30% of the lid's 505px
   height (ref: "noticeably deeper than the lid is thick"). Whole device is
   837x657 native. Same space-black metal gradient (150deg, dark stops)
   drives the lid and the deck, so they read as one shell folded at the
   hinge; the keyboard well/trackpad below translate the reference's silver
   value relationships (deck lighter than well, lip lightest) into that
   same black palette instead of introducing a new grey.
   ============================================================================ */
.imac-box { --s: 1; position: relative; width: calc(738px * var(--s)); height: calc(662px * var(--s)); perspective: 1700px; }
.imac {
  position: absolute; left: 50%; top: 50%; width: 738px; height: 662px; margin: -331px 0 0 -369px;
  transform-style: preserve-3d; will-change: transform;
  transform: scale(var(--s)) rotateY(calc(var(--sry, 0deg) + var(--pry, 0deg))) rotateX(calc(var(--srx, 0deg) + var(--prx, 0deg)));
  transition: transform .5s var(--glide);
  filter: drop-shadow(0 30px 34px rgba(0,0,0,.24));
}
/* the neck-foot piece and its shadow paint first (same DOM-order rule the
   iPad's shell pieces follow: chrome first, the frame/screen stack last).
   .imac-neck is now the single continuous neck+foot shape (see the recipe
   comment above); .imac-foot is the flattened contact-shadow ellipse under
   it, not a physical part of the stand. */
.imac-neck {
  position: absolute; left: 50%; top: 566px; width: 526px; height: 84px; margin-left: -263px; z-index: 1;
  clip-path: polygon(39.54% 0%, 60.46% 0%, 100% 88.1%, 100% 100%, 0% 100%, 0% 88.1%);
  /* darkened a step past a literal titanium sample: measured against the
     page's own warm off-white in the dashboard chapter, the lighter values
     this started with were close enough in lightness (only a few points of
     hue kept them apart) to nearly disappear into the page rather than read
     as a stand. This gradient trades a little realism for a silhouette that
     still holds up off pure-white staging. */
  background:
    linear-gradient(180deg, rgba(255,255,255,.35) 0%, rgba(255,255,255,0) 8%),
    linear-gradient(180deg, transparent 78%, rgba(255,255,255,.55) 88%, rgba(255,255,255,.2) 91%, transparent 95%),
    linear-gradient(180deg, #C6C6CA 0%, #A6A6AB 45%, #86868C 78%, #B0B0B5 100%);
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.2), inset 0 -1px 0 rgba(0,0,0,.45);
  /* box-shadow can't hug a clip-path silhouette (it always follows the
     untransformed box), so on a light backdrop the flared shape had no
     outer edge of its own and read as almost the same tone as the page
     behind it. A silhouette-hugging drop-shadow filter gives it one
     regardless of what's behind it. */
  filter: drop-shadow(0 3px 5px rgba(0,0,0,.3));
}
.imac-foot {
  position: absolute; left: 50%; top: 650px; width: 440px; height: 12px; margin-left: -220px; z-index: 0;
  border-radius: 50%;
  background: radial-gradient(ellipse at center, rgba(0,0,0,.32) 0%, rgba(0,0,0,0) 72%);
  filter: blur(2px);
}
/* explicit z-index on every sibling in this stack, not just DOM order: all
   five pieces sit at the same z=0 plane inside `.imac`'s own
   `transform-style:preserve-3d`, and once that parent carries a real
   rotateY/rotateX (the scroll-tied --sry/--srx rig, not just the
   pointer-tied --pry/--prx this file's own screenshots forced to 0) Chrome's
   depth-sort of coplanar untransformed layers does not reliably fall back to
   paint order the way it does with no rotation applied -- the chin and head
   were observed painting in the wrong order in the live, scroll-rotated
   dashboard chapter despite correct DOM order and correct computed styles.
   Pinning z-index removes the ambiguity outright. */
.imac-head {
  position: absolute; left: 0; top: 0; width: 738px; height: 566px; border-radius: 16px 16px 0 0; z-index: 2;
  background: linear-gradient(180deg, #1C1C1E 0%, #0A0A0B 100%);
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.15), inset 0 0 0 2px rgba(0,0,0,.4), 0 40px 60px -28px rgba(0,0,0,.35);
}
/* the chin: the reference's proportionally large aluminium chin, a distinct
   cool silver rather than a tint of the head's own gradient, lighter at its
   own bottom edge. It sits flush against the screen's bottom edge -- there
   is no separate bottom bezel, the chin height already spans that gap.
   Positioned with `top`, not `bottom`: .imac-chin is a SIBLING of
   .imac-head, not nested inside it, so `bottom:0` resolves against `.imac`'s
   full 662px box (head + neck + foot) rather than the head's own 566px --
   that bug shipped once already (previously invisible only because the old
   neck/foot happened to share the same silver tone the chin painted over
   down there) and left the head's dark background exposed right under the
   screen once the chin stopped matching the shell tone below it. `top:489px`
   (= head's 566px height minus the chin's own 77px) pins it flush under the
   screen instead, where head's own height actually ends. */
.imac-chin {
  position: absolute; left: 0; right: 0; top: 489px; height: 77px; border-radius: 0 0 16px 16px; z-index: 3;
  background: linear-gradient(180deg, #C5CDD0 0%, #D8DEE0 100%);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.55), inset 0 -1px 0 rgba(0,0,0,.08);
}
.imac-screen {
  position: absolute; left: 9px; top: 9px; width: 720px; height: 480px; z-index: 4;
  border-radius: 8px; overflow: hidden; background: var(--bone);
}
.imac-screen .inner { position: absolute; inset: 0; overflow: hidden; container-type: inline-size; background: var(--bone); }
.imac-screen::after {
  content: ""; position: absolute; inset: 0; z-index: 5; pointer-events: none; border-radius: inherit;
  background: linear-gradient(122deg, rgba(255,255,255,.06) 0%, rgba(255,255,255,.02) 22%, transparent 42%);
}

.mac-box { --s: 1; position: relative; width: calc(837px * var(--s)); height: calc(657px * var(--s)); perspective: 1700px; }
.mac {
  position: absolute; left: 50%; top: 50%; width: 837px; height: 657px; margin: -328.5px 0 0 -418.5px;
  transform-style: preserve-3d; will-change: transform;
  transform: scale(var(--s)) rotateY(calc(var(--sry, 0deg) + var(--pry, 0deg))) rotateX(calc(var(--srx, 0deg) + var(--prx, 0deg)));
  transition: transform .5s var(--glide);
  filter: drop-shadow(0 30px 34px rgba(0,0,0,.24));
}
/* the deck paints first (it sits visually behind/below the lid at the
   hinge), the lid > screen stack last, same shell-first convention as the
   iPad and the iMac above. */
.mac-deck {
  position: absolute; left: 0; top: 505px; width: 837px; height: 152px; z-index: 1;
  background: linear-gradient(150deg, #4B4B4E 0%, #2C2C2F 32%, #1A1A1C 62%, #38383B 100%);
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.08);
  /* the trapezoid: 837px wide at the front (bottom), narrowing to the
     790px lid width at the hinge (top) -- (837-790)/2 = 23.5px inset on
     each side of the top edge, i.e. the front is 6% wider than the back. */
  clip-path: polygon(2.81% 0, 97.19% 0, 100% 100%, 0% 100%);
}
/* every piece in this stack gets an explicit z-index for the same reason as
   the iMac stack above: coplanar siblings inside `.mac`'s own
   `transform-style:preserve-3d` mis-sorted in the live, scroll-rotated
   dashboard chapter even with correct DOM order. */
/* .mac-kbd/.mac-trackpad are siblings of .mac-deck (not nested inside it),
   same flat-sibling DOM every device family uses -- so these are placed in
   px, worked out against the deck's own region of `.mac`'s 837x657 box
   (the deck runs from y:505 to y:657), not percentages, which would resolve
   against `.mac` itself and land the well up in the lid/screen area instead.
   Vertically the deck's 152px splits 14/64/14/46/14: a gap off the hinge,
   the keyboard well, a gap, the trackpad, a gap to the front lip -- which is
   also why the well sits toward the hinge and the trackpad toward the front,
   matching the real machine instead of the old dead-centre stack. */
.mac-kbd {
  position: absolute; left: 33.5px; top: 519px; width: 770px; height: 64px; border-radius: 8px; z-index: 2;
  background:
    repeating-linear-gradient(180deg, rgba(255,255,255,.05) 0 1px, transparent 1px 10.5px),
    #26282C;
  box-shadow: inset 0 2px 5px rgba(0,0,0,.6), inset 0 0 0 1px rgba(255,255,255,.04);
}
/* flush in the same tone as the palm rest -- a close sample of the deck's
   own mid-tone rather than the deck's exact gradient (the two are separate
   siblings, not nested, so the gradient can't be made to continue pixel for
   pixel across them without duplicating the deck's own geometry) -- plus
   only a whisper of a lighter edge, no dark seam. */
.mac-trackpad {
  position: absolute; left: 242.5px; top: 597px; width: 352px; height: 46px; border-radius: 8px; z-index: 2;
  background: linear-gradient(150deg, #47474A 0%, #2E2E31 55%, #222224 100%);
  box-shadow: inset 0 0 0 .5px rgba(255,255,255,.18);
}
/* the hinge line, right at the lid/deck seam, and the front lip, a hair of
   light along the deck's own leading edge -- the two thin-line details the
   brief asks for instead of a fully modelled crease. */
.mac-hinge {
  position: absolute; left: 23.5px; right: 23.5px; top: 505px; height: 1px; z-index: 3;
  background: linear-gradient(90deg, transparent, rgba(0,0,0,.65) 12%, rgba(0,0,0,.65) 88%, transparent);
  box-shadow: 0 2px 3px rgba(0,0,0,.35);
}
.mac-lip {
  position: absolute; left: 0; right: 0; bottom: 0; height: 3px; z-index: 3;
  background: linear-gradient(180deg, rgba(255,255,255,.32), rgba(255,255,255,.04));
}
.mac-lid {
  position: absolute; left: 23.5px; top: 0; width: 790px; height: 505px; border-radius: 16px; z-index: 4;
  background: linear-gradient(150deg, #4B4B4E 0%, #2C2C2F 32%, #1A1A1C 62%, #38383B 100%);
  box-shadow: inset 0 0 0 1px rgba(255,255,255,.12), inset 0 0 0 2.5px rgba(0,0,0,.5), 0 40px 60px -28px rgba(0,0,0,.55);
}
/* the small centred camera notch, cut into the top bezel */
.mac-notch {
  position: absolute; left: 50%; top: 0; width: 106px; height: 8px; margin-left: -53px;
  border-radius: 0 0 8px 8px; background: #0A0A0B; z-index: 5;
}
.mac-notch::after {
  content: ""; position: absolute; left: 50%; top: 2px; width: 4px; height: 4px; margin-left: -2px;
  border-radius: 50%; background: radial-gradient(circle at 40% 40%, #2B2B3C, #05050A 70%);
}
.mac-screen {
  position: absolute; left: 38.5px; top: 15px; width: 760px; height: 475px; z-index: 6;
  border-radius: 8px; overflow: hidden; background: var(--bone);
}
.mac-screen .inner { position: absolute; inset: 0; overflow: hidden; container-type: inline-size; background: var(--bone); }
.mac-screen::after {
  content: ""; position: absolute; inset: 0; z-index: 5; pointer-events: none; border-radius: inherit;
  background: linear-gradient(122deg, rgba(255,255,255,.06) 0%, rgba(255,255,255,.02) 22%, transparent 42%);
}

/* ============================================================================
   3D device viewer (js/device3d.js) -- three chapters on index.html replace
   the CSS-drawn .imac/.mac chrome above with a live WebGL render of the
   real CC-BY iMac/MacBook glTF models; `.imac-box`/`.mac-box` stay exactly
   as they are (same native size, same `--s` fit from fitBoxes()) and this
   fills that box instead of the `.imac`/`.mac` transform stack -- rotation
   is the module's own three.js yaw, not a CSS transform, so there is
   nothing here to rotate. `.device3d-still` is the always-present fallback
   img (phones at 900px and below, any browser without WebGL) -- it starts
   visible and only hides once the module confirms a live frame painted. */
.device3d { position: absolute; inset: 0; }
.device3d-canvas { display: block; width: 100%; height: 100%; opacity: 0; transition: opacity .5s var(--glide); }
.device3d.is-ready .device3d-canvas { opacity: 1; }
.device3d-still { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; display: block; }
.device3d.is-ready .device3d-still { opacity: 0; transition: opacity .5s var(--glide); pointer-events: none; }
.device3d.is-fallback .device3d-canvas { display: none; }
.device3d.is-fallback .device3d-still { opacity: 1; }
@media (prefers-reduced-motion: reduce) {
  .device3d-canvas, .device3d-still { transition: none; }
}

/* ---- phone reuse: the iPhone chrome already exists in site.css (.phone,
   .ph-frame, .ph-glass, .ph-screen, .ph-island, .ph-status, .ph-home,
   .ph-back, .ph-side, .ph-btn); it just needs somewhere for the .ui markup
   to live, and its own read of --sry/--srx/--pry/--prx (the home page's
   phone-trio uses --ry/--rx instead, so this is scoped to module pages via
   the wrapping box class and never touches the shared rule). */
.dev-ph-box { --s: .92; position: relative; width: calc(393px * var(--s)); height: calc(852px * var(--s)); perspective: 1500px; margin-inline: auto; }
.dev-ph-box .phone {
  position: absolute; left: 50%; top: 50%; margin: -426px 0 0 -196.5px; width: 393px; height: 852px;
  transform-style: preserve-3d; will-change: transform;
  transform: scale(var(--s)) rotateY(calc(var(--sry, 0deg) + var(--pry, 0deg))) rotateX(calc(var(--srx, 0deg) + var(--prx, 0deg)));
  transition: transform .5s var(--glide);
}
.dev-ph-box .ph-screen { background: var(--bone); }
.dev-ph-box .ph-screen .inner { position: absolute; inset: 0; border-radius: inherit; overflow: hidden; container-type: inline-size; background: var(--bone); }
/* the island + status bar sit on top of the content, not beside it -- the
   app content needs the same top clearance the site's own .ph-view gives it
   (padding: 70px 18px 30px). Scoped to the phone screen's .ui-main only, so
   pad/mac/imac keep product-ui.css's ordinary 16px. */
.dev-ph-box .ph-screen .ui-main { padding: 74px 18px 22px; }
/* .ui is a normal-flow child of .inner (a definite-height absolute box) and
   would otherwise size to its own content instead of filling it -- which is
   what .pv relies on to position its dots/hint against the real screen
   bottom/top rather than the end of its own (short) list of rows. */
.pad-screen .inner > .ui, .mac-screen .inner > .ui, .imac-screen .inner > .ui, .dev-ph-box .ph-screen .inner > .ui { height: 100%; }
/* the mac/imac/phone screens get the same power-on flourish as the tablet,
   reusing site.css's @keyframes screenOn by name (keyframes are global) --
   js/modules.js runs its own small IntersectionObserver for these three
   selectors so it never has to touch the `.pad-screen` class itself, which
   would collide with the phone's own inset/radius rule. */
.mac-screen.screen-on, .imac-screen.screen-on, .dev-ph-box .ph-screen.screen-on {
  animation: screenOn .36s var(--glide) both;
}
/* round 37: below the fold, the screen waits in the keyframe's first frame */
.mac-screen.screen-wait, .imac-screen.screen-wait, .dev-ph-box .ph-screen.screen-wait { transform: scaleY(.02); filter: brightness(2.2); opacity: .7; }
@media (prefers-reduced-motion: reduce) {
  .mac-screen.screen-on, .imac-screen.screen-on, .dev-ph-box .ph-screen.screen-on { animation: none; }
  .mac-screen.screen-wait, .imac-screen.screen-wait, .dev-ph-box .ph-screen.screen-wait { transform: none; filter: none; opacity: 1; }
}

/* .ui inside any of these screens keeps its own scroll for tall content
   rather than overflowing the glass */
.mac-screen .ui, .imac-screen .ui, .pad-screen .ui, .dev-ph-box .ph-screen .ui { min-height: 100%; }

/* pre-JS / no-JS fallback only -- fitBoxes() in js/modules.js measures the
   box's own parent and sets an inline --s once it runs, and an inline style
   always wins over a class rule at equal or lower specificity, so dropping
   `!important` here (which used to pin every <900px viewport to the same
   .62 regardless of how narrow the column actually was, overflowing every
   phone-width mac/imac screen by ~115-197px) lets that measured value stand
   once JS has run, while still giving a sane pre-fit default. */
@media (max-width: 900px) {
  .mac-box, .imac-box { --s: .62; }
  /* .dev-rig's automatic grid-item minimum width used to be its content's
     min-content size (the mac's own unscaled 837px / imac's 738px box), so
     even after the line above stopped fighting fitBoxes()'s measured --s,
     the frame's own drop-shadow filter still pushed the fitted box a few px
     past its column at phone widths, and that in turn widened .mod-body's
     own auto column the same way. Clipping only in this <900px band (never
     on the desktop layout the shadow was tuned for) removes that sliver
     without touching the wider frame. */
  .dev-rig { overflow: hidden; }
}

/* ---- generic screen components, reused across the modules that do not
   already have a bespoke .ui component in product-ui.css --------------- */
.lst { display: grid; gap: 0; }
.lst-row {
  display: grid; grid-template-columns: 30px minmax(0,1fr) auto; gap: 10px; align-items: center;
  padding: 9px 4px; border-bottom: 1px solid var(--ui-line);
}
.lst-row:last-child { border-bottom: 0; }
.lst-row .ic { width: 26px; height: 26px; border-radius: 7px; background: var(--ui-sunk); display: grid; place-items: center; font-size: 13px; }
.lst-row b { display: block; font-size: 11px; font-weight: 600; }
.lst-row small { display: block; font-size: 9.5px; color: var(--ui-ink-3); margin-top: 1px; }
.lst-row .v { font-family: var(--font-mono); font-size: 10.5px; font-weight: 700; white-space: nowrap; }
.lst-row .chip-sm {
  font-size: 9px; font-weight: 700; padding: 2px 7px; border-radius: 999px;
  background: var(--ui-sunk); color: var(--ui-ink-2); white-space: nowrap;
}
.lst-row .chip-sm.ok { background: var(--ok-wash); color: var(--ok-ink); }
.lst-row .chip-sm.warn { background: var(--warn-wash); color: var(--warn-ink); }
.lst-row.is-active { background: var(--ui-acc-sf); border-radius: 8px; }
.lst-row[data-clickable] { cursor: pointer; transition: background .15s var(--snap); }
.lst-row[data-clickable]:hover { background: var(--ui-sunk); border-radius: 8px; }
/* a shorter row where the screen has less room to give (inventory, once the
   slow movers table joined it on a fixed-height mac screen) */
.lst-tight .lst-row { padding: 5px 4px; }

/* movement reveal (inventory) */
.mv-note {
  grid-column: 1 / -1; margin: 2px 0 6px; padding: 7px 10px; border-radius: 8px;
  background: var(--ui-acc-sf); color: var(--ui-on-acc); font-size: 10px; font-weight: 600;
  display: none;
}
.lst-row.is-open + .mv-note { display: block; }

/* toggle switch (menu 86) */
.sw { width: 30px; height: 17px; border-radius: 999px; background: var(--ok); position: relative; flex: none; border: 0; padding: 0; cursor: pointer; }
.sw::after { content: ""; position: absolute; top: 2px; left: 15px; width: 13px; height: 13px; border-radius: 50%; background: #fff; transition: left .15s var(--snap); }
.sw.is-off { background: var(--n-300); }
.sw.is-off::after { left: 2px; }

/* stamp grid (loyalty) */
.stk { display: grid; grid-template-columns: repeat(5, 1fr); gap: 8px; margin-top: 10px; }
.stk i {
  aspect-ratio: 1; border-radius: 50%; border: 1.6px dashed var(--ui-line); display: grid; place-items: center;
  font-style: normal; font-size: 13px; color: var(--ui-ink-3);
}
.stk i.on { border-style: solid; border-color: var(--ui-acc); background: var(--ui-acc-sf); color: var(--ui-on-acc); }

/* step list (setup, kiosk). Round 62: scoped to [data-stp]. Round 61's
   status pill took the class name .stp too (css/site.css), and this rule,
   loaded after site.css on every module-template page, turned each pill into
   a full-width grid block with a 10px top margin (the Works with cards'
   "Certification pending" read as an empty bar). */
.stp[data-stp] { display: grid; gap: 2px; margin-top: 10px; }
.stp-row { display: flex; align-items: center; gap: 10px; padding: 8px 4px; position: relative; }
.stp-row .n {
  width: 22px; height: 22px; border-radius: 50%; background: var(--ui-sunk); color: var(--ui-ink-3);
  display: grid; place-items: center; font-family: var(--font-mono); font-size: 10px; font-weight: 700; flex: none;
}
.stp-row span { font-size: 11px; font-weight: 600; }
.stp-row.is-done .n { background: var(--ok); color: #fff; }
.stp-row.is-on .n { background: var(--ui-acc); color: #000000; }
.stp-row.is-on span { color: var(--ui-ink); }
.stp-row:not(.is-on):not(.is-done) span { color: var(--ui-ink-3); }

/* N-view phone swiper (apps / delivery / loyalty / offline / expenses / ask).
   Width is set inline per call (pv_screen in tools/build-modules.py knows n);
   only the offset is a token here, driven by --pv-i (the current index, set
   by js/modules.js) against --pv-n (the view count, set inline on .pv-views).
   Two views is the common case and still works exactly as before. */
.pv { position: relative; overflow: hidden; height: 100%; }
.pv-views { display: flex; height: 100%; transition: transform .45s var(--glide); transform: translateX(calc(var(--pv-i, 0) * -100% / var(--pv-n, 2))); }
.pv-view { flex: none; padding: inherit; overflow: hidden; }
.pv-dots { position: absolute; left: 50%; bottom: 8px; transform: translateX(-50%); display: flex; gap: 5px; z-index: 4; }
.pv-dots i { width: 5px; height: 5px; border-radius: 50%; background: rgba(0,0,0,.18); }
.pv-dots i.on { background: var(--ui-acc); }
.pv-hint {
  position: absolute; right: 10px; top: 10px; z-index: 4; font-size: 9px; font-weight: 700;
  color: var(--ui-ink-3); background: var(--ui-card); border: 1px solid var(--ui-line); border-radius: 999px;
  padding: 4px 9px; cursor: pointer;
}

/* chat bubbles (ask) */
.chat { display: grid; gap: 8px; margin-top: 8px; }
.bubble { max-width: 88%; padding: 9px 12px; border-radius: 14px; font-size: 11px; line-height: 1.5; }
.bubble.q { justify-self: end; background: var(--ui-acc); color: #000000; border-radius: 14px 14px 3px 14px; font-weight: 600; }
.bubble.a { justify-self: start; background: var(--ui-card); border: 1px solid var(--ui-line); border-radius: 14px 14px 14px 3px; }
.bubble.a small { display: block; margin-top: 5px; color: var(--ui-ink-3); font-size: 9.5px; }

/* ------------------------------------------------------- product.html -----
   The chip strip is gone; a slim sticky rail of the 27 names replaces it,
   highlighting the module whose card is nearest the viewport. Area headings
   group the grid the way the mega menu's five rail areas do. */
.prod-layout { display: grid; grid-template-columns: 200px minmax(0,1fr); gap: var(--s-8); align-items: start; }
@media (max-width: 980px) { .prod-layout { grid-template-columns: 1fr; } .prod-index { display: none; } }
.prod-index { position: sticky; top: 132px; display: grid; gap: 1px; max-height: calc(100vh - 160px); overflow-y: auto; padding-right: var(--s-3); }
.prod-index a {
  font-size: 11.5px; color: var(--ink-3); padding: 6px 10px; border-left: 2px solid transparent;
  border-radius: 0 var(--r-2) var(--r-2) 0; transition: color .15s var(--snap), border-color .15s var(--snap), background .15s var(--snap);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.prod-index a:hover { color: var(--ink); background: var(--surface-2); }
.prod-index a.is-here { color: var(--ink); font-weight: 700; border-left-color: var(--o-500); background: var(--surface-2); }
.prod-area { margin-top: var(--s-8); }
.prod-area:first-child { margin-top: 0; }
.prod-area h2 { font-family: var(--font-mono); font-size: var(--t-xs); text-transform: uppercase; letter-spacing: var(--tr-caps); color: var(--ink-3); padding-bottom: var(--s-3); border-bottom: var(--hair); }

@media (prefers-reduced-motion: reduce) {
  .mh2-photo { transform: none !important; }
  .pv-views { transition: none; }
}

/* -------------------------------------------------------- interactions ---
   The one real thing each screen does. Kept generic so the same rules serve
   every module that shares a component (till/menu/cds all use .dish,
   inventory/reports/api/etc. all use .lst-row). */
.dish[data-clickable] { cursor: pointer; transition: transform .12s var(--snap); }
.dish[data-clickable]:hover { transform: translateY(-2px); }
.dish[data-clickable]:active { transform: translateY(0) scale(.98); }
.tbl[data-clickable] { cursor: pointer; transition: transform .12s var(--snap); }
.tbl[data-clickable]:hover { transform: translateY(-2px); }
.tkt { transition: opacity .4s var(--glide), transform .4s var(--glide); }
.tkt.is-bumped { opacity: 0; transform: translateX(50px) scale(.96); pointer-events: none; }
.rank > div[data-clickable] { cursor: pointer; }
.rank > div[data-hl].is-hl { background: var(--ui-acc-sf); }

/* generic hover + click-highlight, for screens where that is enough */
[data-hl] { cursor: pointer; transition: background .15s var(--snap), transform .12s var(--snap); }
[data-hl]:hover { background: var(--ui-sunk); }
[data-hl].is-hl { background: var(--ui-acc-sf) !important; }
.tile[data-hl].is-hl, .tile.ink[data-hl].is-hl { outline: 2px solid var(--ui-acc); outline-offset: -2px; }

@media (prefers-reduced-motion: reduce) {
  .dish[data-clickable], .tbl[data-clickable], .tkt, [data-hl] { transition: none; }
}

/* ============================================================================
   Plus-cards, ported from css/apple.css for the module pages that now carry
   one of their own (mid-shift facts on m-till.html) -- same <details>/
   <summary> primitive, no JS needed. Kept in this file rather than pulling
   the whole of apple.css into every module page, which owns unrelated home
   -page-only rules (hero, highlights rail, pocket phones) this page never
   uses.
   ========================================================================= */
.plus-card {
  border: var(--hair); border-radius: var(--r-4); background: var(--surface);
  overflow: hidden; align-self: start;
}
.plus-card summary {
  display: flex; align-items: center; gap: var(--s-3);
  padding: 14px var(--s-4); cursor: pointer; list-style: none;
}
.plus-card summary::-webkit-details-marker { display: none; }
.plus-card summary img {
  flex: none; width: 40px; height: 40px; border-radius: 10px;
  object-fit: contain; background: var(--surface-2); padding: 4px;
}
.plus-card .pc-t { flex: 1; min-width: 0; display: grid; gap: 2px; }
.plus-card .pc-t b { font-weight: 650; font-size: var(--t-sm); color: var(--ink); }
.plus-card .pc-t small { color: var(--ink-3); font-size: 11.5px; line-height: 1.3; }
.plus-card .pm { flex: none; width: 20px; height: 20px; position: relative; }
.plus-card .pm::before, .plus-card .pm::after {
  content: ""; position: absolute; inset: 50% 1px auto; height: 2px; background: var(--ink-3);
  transition: transform var(--d-2) var(--snap);
}
.plus-card .pm::after { transform: rotate(90deg); }
.plus-card[open] .pm::after { transform: rotate(0deg); }
.plus-card > div {
  padding: 0 var(--s-4) var(--s-4) calc(40px + var(--s-3) + var(--s-4));
  font-size: var(--t-sm); color: var(--ink-2); line-height: 1.5;
  animation: pcOpen .28s var(--glide);
}
.plus-card > div a { color: var(--accent-text); font-weight: 600; }
@keyframes pcOpen { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: none; } }

/* ---- plus-bento — a 2x2 bento of the same cards (mid-shift facts), icon
   large top, title, one line -- 2 columns at every width, which for exactly
   four cards is a 2x2 grid on mobile too. 2026-09: this card is a plain
   `<div class="plus-card"><img>...<span class="pc-t">` on every page that
   actually uses it (m-till.html) -- there is no `<summary>` wrapper left
   anywhere in the codebase, that was the markup of an accordion variant
   retired when the plus/reveal panel was dropped (see the non-bento
   .plus-card rules above). Every rule below used to be scoped through
   `.plus-bento .plus-card summary`, which never matched real markup, so
   the icon and title carried NO sizing at all and rendered at the source
   image's full native size. Rewritten to target the actual children.
   `align-items: stretch` (the grid default, made explicit) plus a
   two-line-reserved title keep all four cards -- and their descriptions --
   at the same row height regardless of which title wraps. */
.plus-bento {
  display: grid; grid-template-columns: repeat(2, minmax(0,1fr));
  align-items: stretch; gap: var(--s-3); margin-top: var(--s-6);
}
@media (max-width: 700px) {
  .plus-bento { margin-top: var(--s-5); gap: 12px; }
  .plus-bento .plus-card .pc-t b { font-size: 17px; }
  .plus-bento .plus-card .pc-t small { font-size: 15px; }
}
.plus-bento .plus-card {
  display: flex; flex-direction: column; align-items: flex-start; gap: var(--s-2);
  background: var(--surface);
  border-radius: var(--r-5); padding: var(--s-4); border: 0;
  /* the non-bento `.plus-card` above sets `align-self: start` for its own
     (dead, unused) accordion layout -- override back to the grid's own
     `stretch` here or every bento row inherits that and un-equalises. */
  align-self: stretch;
}
.plus-bento .plus-card > img {
  width: 44px; height: 44px; border-radius: 12px; object-fit: contain;
  background: var(--surface-2); padding: 6px;
}
.plus-bento .plus-card .pc-t { display: grid; gap: 3px; }
.plus-bento .plus-card .pc-t b {
  font-family: var(--font-display); font-weight: 700; font-size: var(--t-base); color: var(--ink);
  line-height: 1.25; min-height: calc(1.25em * 2);
}
.plus-bento .plus-card .pc-t small { color: var(--ink-3); font-size: var(--t-xs); line-height: 1.35; }
@media (max-width: 480px) {
  .plus-bento .plus-card > img { width: 38px; height: 38px; }
  .plus-bento .plus-card { padding: var(--s-3); }
}

/* ═══════════════════════════════════════════════════════════════════════════
   Round 26, items 4, 5 and 6: the module page, rebuilt to the home page's
   standard.

   Before this round every m-*.html opened the way the site opened in round
   11: a full-bleed photograph with a liquid-glass card of type floated over
   it. The home page stopped doing that in round 13 and has opened on a
   headline with the product beside it ever since, so the twenty-seven pages
   that are most of the site were the last ones still on the old idiom. The
   spine is four sections now, and every token in them -- type ramp, section
   rhythm, tile radius, glyph family, button recipe -- is the home page's
   own:

     1. the hero below (eggshell, copy left, the module's REAL UI right),
     2. the keynote bento built in rounds 21 and 24, under a section head,
     3. "How it works", three white `.mini-card`s (round 25's idiom),
     4. the FAQ, "Works with", the pager and the shared trial band.

   `.mod-hero` / `.mh2-*` above are NOT deleted: product.html still opens on
   a photographic head, and it is the only page that does.
   ═════════════════════════════════════════════════════════════════════════ */
.mod-hero3 {
  position: relative; isolation: isolate; overflow: clip;
  background: var(--bone);
  padding-block: clamp(26px, 3.6vw, 52px) clamp(28px, 4vw, 60px);
}
.mh3-in {
  position: relative; z-index: 1;
  display: grid; grid-template-columns: minmax(0, .92fr) minmax(0, 1fr);
  gap: clamp(22px, 3.2vw, 54px); align-items: center;
}
.mh3-copy { min-width: 0; max-width: 34rem; }
.mh3-copy .eyebrow { margin-bottom: 12px; }
/* the home page's own eggshell heading ink (css/apple.css, `.section-head
   h2`), at h1 size. `padding-bottom: .08em` is what keeps the descenders of
   a `background-clip: text` fill from being cut at the padding box. */
.mh3-h {
  margin: 0;
  font-family: var(--font-display); font-weight: 800;
  /* bigger than `--t-2xl`'s 56px ceiling, which is what every section
     head under it is set at: a page's h1 has to out-weigh its own h2s. */
  font-size: clamp(34px, 4.3vw, 62px);
  letter-spacing: -0.04em; line-height: 1.05; padding-bottom: .08em;
  max-width: 17ch;
  background-image: linear-gradient(180deg, #000000 0%, #2C2C2E 55%, #6E6E73 100%);
  -webkit-background-clip: text; background-clip: text;
  color: transparent; -webkit-text-fill-color: transparent;
}
.mh3-copy .lede { margin-top: 16px; max-width: 48ch; }
.mh3-copy .hero-actions { margin-top: clamp(20px, 2.4vw, 30px); }
/* ---- Round 27, items A and B: photograph plus device ---------------------
   Parth: a landing page should have a picture in the head, because that is
   what humanises a product. So the right column is a daylight photograph
   from the site's own approved set, in a `--r-lg` frame at the column's
   height, and the module's device overlaps its LOWER LEFT corner by about
   30 percent of the device's own width. People, then product. No type ever
   sits over the photograph.

   `.mh3-dev` is one slot with four possible contents: the CSS iPad, the CSS
   iPhone, the photoreal laptop composite (cut out to alpha since round 33,
   resting on a contact shadow) or one of the three no-device
   elements (a printer photograph with a ticket, a status card, a row of the
   three screen sizes). */
.mh3-shot {
  position: relative; margin: 0; min-width: 0;
  /* the device hangs off the lower left, so the column reserves the room
     rather than the section clipping it */
  padding: 0 0 clamp(48px, 6vw, 96px) clamp(38px, 6.6vw, 118px);
}
.mh3-photo {
  display: block; width: 100%; aspect-ratio: 6 / 5; object-fit: cover;
  border-radius: var(--r-lg);
  box-shadow: 0 30px 60px -34px rgba(0, 0, 0, .42), 0 2px 8px -4px rgba(0, 0, 0, .18);
}
/* Round 33, item 3: no blur band at the photo's bottom edge. Round 29 kept a
   `.pblur` stack here, and on the page it read as a smeared strip across the
   foot of every hero photograph. A photograph ends at its frame, crisp. */
.mh3-dev {
  position: absolute; left: 0; bottom: 0; z-index: 2;
  width: 66%; min-width: 0;
}
.mh3-dev .dev-rig { padding-block: 0; width: 100%; }
/* Round 33, item 2: the laptop is cut out. The composite used to carry the
   photograph's black seamless, so on eggshell every laptop sat inside a black
   rounded card and read as a pasted picture. The files are WebP with alpha
   now (tools/mk-mod-macs.py), so there is no plate, no radius and no box
   shadow; the laptop rests on the page on a soft contact shadow, a blurred
   ellipse under the base. The base's foot measures 95.6 percent down the
   image box and the body runs 11.7 to 88.2 percent across it, so the
   ellipse is centred on that line and a little narrower than the body. */
.mh3-mac {
  display: block; width: 100%; height: auto;
  position: relative; z-index: 1;
}
/* two layers, the way a real object sits: a tight occlusion line right
   under the foot, and a wide, faint penumbra spreading from it */
.mh3-shot.dev-mac .mh3-dev::before,
.mh3-shot.dev-mac .mh3-dev::after {
  content: ""; position: absolute; z-index: 0; pointer-events: none;
  border-radius: 50%;
}
.mh3-shot.dev-mac .mh3-dev::before {
  left: 11%; right: 11%; bottom: 1.2%; height: 5.5%;
  background: radial-gradient(closest-side, rgba(0, 0, 0, .55), rgba(0, 0, 0, .25) 60%, rgba(0, 0, 0, 0));
  filter: blur(3px);
}
.mh3-shot.dev-mac .mh3-dev::after {
  left: 4%; right: 4%; bottom: -3%; height: 13%;
  background: radial-gradient(closest-side, rgba(0, 0, 0, .26), rgba(0, 0, 0, .1) 60%, rgba(0, 0, 0, 0));
  filter: blur(10px);
}
/* product.html's head: the photograph alone, no device hanging off it */
.mh3-shot.prod-shot { padding: 0; }
/* the slot's width per device: the three device compositions are in the
   round 55 block at the end of this file; the no-device pages keep this */
.mh3-shot.dev-none .mh3-dev  { width: 56%; }

/* printing: the printer photograph with a ticket over its lower right */
.mh3-nd-print { position: relative; }
.mh3-nd-print img {
  display: block; width: 100%; height: auto; border-radius: var(--r-lg);
  box-shadow: 0 26px 48px -26px rgba(0, 0, 0, .5);
}
.mh3-ticket {
  position: absolute; right: -8%; bottom: -12%; width: 44%;
  display: grid; gap: 3px; padding: 12px 12px 14px;
  background: #fff; border-radius: 8px;
  font-family: var(--font-mono); font-size: 10.5px; line-height: 1.5;
  color: #1C1C1E;
  box-shadow: 0 18px 34px -18px rgba(0, 0, 0, .48);
}
.mh3-ticket b { font-size: 11px; letter-spacing: .06em; }
.mh3-ticket span { color: #48484A; }
.mh3-ticket i { display: block; height: 1px; background: repeating-linear-gradient(90deg, var(--nt-ink-2) 0 4px, transparent 4px 8px); margin: 3px 0; }

/* offline: a status card, because the point is that there is no screen */
.mh3-status {
  background: #fff; border-radius: var(--r-lg); padding: 18px 18px 16px;
  box-shadow: 0 26px 48px -26px rgba(0, 0, 0, .42), 0 1px 0 rgba(0, 0, 0, .04);
  display: grid; gap: 12px;
}
.mh3-status .ms-top { display: flex; align-items: center; gap: 8px; font-size: 15px; font-weight: 600; }
.mh3-status .ms-dot { width: 9px; height: 9px; border-radius: 50%; background: #D97A00; box-shadow: 0 0 0 4px rgba(217, 122, 0, .16); }
.mh3-status .ms-t { margin-left: auto; font-family: var(--font-mono); font-size: 12px; color: var(--ink-3); }
.mh3-status .ms-rows { display: grid; gap: 7px; }
.mh3-status .ms-rows div { display: flex; justify-content: space-between; align-items: baseline; font-size: 14px; }
.mh3-status .ms-rows span { color: var(--ink-2); }
.mh3-status .ms-rows b { font-family: var(--font-mono); font-weight: 600; }
.mh3-status .ms-foot { display: grid; gap: 6px; padding-top: 10px; border-top: 1px solid var(--line); }
.mh3-status .ms-chip { justify-self: start; font-size: 12px; font-weight: 600; padding: 3px 9px; border-radius: 999px; background: #E8F6EE; color: #14683F; }
.mh3-status .ms-foot small { font-size: 13px; color: var(--ink-3); }

/* apps: a row of the three screen sizes, drawn, no photograph */
.mh3-nd-trio { display: flex; align-items: flex-end; gap: 12px; }
.mh3-nd-trio span {
  display: block; background: #1C1C1E; border-radius: 10px; padding: 5px;
  box-shadow: 0 20px 38px -22px rgba(0, 0, 0, .6);
}
.mh3-nd-trio i {
  display: block; border-radius: 5px;
  /* a suggestion of the same screen on all three: an orange header band, a
     rule under it and two rows of content, drawn rather than photographed */
  background:
    linear-gradient(to right, rgba(255,107,61,.92) 0 34%, rgba(255,107,61,0) 34%) top left / 100% 9% no-repeat,
    linear-gradient(to right, rgba(0,0,0,.12) 0 100%) 0 12% / 100% 1px no-repeat,
    linear-gradient(to right, rgba(0,0,0,.09) 0 62%, rgba(0,0,0,0) 62%) 8% 30% / 84% 7% no-repeat,
    linear-gradient(to right, rgba(0,0,0,.07) 0 44%, rgba(0,0,0,0) 44%) 8% 45% / 84% 7% no-repeat,
    linear-gradient(to right, rgba(0,0,0,.07) 0 52%, rgba(0,0,0,0) 52%) 8% 60% / 84% 7% no-repeat,
    var(--bone);
}
.mh3-nd-trio .mt-lap { flex: 0 0 52%; }
.mh3-nd-trio .mt-lap i { aspect-ratio: 16 / 10; }
.mh3-nd-trio .mt-pad { flex: 0 0 30%; }
.mh3-nd-trio .mt-pad i { aspect-ratio: 4 / 3; }
.mh3-nd-trio .mt-ph  { flex: 0 0 14%; border-radius: 8px; }
.mh3-nd-trio .mt-ph i { aspect-ratio: 9 / 19; }

/* the two soft blobs from round 24, behind the frame and nothing else: a
   40px orange and a 20px chartreuse wash, 400px across, blurred 80, on the
   same 16s `gdrift` cycle the glass surfaces use. There is no particle
   field on an eggshell ground anywhere on this site and none is added here. */
.mh3-blob {
  position: absolute; z-index: 0; pointer-events: none;
  width: 420px; height: 420px; border-radius: 50%;
  background:
    radial-gradient(circle at 38% 36%, rgba(255,107,61,.13), rgba(255,107,61,0) 62%),
    radial-gradient(circle at 66% 64%, rgba(194,242,34,.08), rgba(194,242,34,0) 60%);
  filter: blur(80px);
  animation: gdrift 16s ease-in-out infinite alternate;
}
.mh3-blob.a { right: 6%;  top: -8%; }
/* Round 56, item 5: the lower blob hangs past the section's clipped bottom
   edge, and its warm wash ended there as a faint straight line on a desktop.
   It fades out inside its own box before it reaches the edge. */
.mh3-blob.b { right: 26%; bottom: -14%; animation-duration: 21s;
  -webkit-mask-image: linear-gradient(to bottom, #000 40%, transparent 72%);
  mask-image: linear-gradient(to bottom, #000 40%, transparent 72%); }
@media (prefers-reduced-motion: reduce) { .mh3-blob { animation: none; } }
@media (prefers-contrast: more) { .mh3-blob { display: none; } }

@media (max-width: 900px) {
  /* phones and small tablets: text, then the frame. Nothing pins, nothing
     rails, and the frame keeps `fitBoxes`'s own parent-width scale. */
  .mh3-in { grid-template-columns: 1fr; gap: 26px; }
  .mh3-copy { max-width: none; }
  .mh3-h { max-width: 20ch; }
  .mh3-blob { width: 260px; height: 260px; }
  .mh3-blob.a { right: -10%; top: 34%; }
  .mh3-blob.b { display: none; }
  /* text, then the photograph with the device over its corner. Nothing pins
     and nothing rails; the frame keeps fitBoxes's own parent-width scale. */
  .mh3-shot { padding: 0 0 clamp(38px, 13vw, 74px) clamp(14px, 6vw, 40px); }
  .mh3-photo { aspect-ratio: 4 / 3; }
  /* the device slots: round 55 block at the end of this file */
  .mh3-shot.dev-none .mh3-dev  { width: 62%; }
  .mh3-ticket { font-size: 9px; padding: 9px 9px 10px; }
}
@media (max-width: 460px) {
  .mh3-ticket { display: none; }
  .mh3-status { padding: 13px 13px 12px; }
  .mh3-status .ms-rows div { font-size: 12.5px; }
}

/* ---- section 2: the bento, under a proper section head ------------------ */
.mod-detail .kb, .mod-detail .replist { margin-top: var(--bento-top); }
.mod-detail .callout { margin-top: var(--s-6); }

/* ---- section 3: How it works ------------------------------------------- */
.mod-how .mini-bento { margin-top: var(--bento-top); }

/* ---- item 5: the pager cards -------------------------------------------
   The eyebrow and the title used to be an inline <span> and an inline <b>
   inside one <div>, so "NEXT MODULE" and "Floor" ran into each other on the
   same line, and `.next` was mirrored to the right which put the label,
   the title and the line on three different alignments. Both cards are one
   shape now: a left block of three stacked lines, the glyph in a 44px slot
   at the right, vertically centred, on a white --r-lg card with the site's
   own shadow and a hover lift. */
.modpager {
  display: grid; gap: 12px; grid-template-columns: repeat(2, minmax(0, 1fr));
  margin: 0 0 var(--s-8); padding-top: var(--s-6);
  border-top: 1px solid var(--line);
}
@media (max-width: 620px) { .modpager { grid-template-columns: 1fr; } }
.modpager .pg {
  display: flex; align-items: center; gap: var(--s-4); text-align: left;
  padding: 18px 20px; border: 0; border-radius: var(--r-lg);
  background: var(--n-0); color: var(--ink);
  box-shadow: 0 4px 14px rgba(0,0,0,.05), inset 0 0 0 1px rgba(0,0,0,.05);
  transition: transform .2s var(--glide), box-shadow .2s var(--glide);
}
/* the first and the last module have one card, not two: on its own it takes
   the first column rather than leaving a hole where its partner would be. */
.modpager .pg.next { grid-column: 2; }
.modpager .pg.next:only-child { grid-column: 1; }
@media (max-width: 620px) { .modpager .pg.next { grid-column: 1; } }
@media (hover: hover) and (pointer: fine) {
  .modpager .pg:hover {
    transform: translateY(-2px);
    box-shadow: 0 10px 26px rgba(0,0,0,.09), inset 0 0 0 1px rgba(0,0,0,.06);
  }
}
.modpager .pg:active { transform: translateY(0) scale(.99); transition-duration: .1s; }
@media (prefers-reduced-motion: reduce) {
  .modpager .pg { transition: none; }
  .modpager .pg:hover, .modpager .pg:active { transform: none; }
}
.modpager .pg-txt { display: grid; gap: 2px; min-width: 0; flex: 1 1 auto; }
.modpager .pg-lbl {
  font-family: var(--font-mono); font-size: 11px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--ink-2); line-height: 1.2;
}
.modpager .pg-ttl {
  display: flex; align-items: center; gap: 6px;
  font-family: var(--font-display); font-weight: 700; font-size: 24px;
  letter-spacing: -.03em; line-height: 1.16; margin-top: 4px;
}
.modpager .pg-chev { width: 18px; height: 18px; flex: none; color: var(--ink-3); }
.modpager .pg small {
  display: block; margin-top: 4px; font-size: 15px; line-height: 1.45;
  color: var(--ink-2); max-width: 34ch;
}
/* the glyph's own 44px slot, at the right, vertically centred */
.modpager .pg-ic {
  --ic-tile-s: 28px;
  width: 44px; height: 44px; flex: none; align-self: center;
  display: grid; place-items: center; order: 2;
}

/* ═════════════════════════════════════════════════════════════════════════
   Round 27, item C: the four archetypes' own sections.
   Every value here comes from the tokens the home page already uses: the
   `--r-lg` radius, the 12px gap, `--bento-top`, the white card shadow the
   `.mini-card` carries, `--grad-text` for a big number, and the same two
   type sizes inside a tile (a value and a 15px caption). Nothing new is
   invented; only the arrangement differs by archetype.
   ═══════════════════════════════════════════════════════════════════════ */

/* ---- front of house: three photographed steps --------------------------- */
.flow {
  list-style: none; margin: var(--bento-top) 0 0; padding: 0;
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px;
}
.flow .fs {
  display: grid; gap: 6px; align-content: start;
  background: var(--n-0); border-radius: var(--r-lg); padding: 12px 18px 22px;
  box-shadow: 0 4px 14px rgba(0, 0, 0, .05), inset 0 0 0 1px rgba(0, 0, 0, .05);
}
.flow .fs-media {
  position: relative; margin: 0 0 12px; border-radius: 16px; overflow: hidden;
  aspect-ratio: 16 / 10;
}
.flow .fs-media img { display: block; width: 100%; height: 100%; object-fit: cover; }
.flow .fs-dev {
  position: absolute; left: 10px; bottom: 10px;
  display: inline-flex; align-items: center; gap: 7px;
  padding: 5px 10px 5px 7px; border-radius: 999px;
  background: rgba(28, 28, 30, .78); color: #fff;
  backdrop-filter: blur(10px) saturate(1.4); -webkit-backdrop-filter: blur(10px) saturate(1.4);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .22);
}
.flow .fs-dev small { font-size: 11.5px; font-weight: 600; letter-spacing: .01em; }
.flow .fs-dev i { display: block; border: 1.4px solid rgba(255, 255, 255, .82); border-radius: 2.5px; }
.flow .fs-dev .md-pad { width: 13px; height: 10px; }
.flow .fs-dev .md-ph  { width: 7px;  height: 12px; border-radius: 2px; }
.flow .fs-dev .md-lap { width: 15px; height: 9px; border-bottom-width: 3px; }
.flow .fs-n {
  font-family: var(--font-mono); font-size: 11px; font-weight: 600;
  letter-spacing: .12em; color: var(--ink-3);
}
.flow .ic-tile { --ic-tile-s: 28px; margin: 2px 0 2px; }
.flow .fs b {
  font-family: var(--font-display); font-weight: 600; font-size: 17px;
  line-height: 1.24; letter-spacing: var(--tr-head);
}
.flow .fs p { margin: 0; font-size: 14px; line-height: 1.45; color: var(--ink-2); }
@media (max-width: 900px) { .flow { grid-template-columns: 1fr; gap: 10px; } }

/* ---- front of house: the scrub, where one exists ------------------------ */
.mod-scrub .scrub-frame {
  margin-top: var(--bento-top); border-radius: var(--r-lg); overflow: hidden;
  background: #000; box-shadow: 0 28px 54px -30px rgba(0, 0, 0, .5);
  /* Round 56, item 6: the frame takes the video's own ratio. The home
     chapter's 1200/749 frame (css/apple.css) cropped the till recording
     (1200 x 818) at the foot, so the tablet's bottom bezel and the modifier
     sheet were cut off; the frame shows the whole device now, on every width. */
  aspect-ratio: auto; width: min(1120px, 100%); margin-inline: auto;
}
.mod-scrub .scrub-v { display: block; width: 100%; height: auto; }
/* Round 60, item 2: the frame sequence takes the recording's own ratio from
   its first frame, which sits in flow; the canvas lies over it */
.mod-scrub .seq { position: relative; }
.mod-scrub .seq-first { position: relative; height: auto; }

/* ---- guest facing: the journey, four frames ----------------------------- */
.gj {
  list-style: none; margin: var(--bento-top) 0 0; padding: 0;
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 12px;
}
.gj-step {
  display: grid; gap: 5px; align-content: start;
  transition: opacity .55s cubic-bezier(.22, .61, .36, 1) calc(var(--i) * 90ms),
              transform .55s cubic-bezier(.22, .61, .36, 1) calc(var(--i) * 90ms);
}
/* round 30: hidden only once js/modules.js has armed the step's observer */
.gj-step.is-armed:not(.is-in) { opacity: 0; transform: translateY(16px); }
.gj-step.is-in { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  .gj-step { opacity: 1; transform: none; transition: none; }
}
.gj-ph {
  /* a phone at 9/16 in a quarter-width column stands 530px tall with 120px
     of content in it, which reads as an empty screen rather than a stage.
     Capped and shortened so the frame is phone-shaped and the content fills
     the part of it the eye actually lands on. */
  /* `margin-inline: auto` on a grid item overrides `justify-self: stretch`
     and makes the box shrink to fit its own content, which is how the QR
     stage collapsed to 36px while the list stage stood at 186px. An explicit
     width is what centres it AND keeps all four frames the same size. */
  aspect-ratio: 9 / 13; width: min(100%, 236px); margin: 0 auto 12px;
  border-radius: 22px; padding: 7px; background: #1C1C1E;
  box-shadow: 0 22px 42px -26px rgba(0, 0, 0, .62), inset 0 1px 0 rgba(255, 255, 255, .14);
}
.gj-scr {
  height: 100%; border-radius: 16px; background: var(--bone);
  padding: 14px 11px; overflow: hidden; display: grid; align-content: start; gap: 7px;
}
/* Round 56, item 7: a real code, not four squares. tools/build-modules.py
   inlines an SVG made with segno (https://deleat.io, error correction M, a
   one-module quiet zone, black on white) and the modules stay crisp at any
   size the mock draws them. */
.gj-qr {
  align-self: center; justify-self: center; width: 66%; aspect-ratio: 1;
  display: block; padding: 7%; border-radius: 10px; background: #fff;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .08);
}
.gj-qr svg { display: block; width: 100%; height: 100%; shape-rendering: crispEdges; }
.gj-list div, .gj-total div {
  display: flex; justify-content: space-between; gap: 8px; align-items: baseline;
  font-size: 11.5px; padding: 6px 8px; background: #fff; border-radius: 8px;
}
.gj-list span, .gj-total span { color: var(--ink-2); }
.gj-list b, .gj-total b { font-family: var(--font-mono); font-size: 11px; font-weight: 600; }
.gj-btn {
  margin-top: 4px; text-align: center; padding: 8px; border-radius: 999px;
  background: var(--accent, #FF6B3D); color: #fff; font-size: 12px; font-weight: 600;
}
.gj-stages div { display: flex; align-items: center; gap: 8px; font-size: 11.5px; color: var(--ink-3); }
.gj-stages div.on { color: var(--ink); }
.gj-stages i {
  flex: none; width: 13px; height: 13px; border-radius: 50%;
  box-shadow: inset 0 0 0 1.4px rgba(0, 0, 0, .18);
}
.gj-stages div.on i { background: #14683F; box-shadow: none; }
.gj-n {
  font-family: var(--font-mono); font-size: 11px; font-weight: 600;
  letter-spacing: .12em; color: var(--ink-3);
}
.gj-step b {
  font-family: var(--font-display); font-weight: 600; font-size: 17px;
  line-height: 1.24; letter-spacing: var(--tr-head);
}
.gj-step p { margin: 0; font-size: 14px; line-height: 1.45; color: var(--ink-2); }
@media (max-width: 900px) { .gj { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
@media (max-width: 460px) { .gj { grid-template-columns: 1fr; gap: 14px; }
  .gj-ph { aspect-ratio: 16 / 10; width: 100%; } .gj-scr { padding: 12px 14px; } }

/* ---- back office: the figures band -------------------------------------- */
.figs {
  display: grid; gap: 12px; margin-top: var(--bento-top);
  grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
}
.fig-t {
  position: relative; isolation: isolate;
  background: var(--n-0); border-radius: var(--r-lg); padding: 20px 20px 22px;
  box-shadow: 0 4px 14px rgba(0, 0, 0, .05), inset 0 0 0 1px rgba(0, 0, 0, .05);
  display: grid; gap: 4px; align-content: start;
}
.fig-v {
  font-family: var(--font-display); font-weight: 700; font-size: clamp(40px, 4.4vw, 56px);
  line-height: 1; letter-spacing: -.03em; padding-bottom: .06em;
  background: var(--grad-text); -webkit-background-clip: text; background-clip: text;
  color: transparent; -webkit-text-fill-color: transparent;
}
.fig-l { margin: 0; font-size: 17px; font-weight: 600; letter-spacing: var(--tr-head); }
.fig-t small { font-size: 15px; line-height: 1.45; color: var(--ink-2); }

/* ---- back office: the questions it answers ------------------------------ */
.qlist {
  list-style: none; margin: var(--bento-top) 0 0; padding: 0;
  display: grid; gap: 10px; grid-template-columns: repeat(2, minmax(0, 1fr));
}
.qlist li {
  display: flex; gap: 12px; align-items: flex-start;
  background: var(--n-0); border-radius: var(--r-lg); padding: 18px 20px;
  box-shadow: 0 4px 14px rgba(0, 0, 0, .05), inset 0 0 0 1px rgba(0, 0, 0, .05);
  font-family: var(--font-display); font-weight: 500; font-size: 19px;
  line-height: 1.3; letter-spacing: var(--tr-head);
}
.qlist li::before {
  content: "?"; flex: none;
  width: 28px; height: 28px; border-radius: 8px; margin-top: 1px;
  display: grid; place-items: center;
  font-family: var(--font-ui); font-size: 15px; font-weight: 700; color: #fff;
  background: linear-gradient(160deg, #FF5A1F, #FF8A5B);
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .35);
}
@media (max-width: 760px) { .qlist { grid-template-columns: 1fr; } .qlist li { font-size: 17px; } }

/* ---- hardware and platform: the compatibility grid ---------------------- */
.cgrid {
  display: grid; gap: 12px; margin-top: var(--bento-top);
  grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
}
.cg-col {
  background: var(--n-0); border-radius: var(--r-lg); padding: 20px;
  box-shadow: 0 4px 14px rgba(0, 0, 0, .05), inset 0 0 0 1px rgba(0, 0, 0, .05);
}
.cg-col > b {
  display: block; font-family: var(--font-mono); font-size: 11px; font-weight: 600;
  letter-spacing: .12em; text-transform: uppercase; color: var(--ink-3);
  padding-bottom: 12px; margin-bottom: 12px; border-bottom: 1px solid var(--line);
}
.cg-col ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 9px; }
.cg-col li {
  display: flex; gap: 9px; align-items: flex-start;
  font-size: 15px; line-height: 1.4; color: var(--ink-2);
}
.cg-col li::before {
  content: ""; flex: none; width: 6px; height: 6px; margin-top: 8px;
  border-radius: 50%; background: var(--accent, #FF6B3D);
}

/* ═════════════════════════════════════════════════════════════════════════
   Round 27, item E: product.html as a feed.
   Apple's News page, read headless at 1440: a content column inside a wide
   gutter, a family label, then cards in two sizes only, each a photograph
   with a white headline over it and nothing else on the card. The old
   `.prod-index` rail and `.modcard` grid are gone with it; `.prod-layout`,
   `.prod-area`, `.modcards` and `.modcard` are unreferenced now and stay in
   css/apple.css unused rather than being deleted from a file three other
   pages read.
   ═══════════════════════════════════════════════════════════════════════ */
.prod-feed { padding-block: clamp(26px, 3.4vw, 48px); }
.fam-jump {
  display: flex; flex-wrap: wrap; gap: 8px;
  padding-bottom: clamp(18px, 2.2vw, 30px);
  border-bottom: 1px solid var(--line);
}
.fam-jump a {
  font-family: var(--font-mono); font-size: 11px; font-weight: 600;
  letter-spacing: .12em; text-transform: uppercase; color: var(--ink-2);
  padding: 7px 12px; border-radius: 999px; background: var(--n-0);
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .07);
  transition: color .18s ease-out, box-shadow .18s ease-out;
}
.fam-jump a:hover { color: var(--ink); box-shadow: inset 0 0 0 1px rgba(0, 0, 0, .2); }
.prod-fam { padding-top: clamp(34px, 4.4vw, 68px); scroll-margin-top: 100px; }
.prod-fam > .eyebrow { margin-bottom: clamp(14px, 1.6vw, 22px); }
.feed {
  display: grid; gap: 12px;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  grid-auto-rows: minmax(0, auto);
}
.fcard {
  position: relative; isolation: isolate; overflow: hidden;
  border-radius: var(--r-lg); aspect-ratio: 1; display: block;
  background: #1C1C1E; color: #fff;
  transition: transform .26s var(--glide, cubic-bezier(.22, .61, .36, 1)),
              box-shadow .26s ease-out;
}
.fcard.fc-lead { grid-column: span 2; grid-row: span 2; }
.fcard img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; z-index: 0;
  transition: transform .5s var(--glide, cubic-bezier(.22, .61, .36, 1));
}
.fc-scrim {
  position: absolute; inset: 0; z-index: 1;
  /* Round 29, item 2: measured on the rendered pixels rather than chosen.
     At .62 the lead card's headline sat at 2.0:1 over the bright half of
     its photograph; the foot is .86 now and the ramp holds .6 through the
     whole two-line block, which is where the type actually is. */
  background: linear-gradient(to top, rgba(0, 0, 0, .86) 0%, rgba(0, 0, 0, .74) 14%,
              rgba(0, 0, 0, .46) 30%, rgba(0, 0, 0, .18) 44%, rgba(0, 0, 0, 0) 60%);
}
.fc-in {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;
  display: grid; gap: 4px; padding: 18px 20px 20px;
}
.fc-n {
  font-family: var(--font-mono); font-size: 11px; font-weight: 600;
  letter-spacing: .12em; color: rgba(255, 255, 255, .74);
}
.fcard b {
  font-family: var(--font-display); font-weight: 700;
  font-size: clamp(17px, 1.4vw, 21px); line-height: 1.14;
  letter-spacing: -.018em; color: #fff; text-wrap: balance;
}
.fcard.fc-lead b { font-size: clamp(24px, 2.6vw, 38px); }
.fcard.fc-lead .fc-in { padding: 26px 28px 28px; }
.fcard:hover { transform: translateY(-2px); box-shadow: 0 22px 42px -26px rgba(0, 0, 0, .55); }
.fcard:hover img { transform: scale(1.035); }
.fcard:active { transform: translateY(0) scale(.995); transition-duration: .1s; }
@media (prefers-reduced-motion: reduce) {
  .fcard, .fcard img { transition: none; }
  .fcard:hover { transform: none; } .fcard:hover img { transform: none; }
}
/* a family of one takes the lead slot and half the row, not a lonely square */
.feed > .fc-lead:only-child { grid-column: span 2; grid-row: span 1; aspect-ratio: 2; }
@media (max-width: 1100px) { .feed { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
@media (max-width: 700px) {
  .feed { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .fcard.fc-lead { grid-column: 1 / -1; grid-row: span 1; aspect-ratio: 4 / 3; }
  .feed > .fc-lead:only-child { grid-column: 1 / -1; aspect-ratio: 16 / 10; }
}

/* ═════════════════════════════════════════════════════════════════════════
   Round 44, item 5 — the module hero's phone on a phone.

   Two faults, one root. `.dev-ph-box`'s CSS default is `--s: .92`, and the
   only thing that ever corrected it was `fitBoxes` in js/modules.js, which
   is deferred. So until that file parses -- and for good if it never does --
   the frame is drawn at 362 x 784 inside a 390px screen: it covers the
   headline, the lede and the buttons, which is exactly what Parth's iPhone
   showed. The scale is read off the column in CSS now, the same technique
   `.pad-box` has used since round 25 (a length over a length is a number),
   so the frame fits before a byte of JavaScript is parsed. `fitBoxes` still
   runs and still wins on the element's own style attribute; it is a
   refinement now rather than the only thing standing between the reader and
   a phone the size of the page.

   .mh3-dev is 38% of `.mh3-shot`, whose padding box is the wrap's own width
   (100vw less two gutters, to the page max). Checked against fitBoxes's own
   number at every 20px from 320 to 900: identical to three decimals.

   And the screen inside it: this frame is the same chain round 41 found
   behind Safari's blank boards on the home page -- `perspective` over
   `preserve-3d` + `will-change: transform` + a scale, over two nested
   rounded overflow clips. On the phone nothing here rotates (--sry/--pry
   are only ever set by the pointer and the desktop scrub), so the 3D costs
   the page a dropped subtree and buys it nothing. Below 901px the box is
   flat: no perspective, no preserve-3d, no promoted layer, and the four
   parts that exist only to be seen in 3D (the back panel, the three side
   walls and the buttons, all of them edge-on at 0deg) are not drawn.
   ═════════════════════════════════════════════════════════════════════════ */
@media (max-width: 900px) {
  .mh3-shot.dev-phone .dev-ph-box {
    --s: min(.62, calc((100vw - 2 * var(--gutter)) * .38 / 393px));
    perspective: none;
  }
  .mh3-shot.dev-phone .dev-ph-box .phone {
    transform-style: flat; will-change: auto; transition: none;
    transform: scale(var(--s));
  }
  .mh3-shot.dev-phone .dev-ph-box .ph-back,
  .mh3-shot.dev-phone .dev-ph-box .ph-side,
  .mh3-shot.dev-phone .dev-ph-box .ph-btn { display: none; }
  /* The tablet in the same hero slot carries the same chain, and it is NOT
     flattened. Tried and reverted in this round: the iPad's screen, back and
     side walls are real 3D children, and with `preserve-3d` off the right
     third of the frame stopped being drawn at 390 and 430 (captured on
     m-safety.html). The phone above can be flattened because its own 3D
     parts are edge-on at 0deg and are simply not drawn; the tablet's are
     the device. Left as it was. */
}

/* ---- round 44, the sweep: the device badge over a flow figure ----------
   `.fs-dev small` is 11.5px on twelve module pages, set in px rather than
   on the type scale, so it did not move when --t-xs was raised for phones.
   Phones only; the desktop size is untouched. */
@media (max-width: 700px) {
  .flow .fs-dev small, .fs-media .fs-dev small { font-size: 13px; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   ROUND 46: the module and solution hero tablet is a flat picture on a phone
   ═══════════════════════════════════════════════════════════════════════════
   Round 44 flattened the phone in this same slot and wrote down that it could
   not flatten the tablet: with `preserve-3d` off, the iPad's back and side
   walls stop being real 3D children and the right third of the frame is not
   drawn. That was true, and it is why this one is not flattened either -- it
   is replaced. Below 901px `tools/build-modules.py` emits one <img> of the
   whole device, frame and screen together (tools/mkflat.js), the composition
   is not drawn, and the box stops being a 3D context. The geometry rules that
   place it live in the round 46 block at the end of css/apple.css, which every
   page that carries a device already loads.

   `--s` is pinned here rather than left to `fitBoxes` in js/modules.js. .4 is
   exactly what that function computes at every phone width from 320 to 430,
   so nothing moves with JavaScript on; with it off the css clamp was giving
   378px at 430 against fitBoxes's 296, which is round 44's item 5 over again
   on the device it did not reach. A picture that needs a script to be the
   right size is the failure this round exists to remove. ----------------- */
@media (max-width: 900px) {
  .mh3-shot.dev-pad .dev-rig .pad-box { --s: .4; perspective: none; }
}

/* ═══════════════════════════════════════════════════════════════════════════
   ROUND 55: three hero compositions, a device that is never clipped, and a
   reveal on load. Parth, on his iPhone: "tablets look cut off and the same
   is used on multiple pages."
   ═══════════════════════════════════════════════════════════════════════════
   The figure keeps its place (copy left, figure right on a desktop; copy,
   then figure on a phone) and its composition follows the device the page
   opens on, which is what the archetype puts there (tools/build-modules.py,
   DEVICE and ARCHETYPE):
     * FOH pages (the tablet, the kitchen screen): the device large and
       front, over the photograph's lower edge, the photograph at 4:3;
     * office pages (the laptop): the laptop wide across the figure and the
       photograph a shallower 16:10 band behind it, so the screen is the hero;
     * guest pages (the phone): the phone upright at the right third, the
       photograph tall, 3:4.
   The three no-device pages (printing, offline, apps) keep round 27's frame.

   The clipping. Below 901px the tablet is a flat picture (round 46) whose
   box was pinned at --s .4, 296 x 208, inside a slot 66 percent of a 350px
   figure, so `.dev-rig`'s overflow took 65px off it on a 390px phone. The
   box is the slot now: the picture is `width: 100%` of a box that keeps the
   frame's own 740:520, and --s, which only draws the picture's radius and
   shadow at these widths (css/apple.css, round 46), is the slot over 740: a
   container unit where the engine has typed arithmetic and the same number
   from js/site.js's fitPads everywhere, with no floor. The phone's frame
   already sizes off its slot (round 44) and gets the same container unit;
   the laptop is an <img> at 100 percent. Nothing in the hero clips, at any
   width from 320 to 1440 (checked by the round 55 clip harness). */
.mh3-shot.dev-pad .mh3-photo   { aspect-ratio: 4 / 3; }
.mh3-shot.dev-pad .mh3-dev     { width: 74%; }
/* the laptop hangs far enough below the band that the top third of the
   photograph, where the people are, stays clear above the lid */
.mh3-shot.dev-mac              { padding: 0 0 clamp(60px, 7.6vw, 112px) 0; }
.mh3-shot.dev-mac .mh3-photo   { aspect-ratio: 16 / 10; }
.mh3-shot.dev-mac .mh3-dev     { width: 86%; left: 7%; }
.mh3-shot.dev-phone            { padding: 0 0 clamp(40px, 5vw, 72px) 0; }
.mh3-shot.dev-phone .mh3-photo { width: 64%; aspect-ratio: 3 / 4; }
.mh3-shot.dev-phone .mh3-dev   { width: 34%; left: auto; right: 2%; }
@media (max-width: 900px) {
  /* Round 56, item 5 (Parth: "tablet/device screens are too big and the
     section line due to the shadow below are visible"): the tablet slot is
     66% of the figure, the laptop 84%, the phone 34%, scaled, never clipped.
     The device's drop shadow (100 x --s below the tablet, 120 x --s below
     the phone, the laptop's penumbra) must end inside the hero: the section
     keeps enough bottom padding for it at every phone width, so its
     overflow clip never cuts the shadow into a line at the seam. */
  .mod-hero3                     { padding-bottom: clamp(56px, 15vw, 72px); }
  .mh3-shot.dev-pad .mh3-dev     { width: 66%; }
  .mh3-shot.dev-mac              { padding: 0 0 clamp(56px, 22vw, 96px) 0; }
  .mh3-shot.dev-mac .mh3-dev     { width: 84%; left: 8%; }
  .mh3-shot.dev-phone            { padding: 0 0 clamp(28px, 8vw, 48px) 0; }
  .mh3-shot.dev-phone .mh3-photo { width: 66%; }
  .mh3-shot.dev-phone .mh3-dev   { width: 34%; right: 0; }
  /* the hero's rig has nothing left to clip once the box is the slot; the
     body rigs keep the clip they have had since round 26 */
  .mh3-dev .dev-rig { overflow: visible; }
  .mh3-shot.dev-pad .dev-rig .pad-box { width: 100%; height: auto; aspect-ratio: 740 / 520; }
  .mh3-shot.dev-pad .pad-box > .r46-pic > .r46-flat { position: static; width: 100%; height: auto; }
}
@supports (width: calc(100vw / 1px * 1px)) {
  @media (max-width: 900px) {
    .mh3-dev { container-type: inline-size; }
    .mh3-shot.dev-pad .dev-rig .pad-box { --s: calc(100cqw / 740px); }
    .mh3-shot.dev-phone .dev-ph-box { --s: min(.62, calc(100cqw / 393px)); }
  }
}

/* ---- Round 57, item 3: the lift, with amplitude ---------------------------
   Round 55's reveal (28px, 8 degrees) was too small to register. This is the
   Mac mini lift, measured off apple.com's own hero film: the device starts
   55 percent of its own height below where it rests, 1.12 times its size,
   tilted back 16 degrees and turned 10, at opacity 0, and rises and lays
   down to rest over 1.4s on one strong ease-out; its shadow starts small and
   sharp and grows and softens with it; the screen is dark glass until 0.9s,
   then lights, and one band of light crosses it. The photograph settles
   first (.7s, from half: it is the page's LCP, and Chrome only credits an
   image once it is past opacity 0).

   Only the `.mh3-dev` wrapper moves (on a phone the tablet is a flat <img>;
   on a desktop the composed frames sit in preserve-3d shells that are never
   transformed themselves: the round 48 Safari trap). `backwards` holds the
   start pose from first paint through the delay and leaves the base rules,
   the rest state, once it is done. The hero clips only sideways
   (`overflow-x: clip`, below), so the first frames, 55 percent lower, rise
   from under its bottom edge instead of being cut by it. Reduced motion:
   everything at rest; the glass is at opacity 0 unless it is animating. */
@keyframes mh3photo { from { opacity: .5; transform: scale(.98); } to { opacity: 1; transform: none; } }
@keyframes mh3lift {
  0%   { opacity: 0; transform: perspective(1400px) translate3d(0, 55%, 0) rotateX(16deg) rotateY(10deg) scale(1.12); }
  30%  { opacity: 1; }
  100% { opacity: 1; transform: none; }
}
/* the shadow under the device: small and sharp, then grown and soft */
@keyframes mh3shade {
  from { opacity: .7; transform: scale(.55, .5); filter: blur(2px); }
  to   { opacity: 1; transform: none; filter: blur(var(--shade-blur, 14px)); }
}
@keyframes mh3glass { from { opacity: 1; } to { opacity: 0; } }
@keyframes mh3sweep {
  0%   { transform: translateX(-160%) skewX(-14deg); opacity: 0; }
  12%  { opacity: 1; }
  88%  { opacity: 1; }
  100% { transform: translateX(320%) skewX(-14deg); opacity: 0; }
}
.mod-hero3 { overflow: visible; overflow-x: clip; }
.mh3-sweep { display: none; position: absolute; inset: 0; z-index: 6; pointer-events: none; overflow: hidden; }
.mh3-sweep::before {
  content: ""; position: absolute; top: -40%; bottom: -40%; left: 0; width: 42%;
  background: linear-gradient(90deg, rgba(255,255,255,0), rgba(255,255,255,.18) 50%, rgba(255,255,255,0));
  transform: translateX(-160%) skewX(-14deg); opacity: 0;
}
/* the dark glass: the screen's own glass colour with its 6 percent sheen */
.mh3-glass {
  display: none; position: absolute; inset: 0; z-index: 6; pointer-events: none; opacity: 0;
  background: linear-gradient(122deg, rgba(255,255,255,.07) 0%, rgba(255,255,255,.02) 26%, rgba(255,255,255,0) 50%), #060607;
}
.pad-screen > .mh3-glass, .ph-screen > .mh3-glass { display: block; border-radius: inherit; }
/* each device's screen as a fraction of its box. The laptop composites share
   one crop, and their screen is the quad 13.44/7.5, 86.5/8.3, 80.06/49.82,
   19.94/49.82 (measured off img/devices/mod/*-mac.webp); the tablet's flat
   picture keeps its screen at 3.5/4.97 to 96.89/95.58 percent (measured off
   img/devices/flat/pad-*.webp). */
.mh3-shot.dev-mac .mh3-glass-pic { display: block; clip-path: polygon(13.44% 7.5%, 86.5% 8.3%, 80.06% 49.82%, 19.94% 49.82%); }
.mh3-shot.dev-pad .mh3-sweep   { display: block; clip-path: inset(3% 2.4% round 10px); }
.mh3-shot.dev-phone .mh3-sweep { display: block; clip-path: inset(0 round 22px); }
.mh3-shot.dev-mac .mh3-sweep   { display: block; clip-path: polygon(13.44% 7.5%, 86.5% 8.3%, 80.06% 49.82%, 19.94% 49.82%); }
@media (max-width: 900px) {
  .mh3-shot.dev-pad .mh3-sweep { clip-path: inset(4.97% 3.11% 4.42% 3.5% round 1.4% / 2%); }
  .mh3-shot.dev-pad .mh3-glass-pic { display: block; inset: 4.97% 3.11% 4.42% 3.5%; border-radius: 1.4% / 2%; }
}
/* the tablet and the phone get the laptop's two-layer contact shadow, so
   every device has one to grow */
.mh3-shot.dev-pad .mh3-dev::after, .mh3-shot.dev-phone .mh3-dev::after {
  content: ""; position: absolute; z-index: -1; pointer-events: none; border-radius: 50%;
  left: 10%; right: 10%; bottom: -5%; height: 12%;
  background: radial-gradient(closest-side, rgba(0, 0, 0, .24), rgba(0, 0, 0, .08) 60%, rgba(0, 0, 0, 0));
  filter: blur(14px);
}
.mh3-shot.dev-phone .mh3-dev::after { left: 4%; right: 4%; bottom: -3%; height: 7%; }
.mh3-shot.dev-mac .mh3-dev::before { --shade-blur: 3px; }
.mh3-shot.dev-mac .mh3-dev::after  { --shade-blur: 10px; }
@media (prefers-reduced-motion: no-preference) {
  .mh3-photo { animation: mh3photo .7s cubic-bezier(.16, 1, .3, 1) backwards; }
  .mh3-dev   { animation: mh3lift 1.4s cubic-bezier(.16, 1, .3, 1) .12s backwards; will-change: transform, opacity; }
  .mh3-dev::before, .mh3-dev::after { animation: mh3shade 1.4s cubic-bezier(.16, 1, .3, 1) .12s backwards; }
  .mh3-glass { animation: mh3glass .6s cubic-bezier(.4, 0, .2, 1) .9s backwards; }
  .mh3-sweep::before { animation: mh3sweep .85s cubic-bezier(.32, .06, .4, 1) 1s both; }
}

/* ---- Round 60, item 5: Ask delEat's headline and its thinking orb ---------
   The orb (js/orbs.js, 64px) stands beside the headline, centred on its first
   line; the headline keeps its own measure beside it. (A stray closing brace
   above this block, harmless while it was the file's last character, would
   have swallowed the first rule here; it is gone.) */
/* Round 63: the orb is the hero's mark, large, above the headline */
/* Round 63, item 2: the orb sits ABOVE the headline now, not beside it, so
   its visible sphere can share the eyebrow/headline/lede/buttons' one left
   edge exactly. `--orb-s` is the orb's own box; the sphere the physics draws
   sits inset inside that box (js/orbs.js: dots settle to about 0.72 of the
   box, at rest), so the box is pulled left by that same inset rather than
   left at flush zero, which would put the DOTS a visible margin to the right
   of the text above and below. */
.mh3-orbline { display: flex; flex-direction: column; align-items: flex-start; gap: clamp(14px, 2vw, 24px); }
.mh3-orbline .mh3-h { min-width: 0; }
.mh3-orb {
  --orb-s: clamp(180px, 17vw, 260px);
  flex: none; width: var(--orb-s); height: var(--orb-s);
  margin-left: calc(var(--orb-s) * -0.118);
}
@media (max-width: 900px) {
  .mh3-orb { --orb-s: 160px; }
}

/* ---- Round 61: the device-only composition ------------------------------
   Three of the five modules added this round (franchise fees, UAE VAT,
   outlet costs) have no photograph in the approved set that shows what
   they do, so the hero opens on the laptop alone rather than on a stand-in:
   the device fills the figure in flow, on its own contact shadow, with
   room below it for the penumbra. tools/build-modules.py writes
   `.mh3-solo` when PHOTO[key] is None. */
.mh3-shot.mh3-solo,
.mh3-shot.dev-mac.mh3-solo { padding: 0 0 clamp(20px, 3vw, 40px) 0; }
.mh3-shot.mh3-solo .mh3-dev,
.mh3-shot.dev-mac.mh3-solo .mh3-dev { position: relative; left: auto; right: auto; bottom: auto; width: 100%; }
@media (max-width: 900px) {
  .mh3-shot.dev-mac.mh3-solo .mh3-dev { width: 94%; margin-inline: auto; }
}

/* ═════════════════════════════════════════════════════════════════════════
   Round 64, item 1: the mac modules' hero opens on the module's own live
   screen instead of the photoreal laptop composite. `.mh4-hero` swaps the
   `.mh3-in` two-column grid for one centred column -- copy, then a wide
   16:10 display below it; every non-mac module keeps `.mh3-in` untouched.
   The display is a flat on-page object, a thin near-black bezel round a
   crisp screen, never a photograph of hardware. Motion in js/mh4.js: the
   arrival (rise and flatten, once), the UI's own row/tile cascade and
   number count-up, and the desktop-only recede as the page scrolls past.
   ═════════════════════════════════════════════════════════════════════════ */
.mh4-hero .mh4-in {
  display: flex; flex-direction: column; align-items: center;
  gap: clamp(32px, 5vw, 56px); text-align: center;
}
.mh4-hero .mh4-copy { max-width: 640px; }
.mh4-hero .mh4-copy .eyebrow { margin: 0 auto 12px; }
.mh4-hero .mh4-copy .mh3-h { margin-inline: auto; }
.mh4-hero .mh4-copy .lede { margin: 16px auto 0; }
.mh4-hero .mh4-copy .hero-actions { justify-content: center; }

.mh4-stage { position: relative; margin: 0; width: min(1120px, 92vw); }
/* a soft warm glow behind the display, off the site's own orange tokens */
.mh4-glow {
  position: absolute; z-index: 0; inset: -20% -8%; pointer-events: none;
  background: radial-gradient(60% 60% at 50% 42%, rgba(255, 107, 61, .22), rgba(255, 107, 61, 0) 70%);
  filter: blur(6px);
}
/* the floor shadow is a blurred ellipse under the display, not a
   box-shadow, which would draw a hard-edged rectangle behind a rounded
   object sitting flat on the page */
.mh4-stage::after {
  content: ""; position: absolute; z-index: 0; pointer-events: none;
  left: 6%; right: 6%; bottom: -7%; height: 20%;
  background: radial-gradient(closest-side, rgba(0, 0, 0, .30), rgba(0, 0, 0, 0) 72%);
  filter: blur(8px);
}
.mh4-display {
  position: relative; z-index: 1;
  aspect-ratio: 16 / 10; width: 100%;
  padding: 10px; border-radius: 20px;
  background: linear-gradient(180deg, #232326 0%, #0a0a0b 55%, #000000 100%);
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, .09),
    0 40px 70px -32px rgba(0, 0, 0, .55),
    0 10px 26px -14px rgba(0, 0, 0, .4);
}
/* the faint top-edge reflection sits behind the screen (a ::before paints
   before the real .mh4-screen child, so the screen always covers it) */
.mh4-display::before {
  content: ""; position: absolute; inset: 0; z-index: 0;
  border-radius: inherit; pointer-events: none;
  background: linear-gradient(180deg, rgba(255, 255, 255, .12) 0%, rgba(255, 255, 255, 0) 34%);
}
.mh4-cam {
  position: absolute; z-index: 2; top: 4px; left: 50%; transform: translateX(-50%);
  width: 5px; height: 5px; border-radius: 50%; background: #000;
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .18), 0 0 0 1px rgba(0, 0, 0, .5);
}
.mh4-screen {
  position: relative; z-index: 1;
  width: 100%; height: 100%;
  border-radius: 11px; overflow: clip;
  background: var(--bone);
  --mh4-scale: 1;
  --mh4-design: 1000;
  /* the hero copy above is centred; the product inside the screen never is */
  text-align: left;
}
/* the real product UI, laid out at its own design width and scaled down to
   the screen's real (much smaller) box; js/mh4.js keeps --mh4-scale
   current with a ResizeObserver. transform never changes the layout box,
   so a browser with no JS at all still clips cleanly at 1:1 rather than
   overflowing the page. */
.mh4-ui { width: calc(var(--mh4-design) * 1px); transform: scale(var(--mh4-scale)); transform-origin: top left; }
/* list rows carry an icon slot the POS fills; empty here, it read as a grey placeholder at this size */
.mh4-ui .lst-row > .ic:empty { display: none; }
.mh4-ui .lst-row:has(> .ic:empty) { grid-template-columns: minmax(0, 1fr) auto; }

@media (max-width: 900px) {
  .mh4-hero .mh4-in { align-items: stretch; text-align: left; }
  .mh4-hero .mh4-copy { max-width: none; }
  .mh4-hero .mh4-copy .eyebrow { margin: 0 0 12px; }
  .mh4-hero .mh4-copy .mh3-h { margin-inline: 0; }
  .mh4-hero .mh4-copy .lede { margin: 16px 0 0; }
  .mh4-hero .mh4-copy .hero-actions { justify-content: flex-start; }
  .mh4-stage { width: 100%; }
}
/* phones: at the desktop layout the UI shrank to a third and could not be read. It is laid out
   narrower instead, in a portrait screen, so the type lands at a readable size. */
@media (max-width: 700px) {
  .mh4-display { aspect-ratio: auto; padding: 8px; border-radius: 18px; }
  .mh4-screen { min-height: 240px; }
  .mh4-screen { --mh4-design: 520; border-radius: 11px; }
}
@media (prefers-reduced-motion: reduce) {
  .mh4-display { opacity: 1; transform: none; }
}
