
.dh-main{flex:1;padding:2rem 1.5rem 5rem}
/* The copy column's vertical rhythm - badge → headline → lede → buttons.
 *
 * It lives here rather than as mt-* utilities for the same reason the rest of
 * the hero's layout does: spacing that only exists in the generated stylesheet
 * can arrive missing, and a column whose four blocks collapse onto each other
 * is a different page, not a degraded one.
 *
 * The steps are deliberately UNEVEN and they grow downward: the badge is a
 * label, so it sits close to the headline it labels; the headline earns the
 * biggest break before the paragraph; the buttons take the most because they
 * are the thing the reader is being sent to. An even rhythm down the column
 * reads as a form rather than a pitch.
 *
 * Two sets, because the constraint differs. On a phone the page scrolls and
 * the column is the whole screen, so the steps stay compact. From the layout
 * breakpoint up, '.dh-main' holds the hero to one viewport height (see below)
 * and the column has slack beside a 576px artwork, so the steps open out. */
.dh-title{margin-top:1.5rem}
.dh-cta{display:flex;flex-direction:column;gap:1rem;margin-top:2.25rem}
@media (min-width:640px){.dh-cta{flex-direction:row}}
.dh-grid{margin-inline:auto;width:100%;max-width:1500px;display:grid;grid-template-columns:minmax(0,1fr);gap:3rem;align-items:center}
/* 46ch is a measurement, not a preference: the narrowest width at which the
   Hungarian lede sets in three lines instead of four. It only holds where the
   copy is not competing with the floating chips for the same gutter - see the
   two overrides further down. */
.dh-lede{max-width:46ch;margin-top:2rem}
.dh-stage{position:relative;width:100%;max-width:30rem;margin-inline:auto}
.dh-frame{position:relative}
.dh-artwork{position:relative;width:100%;aspect-ratio:2062/2289}
.dh-layer{position:absolute;inset:0}
.dh-parallax{position:absolute;inset:0}
.dh-layer img{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;user-select:none}
.dh-chips{margin-top:1.5rem;display:flex;flex-wrap:wrap;justify-content:center;gap:.5rem}
.dh-chip-body{display:inline-flex;align-items:center;gap:.5rem;padding:.5rem .75rem;border-radius:9999px;font-size:.75rem;line-height:1rem;font-weight:500;white-space:nowrap}
.dh-chip-dot{flex:none;width:.375rem;height:.375rem;border-radius:9999px}
.dh-card{display:none}
.dh-card-body{padding:1rem;border-radius:1rem}
.dh-card-dot{flex:none;width:.5rem;height:.5rem;border-radius:9999px}
.dh-card-title{display:flex;align-items:center;gap:.5rem;font-weight:600;font-size:.875rem;line-height:1.25rem}
.dh-card-text{font-size:.75rem;line-height:1.5;opacity:.6;margin-top:.25rem}

/* Motion. It lives here rather than in the utility stylesheet for the same
   reason the layout does - a hero that arrives without its movement reads as
   a dead picture, and that is precisely the complaint this file exists to
   answer.

   Three kinds, and no more:
     1. entrance  - the stack rises in once, blob first, phones behind it
     2. drift     - a slow, permanent breath, each element on its own period
     3. parallax  - the layers follow the pointer by different amounts

   The drift is TRANSLATE ONLY, deliberately. An earlier idle loop was pulled
   from this hero because a permanently animating layer stays composited and
   the browser rasterises it at a pinned scale, which visibly softened the SVG
   artwork. That penalty comes from animating SCALE; a whole-pixel translate
   keeps the raster at 1:1, so the artwork stays sharp while it moves. Nothing
   here may animate scale or rotation for that reason.

   Every period is a different prime-ish number of seconds, so the elements
   never resynchronise into a single pulsing mass - which is what makes the
   composition read as alive rather than as one thing on a timer. */
@keyframes hero-rise{from{opacity:0;transform:translate3d(0,34px,0) scale(.97)}to{opacity:1;transform:none}}
@keyframes hero-chip-in{from{opacity:0;transform:translate3d(0,12px,0) scale(.92)}to{opacity:1;transform:none}}
@keyframes hero-card-in{from{opacity:0;transform:translate3d(-14px,14px,0) scale(.94)}to{opacity:1;transform:none}}
@keyframes dh-drift{0%,100%{transform:translate3d(0,calc(var(--drift,0px) * -1),0)}50%{transform:translate3d(0,var(--drift,0px),0)}}
.dh-drift{animation:dh-drift var(--drift-dur,9s) ease-in-out infinite;animation-delay:var(--drift-delay,0s)}
/* --hx / --hy are the pointer's position across the viewport, -1..1, written by
   HeroPhones; --depth is per layer.
   NO transition here, deliberately. There used to be a 500ms one, on the
   theory that it smoothed the rAF-sampled pointer - but HeroPhones rewrites
   the variables every frame, so the transition restarted every frame and never
   completed, and the layers rubber-banded after the cursor in visible steps.
   The smoothing lives in HeroPhones now, as an exponential follow. */
.dh-parallax{transform:translate3d(calc(var(--hx,0) * var(--depth,0px)),calc(var(--hy,0) * var(--depth,0px)),0)}
@media (prefers-reduced-motion:reduce){
.dh-drift{animation:none}
}

@media (min-width:1024px){
.dh-main{display:flex;align-items:center;min-height:calc(100svh - 6.5rem);padding-top:1rem;padding-bottom:2rem}
/* The container gives up 6rem on each side that the rest of the page keeps.
   That margin is not whitespace - it is where the ring hangs. The artwork's
   right edge IS the container's right edge, so anything parked outside the
   stage on that side spends the page margin, and at 1180px a percentage-based
   overhang ran straight off the viewport (chips clipped mid-word, and a
   horizontal scrollbar on a page that should never have one). Reserving the
   gutter here means every offset below can be a fixed rem that always fits,
   instead of a percentage that fits only on a wide monitor.

   6rem is not a round number, it is the deepest hang below (5.5rem) plus a
   little. Deepening a slot means widening this too. */
.dh-grid{grid-template-columns:minmax(0,1fr) minmax(0,1.05fr);gap:1rem;max-width:min(1500px,100vw - 12rem)}
.dh-stage{max-width:36rem;margin-inline:0;justify-self:end}
/* Between this breakpoint and the ring's, the cards float but the chips are
   still a row underneath - and the bottom card is parked at 91% of a box it is
   taller than, so it hangs ~40px past the artwork's bottom edge. 1.5rem of
   clearance put the row straight through it. (Below 1024 the cards are hidden
   and the row goes back to sitting close.) */
.dh-chips{margin-top:3.5rem}
.dh-card{display:block;position:absolute;width:13.5rem}
.dh-card-body{height:100%}
/* The left-hand pair hangs into the text column's own slack rather than the
   page margin - the copy stops well short of the artwork - so these can be
   deeper than anything on the right. */
.dh-card-1{left:-5rem;top:1%}
.dh-card-2{right:-3rem;top:78%}
.dh-card-3{left:-2rem;top:91%}
}

/* The roomier rhythm - see the note where these are first set.
 *
 * Width only, and that was checked rather than assumed. The hero is held to one
 * viewport ('.dh-main' min-height) with the columns centred against each other,
 * so opening the steps looks like it should push the artwork down - but the
 * ARTWORK column is the taller of the two at every width from 1024 up, so the
 * copy growing does not move it at all. Measured at 1280, the phones end at
 * 712px with the compact steps and at 712px with these. The whole cost is 10px
 * on the buttons (655 -> 665), which changes nothing on any viewport the hero
 * already fit. A height query here would be a guard against a problem that does
 * not exist. */
@media (min-width:1024px){
.dh-title{margin-top:2rem}
.dh-lede{margin-top:3rem}
.dh-cta{margin-top:3.25rem}
}

/* The ring - and it only exists here, not at the layout breakpoint.
 *
 * Between 1024 and 1280 the page is already two columns, but the artwork is
 * small and the gutters beside it are still spoken for, so the chips have
 * nowhere to go but onto the phones. Measured at 1024px, five of the seven
 * ended up over a screen, one of them by 66px. The phones are the product;
 * a promise pinned across one is worse than no promise. So below this width
 * the chips stay in their plain wrapped row under the artwork - the same
 * arrangement a phone gets - and only fly when there is genuinely room.
 *
 * They are grouped, not spread. Seven labels at even angular spacing read as a
 * dial around the artwork - a mechanical, decorative border rather than notes
 * pinned to a product. So they sit as three PAIRS and one loner: a pair above,
 * a pair down the right, a pair in the left gutter, one alone at the bottom.
 * Within a pair the two are close and deliberately offset - never level with
 * each other, never at the same depth - and BETWEEN pairs there is a long empty
 * stretch. The gaps are the composition; that is what makes it read as scatter
 * rather than as spacing.
 *
 * Grouping is not crowding. Every pair still clears the measured minimum with
 * the drift amplitude added in - see the collision figures in the tests.
 *
 * Slots are numbered in the order the entrance staggers through them, so each
 * pair arrives together.
 *
 * Free space, measured off the artwork's own alpha channel rather than guessed
 * - the phones occupy
 *   services  x 40.6%..86.8%  y  1.0%..78.8%
 *   map       x  1.5%..50.8%  y 18.6%..92.7%
 * which leaves the strip above them, the band below, and the two gutters.
 * Every slot below is in one of those; a new one has to find space there too.
 * (An older comment here put the services phone at 83.0%/75.5% and the map at
 * 46.7%/89.9%. Those were about 4% optimistic in every direction, which is
 * exactly the margin the long-language chips were running out of.)
 *
 * The two RIGHT-hand slots are the only width-sensitive ones: they sit beside
 * the phones, so a chip that grows leftward eventually covers a screen. Free
 * room there is (stage - artwork right edge) + the hang, ≈ 167px at slot 3 and
 * ≈ 170px at slot 4. Which labels may occupy them is therefore not a free
 * choice - see the chip order in app/page.tsx.
 */
@media (min-width:1440px){
.dh-chips{margin-top:0;position:absolute;inset:0;pointer-events:none}
.dh-chip{position:absolute}
/* Pair one, above the artwork. Both sit clear of the services phone's top edge
   (1.0%) rather than beside it: between the top-left card's right edge (23.6%)
   and the phone's left edge (40.6%) there is no room for a chip at phone height
   in any language. Offset by 2% so they are not a level row. */
.dh-chip-1{left:22%;top:-8%}
.dh-chip-2{left:53%;top:-6%}
/* Pair two, down the right - close together, at different depths. This is the
   width-constrained side; see the note above. Both figures are set by the
   NARROWEST stage the ring ever runs at (1280px, where the stage is ~549px):
   the labels are fixed-width text, so they take a bigger bite of a smaller
   stage, and a slot tuned on a 1785px window is not tuned.

   The 7.5% between a pair's two members is a VERTICAL figure and language does
   not enter into it - which is worth stating, because this comment used to say
   the opposite. It claimed Thai and Vietnamese set the floor at 8.5% because
   their diacritics make a chip taller. Measured across all eight bundles at
   1785px, every pill is 34.0px tall and every pair's resting gap is identical
   to a tenth of a pixel: '.dh-chip-body' pins 'line-height' to 1rem, so the box
   cannot grow with the glyphs, and the ink stays inside the 8px padding in
   every language. What DOES change with language is width (128px in Thai to
   189px in Indonesian) - that is the horizontal budget the note above spends,
   and it is real. The vertical one was not.
   7.5% of the 639px stage leaves 13.9px between the pills at rest. Each chip
   drifts +/-2px on its own period (HeroPhones passes driftVars(i, 2); the cards
   are the ones on 3), so two in opposite phase close it to 9.9px - that is the
   number to check against when tightening further. */
.dh-chip-3{right:-4.5rem;top:26%}
.dh-chip-4{right:-5.5rem;top:33.5%}
/* The loner. Clears the bottom-left card, which reaches 31.9% across. */
.dh-chip-5{left:36%;top:95%}
/* Pair three, in the left gutter, and the two roomiest labels go here: these
   are anchored by their RIGHT edge against the stage's left edge, not by a
   negative 'left'. A chip's width is its label's width - seven strings across
   eight languages - and a negative 'left' would park a different amount of each
   one over the artwork. Anchoring the trailing edge puts every chip fully in
   the gutter whatever it says, so width stops mattering here entirely. */
/* Between here and 1600 the copy and the left-hand chips are fighting over the
   same strip, and the chips are already as shallow as their labels allow. So
   the lede gives way - measured, it can hold 42ch at 1500 and 44ch at 1512 but
   nothing over ~36ch at 1440, and a width that changes three times across
   160px is worse than one that steps once. Above 1600 there is room for both
   (48ch measured, in all eight languages) and it goes back to three lines. */
.dh-lede{max-width:34ch}
.dh-chip-6{right:calc(100% + .25rem);top:52%}
.dh-chip-7{right:calc(100% + 2rem);top:59.5%}
}


@media (min-width:1600px){
.dh-lede{max-width:46ch}
}
/* Above this width the container is at its CAP, so the extra room the last
   widening bought is real page margin rather than the hero's own gutter. The
   artwork is pinned to the container's right edge (that is what lets every chip
   offset below be a fixed rem), so widening the container moved the whole shot
   70px further right on each step and it started to read as marooned in the
   corner. This gives the same 70px back - the copy keeps the extra width, the
   artwork goes back to where it sat.

   The threshold is the cap plus the 12rem gutter, i.e. exactly the width at
   which the cap starts binding instead of '100vw - 12rem'. Below it there is no
   surplus to give back, and taking it anyway would push the left-hand chips
   into the copy: measured, the clearance between them is 170px at 1790 but
   already 0 at 1512. */
@media (min-width:1692px){
.dh-stage{margin-right:4.5rem}
}

/* Its own beat between the hero and the calendar. The padding is the only
   height it has - the wash is far taller and hangs off it, so this stays at
   what the FIGURE needs. An earlier version sized this section to contain the
   whole gradient and cost most of a screen. */
.stat-section{position:relative;padding:clamp(3rem,6vw,4.5rem) 1.5rem}

/* The hero grid's container, copied EXACTLY - including the wide-screen
   'min(PAGE_MAX_WIDTH_PX, 100vw - 12rem)', which is what actually decides
   where the headline starts. The cap alone put the number 24px from the viewport
   edge while the hero copy directly above it sat at 96px, because above the
   breakpoint the hero reserves a 6rem gutter for the floating chips. Measured
   across widths, this lands the number on the headline's left edge to the
   pixel: 1920 -> 280/280, 1400 -> 96/96, 390 -> 24/24. */
.stat-inner{margin-inline:auto;width:100%;max-width:1500px}
@media (min-width:1024px){.stat-inner{max-width:min(1500px,100vw - 12rem)}}

/* The background light. Not decoration on the number: it is weighted to the
   LEFT, where the number is, and falls off to the right so the product shot
   above keeps its own backdrop rather than fighting a second one. The two
   offsets are measured against a full-width box that overhangs 45vw each side
   - on a 1400px window the strong light lands ~250px in, under the digits.

   'z-index:-1' is what puts it UNDER the neighbouring content instead of over
   it - without it a layer reaching up into the hero covers the phones. It
   needs the page's outer wrapper to be a stacking context: see
   'isolation:isolate' in app/page.tsx, and why removing that hides this
   completely rather than moving it. */
.stat-glow{position:absolute;z-index:-1;pointer-events:none;top:-18rem;bottom:-18rem;left:-8%;right:-8%;background:radial-gradient(58% 110% at 20% 50%,var(--accent-soft-strong) 0%,transparent 70%),radial-gradient(52% 105% at 80% 50%,var(--accent-soft) 0%,transparent 72%),var(--card-bg);opacity:0.57;filter:blur(30px);-webkit-mask-image:linear-gradient(to bottom,transparent 0%,rgba(0,0,0,.5) 20%,#000 36%,#000 64%,rgba(0,0,0,.5) 80%,transparent 100%);mask-image:linear-gradient(to bottom,transparent 0%,rgba(0,0,0,.5) 20%,#000 36%,#000 64%,rgba(0,0,0,.5) 80%,transparent 100%)}

/* Accent on near-black has far more contrast to spend than accent on cream:
   the same alphas that read as a soft light in the light theme rendered a gold
   searchlight across the dark one. */
[data-theme="dark"] .stat-glow{background:radial-gradient(58% 110% at 20% 50%,var(--accent-soft) 0%,transparent 70%),radial-gradient(52% 105% at 80% 50%,var(--accent-soft) 0%,transparent 72%),var(--card-bg);opacity:0.82}

/* Two columns across the FULL container, not a cluster centred in it. The
   number holds the left edge, the statement runs to the right edge, and the
   space between them is the composition rather than a gap left over.

   The previous version sized both columns to their content and centred the
   pair, which left the band two-thirds empty on a wide screen - measured, 600
   of 1352 available pixels were used.

   It stacks below 820px, where two columns leave the statement four words
   wide. */
.stat-figure{position:relative;display:grid;gap:1.75rem;text-align:center}
@media (min-width:820px){.stat-figure{grid-template-columns:auto minmax(0,1fr);gap:clamp(2.5rem,6vw,7rem);align-items:center;text-align:left}}
/* The number sits CENTRED over its caption, not flush with its left edge.
   The column is auto-width, so whichever of the two is wider sets the column
   (the caption today, the number once it reaches four digits) and the other
   centres inside it - the pairing reads as one figure either way, and the
   wider of the two still holds the container's left edge, which is what keeps
   the block aligned to the headline above.

   align-items and not text-align: the cross axis of a flex column is what
   places the whole child box, and the number is a box whose width is set by
   its digit cells rather than by text flow. */
.stat-count{margin:0;display:flex;flex-direction:column;align-items:center;gap:.375rem}

/* Sized against the sentence beside it, not against the page: big enough to be
   read first, small enough that the claim is read second rather than skipped.
   Tabular figures because the digits change while it counts, and proportional
   ones make the whole number shuffle sideways every frame. */
.stat-number{font-size:clamp(3.5rem,9vw,6.5rem);font-weight:700;line-height:.95;letter-spacing:-.03em;color:var(--accent-dark);font-variant-numeric:tabular-nums}

/* …except tabular-nums above is a request the display serif cannot grant: the
   Playfair Display subset we ship carries no tnum feature, so the declaration
   silently does nothing and the figures stay proportional - the 1 is 370/1000
   em against the 0's 600. Counting from 619 to 755 therefore re-measured the
   number on every frame, and because the figure is an auto-width grid column,
   the claim beside it was shoved back and forth with it. That is the shake.

   So the cells are built by hand instead: one per digit, all the same width,
   the figure centred in its cell - which is what tabular figures do
   internally. The number's width now depends only on how many digits it has,
   not which ones.

   The cell width is measured, not guessed. At the weight this is set in (700)
   the subset's figures advance 383-645 units per em, and their INK is at most
   555 wide (the 0). A full 1ch - the 0's 645-unit advance - is the obvious
   choice and is wrong: it leaves ~200 units of air around an average figure
   against the ~70 the face's own sidebearings give, and the number visibly
   falls apart into separate digits. .85ch (548) is the widest ink plus
   nothing: no figure can touch its neighbour, and the run sets only a few
   percent wider than the proportional original.

   Non-digits (the grouping separator, which is language-specific) keep their
   natural width - they never change while counting. */
.stat-digit{display:inline-block;width:.85ch;text-align:center}

/* No correction on the digit run. There was one here: while the number was
   flush left, the leading cell's own slack sat in FRONT of the first figure
   and pushed the whole number right of the caption under it, so the run was
   pulled back by that constant. Centring removes the problem rather than
   correcting it - the cells centre as a block, and the same shift would now
   push the number off centre by exactly as much as it used to pull it on. */
.stat-label{font-size:.9375rem;letter-spacing:.01em;opacity:.6}

/* The page's body face and the lede's measure. This is a sentence of landing
   copy that happens to stand next to a number; setting it in the display
   serif would make it a pull quote the page never asked for. */
.stat-say{display:flex;flex-direction:column;gap:.625rem;max-width:34ch;margin-inline:auto}
/* No measure cap once there are two columns: the column IS the measure, and
   capping it again is what left the statement floating with the band's right
   third empty. Pinning the block to the right edge instead only moved the
   hole to the middle. */
@media (min-width:820px){.stat-say{max-width:none;margin-inline:0}}

/* The display serif, the same face the headline above it wears - this is the
   page's one statement about what the number means, not a paragraph. (The
   mock it came from fell back to Georgia because the preview could not load
   the real face; the intent was always this one.)

   A statement, sized like one. At body-copy size it sat in the middle of a
   wide column as a small paragraph with a long empty tail; at this size it
   wraps to two full lines and the right half of the band is used by the
   sentence rather than by nothing. 'pretty' rather than 'balance': balancing
   evens the lines by SHORTENING them, which is the opposite of filling the
   space. Neither is 'pretty', which turned out to do the same thing: measured
   in a 1090px column it held the first line to 884px so the last line would
   not be short, leaving 224px of the band unused at every width above 1500.
   Plain wrapping fills each line and lets the last one be as short as it is. */
.stat-claim{margin:0;font-family:var(--font-serif);font-size:clamp(1.375rem,2.6vw,2.25rem);line-height:1.25;letter-spacing:-.01em;opacity:.9}

/* The conclusion, a step quieter than the claim it follows. It keeps the BODY
   face on purpose: serif for the statement, sans for the note it lands on,
   so the two read as one thought in two beats rather than as two competing
   sentences. */
.stat-claim-note{margin:0;font-size:clamp(.9375rem,1.1vw,1.0625rem);line-height:1.5;opacity:.55;text-wrap:pretty}

/* The entrance is a FADE, and nothing else.

   It used to travel 30px upward as well. The problem was not the distance but
   when it was spent: the observer releases a block once it is already inside
   the viewport, so the reader watches text that is plainly on screen get up and
   move. Reported as "these lines move when I go from one card to the next" -
   the embed section's bullets sliding while the pointer was two sections above
   them, measured at 30px over ~3 seconds. An entrance should be over before it
   is noticed; this one was the most noticeable thing on the page.

   Fading alone keeps what the effect was for - a block that assembles rather
   than snapping in - while never moving anything the reader might be reading.
   0.5s: long enough to register as arrival, short enough not to be watched. */
[data-reveal] .reveal{opacity:0}
[data-reveal] .reveal.is-in{opacity:1}

/* The 01 - 02 - 03 connector drawing itself left to right. This one carries
   meaning the others do not: the line IS the sequence, so it arrives in the
   order the reader is meant to travel it. */
[data-reveal] .reveal-rule{transform:scaleX(0);transform-origin:left center}
[data-reveal] .reveal-rule.is-in{transform:scaleX(1)}

@media (prefers-reduced-motion: no-preference){
[data-reveal] .reveal{transition:opacity .5s ease-out}
[data-reveal] .reveal-rule{transition:transform .8s cubic-bezier(.22,.61,.36,1)}
}

/* The embed section's code card used to drift here - scroll-LINKED rather than
   an entrance, so it kept moving for as long as the reader kept scrolling,
   travelling 40px (+20 to -20) across one pass. Removed: measured against the
   bullets beside it, the card was the only thing in that section changing
   position, and it read as the TEXT moving. Nothing on this page now moves
   itself while the reader is looking at it.

   A scroll-linked animation was also the one thing here that could never be
   composited-and-released: it never finishes, so its layer is held for the life
   of the page - the same cost that took 'view()' off the entrances above. */

/* ── the embed snippet typing itself in (components/CodeType) ─────────────
   Each line steps its own width from nothing to its character count, after
   every line above it has finished plus a beat. The widths are in 'ch', one
   character of a monospace face, which is also why the lines must not wrap: a
   wrapped line has no single width to step through. The card scrolls sideways
   on a narrow screen instead.

   THE CURSOR sits OUTSIDE the clipped text and is stepped along by the same
   timing, so it always lands one cell past the last character drawn. Putting
   it inside the clip was tried and cannot work: pinned to the right edge it
   covers the letter just typed, and reserving a character of width for it
   makes every line reveal its first character before it starts typing - which
   left a stray '>' sitting under the finished snippet.

   Two animations, because one property cannot do both jobs: 'ct-gate' switches
   VISIBILITY off (so only one line at a time shows a cursor), while 'ct-blink'
   does the flashing in opacity. The gate runs a line's duration PLUS the pause
   after it, so during the beat between lines the cursor waits at the end of
   the finished line rather than blinking out of existence. The last line keeps
   its cursor for good - a listing that has finished still has a prompt.

   None of this runs until CodeType adds .ct-run, and the lines are at their
   full width without it - no script, no observer, no animation support, and
   the reader still gets the finished snippet. */
.ct{overflow-x:auto}
.ct-line{position:relative;display:block}
.ct-text{display:block;white-space:pre;overflow:hidden;width:calc(var(--chars,0) * 1ch)}
.ct-cursor{position:absolute;left:0;top:.15em;width:1ch;height:1em;background:currentColor;visibility:hidden;transform:translateX(calc(var(--chars,0) * 1ch))}
.ct-run .ct-text{animation:ct-type var(--dur,0ms) var(--delay,0ms) both}
.ct-run .ct-cursor{animation:ct-walk var(--dur,0ms) var(--delay,0ms) both}
.ct-run .ct-cursor{animation:ct-walk var(--dur,0ms) var(--delay,0ms) both,ct-gate var(--gate,0ms) steps(1) var(--delay,0ms) forwards,ct-blink 1.06s steps(1) var(--delay,0ms) infinite}
.ct-run .ct-cursor-last{animation-name:ct-walk,ct-gate-in,ct-blink}
@keyframes ct-type{from{width:0}to{width:calc(var(--chars,0) * 1ch)}}
@keyframes ct-walk{from{transform:translateX(0)}to{transform:translateX(calc(var(--chars,0) * 1ch))}}
@keyframes ct-gate{from{visibility:visible}to{visibility:hidden}}
@keyframes ct-gate-in{from{visibility:visible}to{visibility:visible}}
@keyframes ct-blink{0%,49%{opacity:1}50%,100%{opacity:0}}

/* The download section's artwork used to keep its float and its glow here.
   They moved to lib/getAppCss with the rest of that section, so its layout and
   its motion cannot arrive separately. */


.soft-band{--band:var(--card-bg);background:var(--band)}
/* Fades in at its top edge, solid the rest of the way down. */
.soft-band-in{background:linear-gradient(to bottom,transparent 0,var(--band) 10rem,var(--band) 100%)}
/* Solid from its top, dissolving away at the bottom edge. */
.soft-band-out{background:linear-gradient(to bottom,var(--band) 0,var(--band) calc(100% - 10rem),transparent 100%)}
/* Both ends, for a tinted section that stands alone rather than joining a
   neighbour. */
.soft-band-in.soft-band-out{background:linear-gradient(to bottom,transparent 0,var(--band) 10rem,var(--band) calc(100% - 10rem),transparent 100%)}
/* A LIGHT, not a band - the same device the booking counter uses above (see
   lib/statFigureCss): two soft lobes of the shared --accent-soft token that
   dissolve in every direction, laid over the region's own tint. A warm tone
   spread across the full width, however gentle, is still a rectangle and the
   eye finds its ends; a light has no ends to find.

   Built as the section's own layered background rather than as the counter's
   masked, blurred, negative-z child. That child is what makes the counter's
   wash able to BLEED past its section - it hangs 18rem into the hero above -
   and it costs a composited layer the size of the section to do it. Nothing
   here needs to escape the section, so the lobes are simply kept inside it:
   a vertical radius of 46% with the last stop at 72% means each one has
   already faded out well before the edge, which is the same seamlessness the
   mask buys, for free. No pseudo-element, no layer, no dependency on the page
   wrapper's stacking context.

   The third layer is --band, so the section still sits at the same tint as
   its neighbours: the light picks it out WITHIN the region instead of cutting
   it out of it. Declared last so it wins over .soft-band's flat background.

   ONE lobe, where the counter has two. Its pair is not decoration: the counter
   is weighted left because the number is, and the second lobe is what stops
   the right half going dark. This section is centred - heading, three steps
   spread across the container, button - so a single wide ellipse lights all of
   it evenly. Two lobes here would overlap in the middle and stack to
   rgb(219,203,175), darker than the counter ever gets, with the deepest point
   landing on step 02 for no reason.

   Strength is the token used neat, which lands the peak at rgb(227,215,194)
   over the region's tint - measured, the same peak the counter's wash reaches
   after its 0.57 scaling. Matching it is the point: the two washes are meant
   to read as one page's lighting. */
.soft-band-swell{background:radial-gradient(72% 46% at 50% 50%,var(--accent-soft) 0%,transparent 72%),var(--band)}

.ga{display:grid;grid-template-columns:minmax(0,1fr);gap:3rem;align-items:center}
/* Artwork on the LEFT, copy on the right - the reverse of the DOM order, which
   stays copy-first so a phone (and a screen reader) still gets the headline
   before the picture. Safe to reorder visually only because the artwork holds
   nothing focusable: the chips are text and the image is decorative, so the tab
   sequence is unaffected by the swap. */
@media (min-width:880px){
.ga{grid-template-columns:minmax(0,1.08fr) minmax(0,.92fr);gap:clamp(2.5rem,5vw,4.5rem)}
.ga-stage{order:1}
.ga-copy{order:2}
}

/* ── the copy ─────────────────────────────────────────────────────────── */
.ga-copy{text-align:center}
.ga-title{margin-top:1rem;font-family:var(--font-serif);font-weight:700;font-size:clamp(1.8rem,3.2vw,2.5rem);line-height:1.14;letter-spacing:-.01em;text-wrap:balance}
.ga-sub{margin:1rem auto 0;max-width:30rem;opacity:.7;line-height:1.65}
/* Pushed to the RIGHT edge of its column, and everything in it shares one
   width. Before this the paragraph and the store panel were capped at 30rem
   and left-aligned in a 38rem column, so they ended 127px short of the
   heading above them - the block read as drifting left, away from the page's
   own right margin, with a gap beside it that belonged to nothing. Capping the
   whole column instead and letting 'margin-left:auto' carry it right gives the
   copy a shared left AND right edge, and lands that edge where the rest of the
   page ends. */
@media (min-width:880px){
.ga-copy{text-align:left;max-width:34rem;margin-left:auto;margin-right:0}
.ga-sub{max-width:none;margin-left:0;margin-right:0}
}

/* ── the stores, as ONE object ────────────────────────────────────────────
   Two free-standing cards read as two small flat tiles on a flat band; the
   section's own complaint was that both halves were lapos. One panel with a
   head, a hairline between the stores and a footer is a single thing with
   structure, and it can carry real elevation: the shadow lifts it off the
   band, and the inset top highlight is the light catching its top edge. */
/* Same 30rem as the paragraph above it, so the copy column reads as one stack
   with a shared left edge and a shared right one, rather than a paragraph and
   a narrower box that happen to be under each other. */
.ga-panel{margin:2rem auto 0;max-width:30rem;text-align:left;border:1px solid var(--border);border-radius:1.6rem;background:var(--background);box-shadow:var(--shadow-lg),inset 0 1px 0 rgba(255,255,255,.6);padding:1.1rem 1.1rem 0}
@media (min-width:880px){.ga-panel{max-width:none;margin-left:0;margin-right:0}}
.ga-head{display:flex;align-items:center;gap:.5rem;font-size:.7rem;font-weight:600;letter-spacing:.09em;text-transform:uppercase;color:var(--accent)}
.ga-head svg{flex:none;opacity:.85}
.ga-stores{display:grid;grid-template-columns:1fr 1fr;margin-top:.85rem}
.ga-store{display:flex;flex-direction:column;align-items:center;justify-content:flex-end;gap:.75rem;padding:0 .8rem 1rem}
.ga-store+.ga-store{border-left:1px solid var(--border)}

/* A third plane inside the panel, and the one place the page is allowed a
   plain white surface: a QR is dark-on-light by specification, so the tile is
   fixed rather than themed - var(--foreground) modules would invert with the
   theme and stop scanning. */
.ga-qr{display:block;line-height:0;padding:.5rem;border-radius:.8rem;background:#fff;border:1px solid var(--border);box-shadow:var(--shadow-md)}

.ga-btn{display:flex;width:100%;align-items:center;justify-content:center;gap:.5rem;padding:.55rem .5rem;border-radius:.9rem;border:1px solid var(--border);background:var(--card-bg);color:var(--foreground)}
.ga-btn small{display:block;font-size:.6rem;letter-spacing:.09em;text-transform:uppercase;opacity:.55;line-height:1.35}
.ga-btn strong{display:block;font-size:.83rem;font-weight:600;line-height:1.25;white-space:nowrap}
/* No listing yet, so this is a state, not a control. */
.ga-btn-soon{opacity:.85;cursor:default}
/* ...and when there is one, it becomes the section's only real button. */
.ga-btn-live{background:var(--foreground);color:var(--background);border-color:transparent;transition:transform .18s ease,box-shadow .18s ease}
.ga-btn-live:hover{transform:translateY(-1px);box-shadow:var(--shadow-md)}

.ga-foot{margin:0 -1.1rem;padding:.65rem 1.1rem;border-top:1px solid var(--border);font-size:.7rem;line-height:1.5;opacity:.55}

/* ── the artwork ──────────────────────────────────────────────────────────
   The cap lives here rather than on the element, because an artwork whose
   width depends on a class that might not arrive is an artwork that can turn
   into a full-bleed blob. 'isolation' keeps the glow's negative z-index local
   to the stage instead of resolving against whichever ancestor happens to
   have a stacking context that day. */
.ga-stage{position:relative;isolation:isolate;width:100%;max-width:26rem;margin:0 auto}
@media (min-width:880px){.ga-stage{max-width:46rem;margin-left:0;margin-right:auto}}

/* drop-shadow, not box-shadow: it follows the PNG's alpha, so the blob the
   phones stand on casts a real shadow on the page instead of a rectangle. It
   is the single thing that stops the composite reading as pasted on. */
.ga-art{display:block;width:100%;height:auto;filter:drop-shadow(0 30px 55px rgba(44,36,32,.22));animation:ga-float 13.7s ease-in-out infinite}

/* The light behind it moves on its own, slower clock and in the opposite
   direction. Two layers travelling together read as one flat sticker; two
   layers travelling differently read as depth - which is the whole job here,
   and it costs no scroll listener.

   It has to reach well PAST the artwork to do anything: the blob is opaque, so
   a glow tucked behind it is a glow nobody sees. The insets put its falloff
   outside the silhouette, where it lands on the band. */
.ga-glow{position:absolute;z-index:-1;inset:-22% -16% -26% -14%;pointer-events:none;background:radial-gradient(58% 55% at 50% 46%,var(--accent-soft-strong) 0%,transparent 68%),radial-gradient(48% 50% at 76% 64%,var(--accent-soft) 0%,transparent 72%);filter:blur(58px);animation:ga-breathe 19.3s ease-in-out infinite}

/* ── the promises around the artwork ──────────────────────────────────────
   The same pills the hero flies, deliberately: they reuse '.dh-chip-body',
   '.dh-chip-dot' and '.dh-drift' from lib/heroCss rather than restating the
   look here, so the two sections cannot drift apart in padding, radius, shadow
   or breath. Only the parking spots are local.

   WHERE they park was measured off the artwork's own alpha channel at a 48x30
   grid, not guessed. The composite is a blob with three phones on it and it
   leaves exactly two usable holes:

     top-left      opaque edge runs 71% across at y=0, 46% at y=7%,
                   35% at y=13% - so it closes fast as you go down
     bottom-right  free from x=67%, but only from y=77% DOWN: at y=73% the
                   third phone still runs from 73% to 92% across, which is
                   what the first attempt at slot 3 clipped (top:76% put the
                   pill's edge at 75.7% and it landed on the handset)

   Everything else is picture. Two pairs, one hole each, staggered horizontally
   and 2.3% apart vertically - the same 15px between a pair's members that the
   hero settled on. */
.ga-chip{display:none}
@media (min-width:1440px){
.ga-chip{display:block;position:absolute}
/* Top-left. Chip 1 is the deeper of the two, so it takes the shallower slot
   where the blob has not arrived yet. */
.ga-chip-1{left:8%;top:1%}
.ga-chip-2{left:0;top:8.5%}
/* Bottom-right, anchored to the stage's right edge - that edge is the column
   boundary, and there is a full gap beyond it before the copy starts. */
.ga-chip-3{right:0;top:79%}
.ga-chip-4{right:8%;top:86.5%}
}

/* TRANSLATE AND OPACITY ONLY, both of them - the same rule the hero states and
   for the same reason. A permanently animating layer stays composited, and a
   keyframe that changes SCALE or ROTATION makes the browser re-rasterise it at
   every step; on a 736x654 image carrying a drop-shadow and a 957x967 backdrop
   carrying a 58px blur, that is a lot of raster to redo forever. The first
   version of these had a 0.35deg tilt and a 1.05 scale in them. Neither was
   worth what it cost, and 'will-change:transform' on the image made it worse by
   pinning the layer whether or not it was moving. */
@keyframes ga-float{0%,100%{transform:translate3d(0,0,0)}50%{transform:translate3d(0,-15px,0)}}
@keyframes ga-breathe{0%,100%{transform:translate3d(0,0,0);opacity:.72}50%{transform:translate3d(0,12px,0);opacity:.92}}
