/* Danjoo blog chrome — styled to be IDENTICAL to the main site's "Our Story" overlay page
   (#storyPage in index.html): textless moving psychedelic band, round × close button, the
   Our-Story type scale, live psychedelic ring "windows" around images, no footer.
   Served as a plain static file from the MAIN site's public/ so the root-absolute
   url('/assets/...') refs are never rebased by Astro/Vite's css pipeline (base '/blog'). */

:root { --purple:#4F1B8F; --lav:#8a6fd6; --ink:#2D1B4D; --body:#3a2c55; --muted:#5b4a78; --vio:#7b2ff7; }
@font-face { font-family:'Archivo'; src:url('/assets/fonts/Archivo-500.woff2') format('woff2'); font-weight:400 500; font-display:swap; }
@font-face { font-family:'Archivo'; src:url('/assets/fonts/Archivo-700.woff2') format('woff2'); font-weight:600 700; font-display:swap; }
@font-face { font-family:'Archivo'; src:url('/assets/fonts/Archivo-900.woff2') format('woff2'); font-weight:800 900; font-display:swap; }
* { box-sizing:border-box; }
/* Our Story body copy: 500 clamp(1rem,1.4vw,1.125rem)/1.65 #3a2c55 */
body { margin:0; background:#fff; color:var(--body); font:500 1.0625rem/1.65 'Archivo',sans-serif; -webkit-font-smoothing:antialiased; }
main p, article p, .card p { font-size:clamp(1rem,1.4vw,1.125rem); line-height:1.65; }
a { color:var(--purple); text-decoration-thickness:2px; text-underline-offset:3px; }
a:focus-visible { outline:3px solid var(--lav); outline-offset:3px; border-radius:2px; }
img { max-width:100%; }

/* round × close button — same as #spClose on the Our Story page */
.spClose { position:fixed; top:26px; right:28px; z-index:82; width:46px; height:46px; border-radius:50%;
           background:var(--ink); color:#fff; font-size:1.6rem; line-height:1; text-decoration:none;
           display:flex; align-items:center; justify-content:center; }
.spClose:hover { background:var(--purple); }
@media (max-width:768px) {
  .spClose { top:clamp(10px,3vw,16px); right:clamp(10px,3vw,16px); width:clamp(32px,8.5vw,42px); height:clamp(32px,8.5vw,42px); font-size:clamp(1.1rem,4vw,1.5rem); }
}

/* top psychedelic band (moving), NO text — same device as #storyPage's .mPlane strip.
   `.psy` is the one rotating-pattern plane used everywhere on these pages: the band, the ring
   windows around images, the partner logos, the device icons, the benefit rings. It is a
   compositor-only @keyframes rotation, NOT a JS transform — which is the whole reason these
   pages can carry the main site's look without the main site's per-frame cost. */
.band { position:relative; overflow:hidden; height:clamp(90px,14vh,150px); }

/* A PLANE IS SIZED BY ITS OWN CLIP, NEVER BY A CONSTANT. Read this before enlarging one.
   Each plane is an animated `transform`, so it is a compositor layer, so it costs w × h × 4 bytes
   of GPU texture whether or not any of it is visible. These planes used to be a flat 1200 × 1200
   each — which is 6 MB apiece, sitting inside circles as small as 68 px (a 332× oversize). At
   1366×768 Cultural Awareness carried ~134 MB of texture that way, and ~184 MB on a big external
   monitor. A 4 GB laptop on shared graphics blows its tile budget well before that, and then
   re-rasters evicted tiles mid-scroll — which is what the client saw as the page stopping for a
   moment and then resuming.

   The geometry that makes the fix free: a square rotated about its centre ALWAYS contains its own
   inscribed circle, because the distance from the centre to every edge stays half the side. So a
   circular window of diameter D needs a plane of exactly D — no margin at all — and the visible
   crop is unchanged, because `background-position:center` centres the pattern on the plane and the
   plane is centred on the window either way. That is the default below.

   A RECTANGULAR window is the exception: it is not inscribed in the plane, so the plane must cover
   its DIAGONAL at every angle. Those two get an explicit oversize, and no others should. */
.psy, .ring .mp { position:absolute; inset:0;
  background:url('/assets/min/Psychedelic.webp') center/120vmax repeat; animation:bandSpin 40s linear infinite; }

/* NON-CIRCULAR WINDOWS NEED THE DIAGONAL, and `inset:0` does not give it to them.
   `inset:0` is right for a CIRCLE — a square rotated about its centre always contains its own
   inscribed circle. It is wrong for anything else: the corners of a rectangle, and the reach of a
   silhouette mask, sit OUTSIDE that circle, so they empty out as the sheet turns. That is what made
   the Values card read as a half-filled border rather than a full frame. Cover the diagonal
   instead — a square at 150% of the box clears √2 with room to spare at any aspect ratio.
   `translate` rather than a negative margin, because a percentage margin resolves against the
   containing block's WIDTH on both axes and would mis-centre a card that is wider than it is tall;
   and it is its own property, so the rotation still owns `transform`. */
.dev > .psy {
  inset:auto; left:50%; top:50%; width:150%; height:auto; aspect-ratio:1; translate:-50% -50%; }

/* THE VALUES CARD BORDER DOES NOT TURN — ONLY ITS ICON DOES (client, 2026-08-07)
   The DC rotates both (the card's frame is an `.mPlane` like everything else), and the client's
   call is that it should not: "we want [the icon] to rotate, not each card background." With five
   cards on screen at once, five turning frames read as the page itself being restless, and they
   pull the eye away from the one element that is meant to be moving.

   A still sheet wants NEITHER of the two sizing rules above. The diagonal oversize exists so the
   corners do not empty out AS IT TURNS; nothing turns here, so `inset:0` fills the card exactly —
   which is both the correct crop and the cheapest one (a 343x203 card carried a 581px square; it
   now carries the card). It also stops being a compositor layer at all, since an animated
   transform is what promoted it. Do not re-add the rotation without re-adding the oversize with
   it — a static plane at 100% starts emptying its corners the moment it moves.

   Dropping it from the diagonal rule above is the whole geometry change — the base `.psy` is
   already `inset:0`, which fills the card exactly. Do NOT restate the insets here: `left:auto` and
   `top:auto` beat two of `inset:0`'s four sides, leaving `right`/`bottom` anchored with `auto`
   dimensions, and the sheet collapses to 0x0 — the border silently disappears. */
.vals li > .psy { animation:none; }

/* the band is full-width and short: its diagonal is a hair over its width */
.band .psy { inset:auto; left:50%; top:50%; width:104vw; height:104vw; margin:-52vw 0 0 -52vw; }
/* the hero is the whole viewport: diagonal = √(vw² + vh²), which is at most √2 × vmax */
.bridge .psy { inset:auto; left:50%; top:50%; width:142vmax; height:142vmax; margin:-71vmax 0 0 -71vmax; }
@keyframes bandSpin { from { transform:rotate(0deg); } to { transform:rotate(360deg); } }

/* page header — Our Story eyebrow + title scale */
.pageHead { text-align:center; padding:clamp(36px,6vw,72px) clamp(22px,6vw,60px) 0; }
.eyebrow { font:700 clamp(0.68rem,0.95vw,0.82rem)/1 'Archivo'; letter-spacing:.34em; text-transform:uppercase; color:var(--vio); margin-bottom:14px; }
.pageHead h1 { margin:0; font-weight:900; font-size:clamp(2.1rem,4vw,3rem); line-height:1.06; letter-spacing:-0.02em; color:var(--purple); }
.crumbs { font-weight:600; font-size:0.85rem; color:var(--muted); margin-bottom:clamp(16px,2.4vw,26px); }
.crumbs a { font-weight:700; text-decoration:none; }
.crumbs a:hover { text-decoration:underline; }
.crumbs span { color:var(--muted); }
.byline { font-weight:700; font-size:clamp(0.8rem,1.05vw,0.92rem); letter-spacing:.06em; text-transform:uppercase; color:var(--vio); margin:14px 0 0; }
.lede { max-width:62ch; margin:clamp(20px,3vw,34px) auto 0; color:var(--body); }

/* ===== Listing ===== */
.posts { list-style:none; margin:clamp(30px,6vh,56px) auto 0; padding:0 24px max(clamp(60px,8vw,110px), var(--hintSpace)); max-width:860px; display:grid; gap:26px; }
.card { display:flex; gap:clamp(18px,3.5vw,34px); align-items:center; padding:22px 26px; border:1px solid #e8e2f4;
        border-radius:22px; text-decoration:none; transition:border-color .25s, transform .25s, box-shadow .25s; }
.card:hover { border-color:var(--lav); transform:translateY(-2px); box-shadow:0 14px 40px rgba(45,27,77,0.10); }
.card:hover h2 { color:var(--purple); }
/* live psychedelic ring "window" around each cover — same device as the Our Story headshots
   (rotating pattern plane clipped to a circle, white inner circle holding the photo) */
.ring { position:relative; overflow:hidden; flex:0 0 clamp(96px,14vw,132px); width:clamp(96px,14vw,132px); height:clamp(96px,14vw,132px);
        border-radius:50%; display:flex; align-items:center; justify-content:center; }
.ring .ph { position:relative; z-index:1; width:90%; height:90%; border-radius:50%; overflow:hidden; background:#fff; display:block; }
.ring .ph img { width:100%; height:100%; object-fit:cover; display:block; }
.card h2 { margin:6px 0 8px; font-weight:900; font-size:clamp(1.2rem,2.4vw,1.5rem); line-height:1.15; letter-spacing:-0.01em; color:var(--ink); transition:color .25s; }
.card p { margin:0; color:var(--body); }
.meta { font:700 clamp(0.68rem,0.95vw,0.82rem)/1 'Archivo'; letter-spacing:.34em; text-transform:uppercase; color:var(--vio); }
@media (max-width:560px) { .card { flex-direction:column; text-align:center; } }
.pager { display:flex; justify-content:center; align-items:center; gap:18px; padding-bottom:clamp(60px,8vw,110px); margin-top:38px; font-weight:700; }
.pager a { text-decoration:none; border:2px solid var(--purple); border-radius:999px; padding:10px 26px; transition:background .25s, color .25s; }
.pager a:hover { background:var(--purple); color:#fff; }
.pager .pg { color:var(--muted); font-size:0.9rem; letter-spacing:0.12em; }

/* ===== Post body ===== */
.wrap { max-width:760px; margin:0 auto; padding:0 24px clamp(60px,8vw,110px); }

/* POST PHOTOGRAPHY — the featured image under the byline, and any image inside the article body.
   Both are real photographs mirrored from the source blog, so they get the same plain treatment as
   the team shot on Our Story: a rounded rectangle and a soft shadow, no rotating plane behind them.
   Every one is 1700x1200 at source and ships at 1400x988, and the markup carries those dimensions —
   which is what keeps the article text from jumping as they decode. */
.postCover, .wrap :is(p, figure) > img { margin:clamp(22px,3.4vw,34px) 0 0; }
.postCover { margin-inline:0; }
.postCover img, .wrap :is(p, figure) > img {
  display:block; width:100%; height:auto; border-radius:18px;
  box-shadow:0 14px 40px rgba(45,27,77,0.14); }
.wrap figcaption { margin-top:10px; font-weight:600; font-size:0.84rem; color:var(--muted); }
article .pageHead { padding-left:0; padding-right:0; }
article h2 { margin:2.2em 0 0.6em; font-weight:900; font-size:clamp(1.7rem,3vw,2.4rem); line-height:1.15; letter-spacing:-0.02em; color:var(--lav); }
article li { margin:0.45em 0; }
article li::marker { color:var(--purple); font-weight:900; }
article blockquote { position:relative; margin:1.8em 0; padding:2px 0 2px 22px; font-weight:700; color:var(--ink); }
article blockquote::before { content:''; position:absolute; left:0; top:0; bottom:0; width:6px; border-radius:3px;
  background:url('/assets/min/Psychedelic.webp') center/500%; }
article em { color:var(--ink); }
article strong { color:var(--purple); }

/* ============================================================================================
   THE BRIDGE — full-bleed psychedelic hero that collapses into the page's title band.
   Driven by /assets/bridge.js; see that file for the choreography and why it is cheap.

   PROGRESSIVE ENHANCEMENT CONTRACT: everything below styles the SETTLED state (a plain purple
   title band, already correct, already in flow). The `.jsBridge` variants — added to <html> by
   an inline head script only when JS runs AND the reader has not asked for reduced motion —
   promote it to the full-screen hero that bridge.js then collapses. If the script never runs,
   the page is simply a document with a band on top. The hero can never be left covering it.
   ============================================================================================ */
:root { --bandH:clamp(90px,14vh,150px); }
/* The room the fixed step hint needs at the foot of every screen. It is a reserve, not a gap: the
   hint is `position:fixed`, so nothing in flow knows it is there and copy would otherwise run
   underneath it — most visibly the closing CTA, which sits lowest on its slide.
   It answers to vh, not to a constant. A flat 58px floor cost two sections their fit on a 600px
   laptop (`verify:fit`: 617px in 600px) — a short screen has no spare height to give away, and the
   hint is smaller there too, since its own offset is `3.4vh`. The floor is what the hint actually
   occupies at that size: ~20px of offset plus its one line. */
:root { --hintSpace:clamp(36px,6.5vh,88px); }

.bridge { position:relative; overflow:hidden; z-index:60; display:flex; flex-direction:column;
          align-items:center; justify-content:center; text-align:center;
          padding:30px clamp(22px,6vw,60px); min-height:var(--bandH); }
.jsBridge .bridge { position:fixed; inset:0; padding:6vw; min-height:0; }

/* THE SETTLED BAND, ONCE <main> COVERS THE HERO. The element stays — the client wants the title
   strip to stick — but it stops costing a viewport. The sheet drops from 142vmax (the hero's
   diagonal) to 104vw (the strip's, the same figure `.band .psy` uses) and is re-centred on the
   strip rather than on the screen. `translate` for that centring, never a percentage margin: a
   percentage margin resolves against the containing block's WIDTH on both axes and would push it
   off vertically. And `pointer-events:none`, because the hero is `inset:0` and only LOOKS like a
   strip — it is clipped, not resized, so without this it would swallow every click on the page. */
.jsBridge .bridge.bandOnly { pointer-events:none; }
.jsBridge .bridge.bandOnly .psy { width:104vw; height:104vw; margin:0;
                                  left:50%; top:calc(var(--bandH) / 2); translate:-50% -50%; }

/* the hero plane's oversize is set with the other plane geometry, at the top of this file */
.bridgeScrim { position:absolute; inset:0; background:rgba(16,2,38,0.8); pointer-events:none; }

/* Settled sizes are the hero sizes × 0.6 — the scale bridge.js lands on — so the JS and no-JS
   bands are the same silhouette and there is no step when the collapse finishes. */
.bridgeTitle { position:relative; margin:0; font-weight:900; font-size:clamp(1.02rem,3vw,1.88rem);
               line-height:1.05; letter-spacing:-0.02em; color:#fff; text-wrap:balance;
               text-shadow:0 4px 30px rgba(8,1,24,0.6); transform-origin:top center; }
.jsBridge .bridgeTitle { font-size:clamp(1.7rem,5vw,3.125rem); will-change:transform; }
.bridgeBody { display:none; }
.jsBridge .bridgeBody { display:block; position:relative; margin:clamp(1rem,2.6vw,2rem) 0 0; max-width:46ch;
               font-weight:600; font-size:clamp(1.05rem,1.7vw,1.25rem); line-height:1.5;
               color:rgba(255,255,255,0.95); text-shadow:0 2px 14px rgba(8,1,24,0.6);
               transform-origin:top center; will-change:transform,opacity; }
/* THE STEP HINT — page chrome, not part of the hero. Lifted from the main site's `#stepHint`:
   the word plus a flowing gradient rule, fixed at the bottom, never interactive. `--hintInk` is
   what bridge.js writes as the hero collapses, so the ink follows the backdrop instead of being
   guessed: white over the full-bleed hero, brand purple over the white page. */
.stepHint { position:fixed; left:50%; bottom:clamp(18px,3.4vh,40px); transform:translateX(-50%);
            z-index:61; display:inline-flex; align-items:center; gap:16px; pointer-events:none;
            white-space:nowrap; color:var(--hintInk, var(--purple));
            font:700 clamp(0.62rem,0.85vw,0.78rem)/1 'Archivo'; letter-spacing:.3em;
            text-transform:uppercase; }
/* A SOFT FLOOR UNDER IT. On a snapping page the hint sits over the section's own bottom margin and
   never touches anything. The blog listing and a post are ordinary long-form scrolling, so a fixed
   element WILL pass over body copy no matter how much end-of-page reserve is given — measured, it
   landed straight across a card's excerpt. This fades the last few centimetres of the page to
   white behind the hint so it stays readable and the copy stays legible under it. Sized in `em` so
   it tracks the hint, and `pointer-events:none` like everything else in this chrome. */
.stepHint::before { content:''; position:absolute; left:50%; translate:-50% 0;
                    bottom:calc(-1 * clamp(18px,3.4vh,40px)); width:min(100vw,52em); height:5.4em;
                    background:linear-gradient(rgba(255,255,255,0), rgba(255,255,255,0.92) 62%);
                    pointer-events:none; z-index:-1; }
.jsBridge .bridge:not(.bandOnly) ~ .stepHint::before { opacity:0; }  /* not over the dark hero */
.stepBar { display:block; flex:none; width:clamp(34px,4.5vw,60px); height:3px; border-radius:2px;
           background:linear-gradient(90deg,#7b2ff7,#ff5a1f,#ffd400,#18c8c8,#7b2ff7);
           background-size:250% 100%; animation:stepFlow 2.6s linear infinite; }
@keyframes stepFlow { to { background-position:250% 0; } }
@media (prefers-reduced-motion:reduce) { .stepBar { animation:none; } }

/* The scroll distance the collapse is spread over. bridge.js reaches p===1 at
   (100vh - bandH), which leaves exactly bandH of this spacer under the settled band — so <main>
   begins flush beneath it with no padding of its own. */
.bridgeSpacer { display:none; }
.jsBridge .bridgeSpacer { display:block; height:100vh; }
.jsBridge main { position:relative; z-index:1; background:#fff; }

/* ===== Story-page sections (Cultural Awareness, Our Story) =====
   The deck gave every section the whole screen, dead-centre, on BOTH the desktop and the mobile
   DC — so every section here is at least a screen tall and centres its content, and the page
   snaps between them (proximity, never mandatory: a section taller than the viewport, and the
   bridge collapse above the first one, must always stay freely scrollable). */
/* WIDTH IS WHAT KEEPS A SECTION ON ONE SCREEN. This was 960px flat, which on a 1500×771 laptop
   forced the five Our Values cards into 3 columns — two rows, 336px-tall cards, an 844px section
   in a 771px viewport. A section taller than the screen cannot sit on its own snap point, so the
   heading ended up scrolled under the title band while half the screen either side sat empty.
   Prose does not get wider (`.sec > p` still caps at 64ch, which is the readable measure); the
   grids do, and that is what buys the height back. */
/* No vw term in this max-width. A `94vw` cap was tried and it regressed the phone: on a 390px
   screen it made every section 367px wide, inset 12px from both edges, on top of the section's own
   padding — content dropped from 343px to 320px and the whole page sat in a box. The gutter is the
   PADDING's job (`6vw`, up to 60px); the max-width's only job is to stop the line growing on a big
   monitor. */
.sec { max-width:1560px; margin:0 auto; padding:clamp(32px,6vh,96px) clamp(22px,6vw,60px); text-align:center;
       min-height:100svh; display:flex; flex-direction:column; justify-content:center; }
.sec { padding-bottom:max(clamp(32px,6vh,96px), var(--hintSpace)); }
.sec + .sec { padding-top:clamp(32px,6vh,96px); }
/* snapping rides with the deal-in: armed only when JS runs and motion is welcome */
.rvArm { scroll-snap-type:y proximity; }
.rvArm .sec { scroll-snap-align:start; scroll-snap-stop:normal; }

/* ===== THE DECK STAGE — five sections on one spot, crossing (2026-08-08) =====
   The DC's sections are absolutely positioned in the SAME place and only their x moves, which is
   the difference between a deck and a reveal. Reproducing that in a scrolling document is what the
   first build could not do: the outgoing section was leaving out of the top while the incoming
   arrived from the bottom, so the two had to travel on split clocks (ENTER_SPAN/EXIT_SPAN) and the
   kangaroo — which rides one clock, the design's — could never agree with either.

   Pinning is what makes the design's own numbers work: `.deckPin` sticks for the stage, every
   section is centred on it, and the `.deckStop`s are the scroll length AND the snap ruler. The
   sections themselves must NOT snap any more — they are absolute and would snap to the stage top
   all at once. Progressive enhancement is preserved by scoping every one of these rules to
   `.rvArm`: with no script the stage is plain flow and the sections read as a normal document,
   which is the same contract the ring carousel already had. */
/* THE PIN IS PROMOTED, AND THAT IS THE POINT (client, 2026-08-11, third report of the same thing:
   "when cards or rings slide or move, the text above that section is like moving up and down a
   little bit ... and this only happen in the pages not home page").
   Measured first, because two earlier fixes aimed at the wrong thing: stepping the carousel through
   its whole rail in 4px increments, the heading and the copy above it move 0.00px across 800+
   samples on all three deck pages. The layout is provably still, so this is not a layout bug — and
   the client's own observation says where it is. The HOME page never does this and its stage is
   `position:fixed`, which iOS pins to the VISUAL viewport; a `position:sticky` element is resolved
   against the LAYOUT viewport and updated on the main thread, so during a scroll it can lag a frame
   and visibly slide. In a pinned section, where the content is supposed to be nailed in place, a
   one-frame lag is exactly "moving up and down a little bit".
   Promoting the sticky element is the established remedy — it moves the position update onto the
   compositor. One viewport-sized layer per deck page (~1.3MB at 390x844), which is affordable; see
   the texture-budget note in CLAUDE.md before adding more.
   NOTE: this cannot be confirmed from here — headless WebKit does not reproduce it, the same wall
   the iOS mask bug and the broken-image bug hit. If it survives on device, the next step is
   structural: make the pin `position:fixed` like the home page's stage, toggled by reveal.js while
   the stage is in view. */
.rvArm .deckPin { position:sticky; top:0; height:100svh; overflow:hidden;
                  will-change:transform; }
.rvArm .deckPin .sec.deck { position:absolute; left:50%; top:0; width:100%;
                            height:100%; min-height:0; margin:0; translate:-50% 0; }
.rvArm .deckPin .sec.deck { scroll-snap-align:none; }
.deckStop { display:none; }
.rvArm .deckStop { display:block; height:100svh; scroll-snap-align:start; }
/* the two extra steps exist only where the rings page as clusters — the phone */
.rvArm .ringStop { display:none; }
@media (max-width:768px) { .rvArm .ringStop { display:block; } }
/* the rings section is a deck slide now, not a pin of its own: a pin inside a pin makes the outer
   transform the containing block for the inner sticky and the rail loses its ruler */
.rvArm .sec.carSec { display:flex; padding:clamp(32px,6vh,96px) clamp(22px,6vw,60px); min-height:0; }
.rvArm .carPin { position:static; height:auto; }
.sec h2 { margin:0; font-weight:900; font-size:clamp(1.7rem,3.4vw,2.5rem); line-height:1.1;
          letter-spacing:-0.01em; color:var(--purple); text-wrap:balance; }
.sec > p { max-width:64ch; margin:clamp(1rem,2vw,1.5rem) auto 0; text-wrap:pretty; }
/* the psychedelic-filled body copy from the main site's .kdBody — the pattern shows THROUGH the
   glyphs, tinted purple. Pure background-clip, no canvas and no per-frame text repaint. */
.psyText { color:transparent; -webkit-text-fill-color:transparent; -webkit-background-clip:text; background-clip:text;
           background-image:linear-gradient(rgba(79,27,143,0.78), rgba(79,27,143,0.78)), url('/assets/min/Psychedelic.webp');
           background-size:auto, 120vmax; background-repeat:repeat, repeat; }
.kicker { font:700 clamp(0.68rem,0.95vw,0.82rem)/1.5 'Archivo'; letter-spacing:.28em; text-transform:uppercase;
          color:var(--vio); margin:0 0 12px; }

/* a psychedelic ring window: rotating pattern clipped to a circle, white inner disc holding the
   content. The main site's #cpS5Ring / #cpS2Logo / .ring device, as static CSS. */
.disc { position:relative; overflow:hidden; flex:0 0 auto; border-radius:50%;
        display:flex; align-items:center; justify-content:center; }
.disc > .in { position:absolute; inset:8px; border-radius:50%; background:#fff; }
.disc > .lbl, .disc > img { position:relative; z-index:1; }

.partners { display:flex; flex-wrap:wrap; gap:clamp(10px,2vw,24px); justify-content:center; align-items:center;
            margin:clamp(30px,5vw,54px) auto 0; }
.partners .disc { width:clamp(118px,13.7vw,192px); height:clamp(118px,13.7vw,192px); }
.partners .disc img { width:calc(100% - 46px); height:calc(100% - 46px); object-fit:contain; }

.devices { display:flex; flex-wrap:wrap; gap:clamp(14px,3vw,32px); justify-content:center; align-items:center;
           margin:clamp(30px,5vw,54px) auto 0; }
/* each icon is the device silhouette used as a MASK over the rotating pattern */
.dev { position:relative; overflow:hidden; width:clamp(40px,5.4vw,68px); height:clamp(40px,5.4vw,68px);
       -webkit-mask-size:contain; mask-size:contain; -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
       -webkit-mask-position:center; mask-position:center; }

/* The seven benefit rings — ONE list in the DC's reading order, laid out per DC:
     desktop (#cpS5Rings): overlapping rows of 4 + 3, all seven arriving together
     phone   (#cpS5Grp)  : three clusters — a pyramid then two pairs — PAGED one per screen
   Desktop is the default here and the clusters dissolve into it (`display:contents`), so the row
   break falls where the DC put it (`.b4`, after the fourth ring) regardless of which cluster it
   sits in. The phone layout is in the 768px block below.

   `white-space:nowrap` on the labels is not decoration: the rings overlap, so a label allowed to
   wrap on its own runs under its neighbour. The breaks come from the DC, per ring. */
.cars { display:flex; flex-wrap:wrap; justify-content:center; align-items:center;
        margin:clamp(30px,5vw,54px) auto 0; max-width:900px; }
.car { display:contents; }
.cars .brk { flex:0 0 100%; width:100%; height:0; display:none; }
.cars .b4 { display:block; }
/* A RING IS A SELF-SIMILAR UNIT. The disc and its label used to be sized on two independent
   curves — 11.6vw for the ring, 0.95vw for the type, each clamping at a different width — so the
   proportion drifted as the window changed and the nowrap label crept toward the edge of its own
   overflow:hidden circle. (That drift is what cut the labels on the main site's Kali rings; see
   the matching note in index.html.) The ring is a container now and everything inside it is a
   fraction of the ring, so the proportion is fixed at every size. Coefficients are today's
   largest desktop proportions: 14.08px label and a 9px inner inset in a 170px ring. */
.cars .disc { container-type:inline-size; width:clamp(140px,11.6vw,170px); height:clamp(140px,11.6vw,170px); margin:-20px -16px; }
.cars .disc > .in { inset:5.3cqw; }
.cars .lbl { margin:0; white-space:nowrap; font-weight:700; font-size:8.28cqw;
             line-height:1.45; color:var(--ink); }
.carPin > h2, .carPin > p { max-width:64ch; margin-left:auto; margin-right:auto; }
.carStop, .carOut { display:none; }

/* The disc is capped by viewport HEIGHT as well as width. A 1366×625 laptop is short, not
   narrow, so a purely vw-based size stayed at its 240px maximum there and pushed the section
   past one screen. The vh cap only ever engages on short viewports — above ~750px tall the
   width rule still wins, so nothing moves on a normal display. */
.photos { --ph:min(clamp(150px,22vw,240px), 32vh);
          display:flex; flex-wrap:wrap; gap:clamp(16px,3vw,34px); justify-content:center;
          margin:clamp(22px,4vh,48px) auto 0; }
.photos .disc { width:var(--ph); height:var(--ph); }
.photos .disc .ph { position:relative; z-index:1; width:calc(100% - 22px); height:calc(100% - 22px);
                    border-radius:50%; overflow:hidden; background:#fff; }
.photos .disc .ph img { width:100%; height:100%; object-fit:cover; display:block; }

/* ===== A DOCUMENTARY PHOTOGRAPH — the team shot on Our Story (client, 2026-08-08) =====
   Deliberately NOT the `.photos` treatment. Those are two studio headshots in rotating psychedelic
   discs; this is a real photograph of people outside a building, and putting a spinning sheet
   behind it would be a third compositor layer bought for nothing. A rounded rectangle with a soft
   shadow, no plane, no animation.

   `width:100%` next to the cap is load-bearing, and the reason is written out at `.people` below:
   `.sec` is a column flex container and this block carries auto inline margins, which disables
   `align-self:stretch` — so a bare `max-width` makes it size to fit-CONTENT (here, the image's
   intrinsic 1040px) instead of to the section. On a phone that overflows the screen.

   A PORTRAIT PHOTO IS A HEIGHT PROBLEM, so the cap carries a vh term like every other block on
   these pages. This one is 1040x1365 — at a flat 520px cap it is 683px tall, which put the section
   at 940px inside a 600px laptop (verify:fit). 42vh is the largest value that still clears the
   shortest laptop in the harness once the heading, the lede and the caption are counted. */
.shot { width:100%; max-width:min(520px, 38vh); margin:clamp(22px,4vh,48px) auto 0; }
.shot img { display:block; width:100%; height:auto; border-radius:22px;
            box-shadow:0 18px 50px rgba(45,27,77,0.16); }
.shot figcaption { margin:14px auto 0; max-width:52ch; text-align:center;
                   font-weight:600; font-size:0.86rem; line-height:1.5; color:var(--muted); }

/* `align-self:center` is what keeps this a BUTTON. `.sec` is a column flex container, so a flex
   item stretches to the cross axis by default — `display:inline-block` does not save it, and the
   pill quietly grew to the full width of the section. It was always doing this; widening the
   section is what made it obvious. */
/* THE BUTTON IS A BUTTON AT EVERY WIDTH (client, 2026-08-08: "the buttons need to be proper for
   both desktop and mobile"). `border-radius:999px` on a box whose label wraps to two lines is not a
   pill, it is a stadium — on a phone "Talk to us about your requirements" became a 100px-tall blob
   with the text floating in the middle of it. The label gets `text-wrap:balance` so it breaks
   evenly rather than leaving an orphan, a max-width so it never spans the full screen, and a
   radius that is a real corner rather than half the height. Padding answers to the text, not to a
   constant, so a two-line label is not padded like a one-line one. */
.cta { display:inline-flex; align-items:center; justify-content:center; align-self:center;
       margin:clamp(24px,4vw,44px) auto 0; padding:0.85em 1.9em; min-height:52px;
       max-width:min(100%, 30ch); border:2px solid var(--purple); border-radius:14px;
       font-weight:700; line-height:1.3; text-align:center; text-wrap:balance;
       color:var(--purple); text-decoration:none; transition:background .3s, color .3s; }
.cta:hover { background:var(--purple); color:#fff; }

/* people / values cards */
/* The bios do not get wider with the container — two columns of 760px would be well past a
   readable measure. Only the card grids spend the extra width. */
/* `width:100%` is REQUIRED next to that max-width, and the omission was a real phone bug.
   `.sec` is a column flex container and this grid has auto inline margins, which disable
   `align-self:stretch` — so the moment a max-width was added the grid stopped filling the section
   and sized to fit-content instead. For `repeat(auto-fit, minmax(280px,1fr))` with two people that
   resolves to its max-content, 2 × 280 + gap = 586px, which on a 393px phone laid the two bios
   side by side and ran the second one off the screen. An explicit width pins it back to the
   section, and the max-width goes back to doing only its intended job on a wide monitor. */
.people { display:grid; gap:clamp(26px,4vw,48px); grid-template-columns:repeat(auto-fit,minmax(280px,1fr));
          width:100%; max-width:1120px; margin:clamp(22px,4vh,48px) auto 0; text-align:center; }
.person h3 { margin:18px 0 4px; font-weight:900; font-size:clamp(1.15rem,2vw,1.4rem); color:var(--ink); }
.person .role { margin:0 0 12px; font:700 clamp(0.68rem,0.95vw,0.8rem)/1.4 'Archivo'; letter-spacing:.2em;
                text-transform:uppercase; color:var(--vio); }
.person p { margin:0; }

/* Flex, not grid, and centred — because these lists are 5 and 2 items long. An auto-fit grid
   leaves a ragged last row hard against the left edge (3 + 2, with a card-shaped hole beside it),
   which is what read as unbalanced. Wrapping flex items centre their last row, so a row that
   cannot be full at least looks deliberate.

   The basis only decides WHERE THE SET WRAPS, not how wide a card ends up: `flex-grow:1` then
   shares the whole row between them. So it is deliberately well below a card's finished width —
   all five Values stay on one row down to ~1150px of container, and on a wide screen they grow
   to ~265px each, the proportion the cards had before the row was widened. */
/* The COLUMN gap sets the layout (it is what the 1/3 basis below is measured against); the ROW
   gap only exists because there are two rows, so it is the one that gives way on a short screen.
   It reaches the full 28px by ~930px of viewport height and only tightens below that. */
.cards { --gap:clamp(18px,3vw,28px);
         display:flex; flex-wrap:wrap; justify-content:center;
         column-gap:var(--gap); row-gap:clamp(12px,2.4vh,28px);
         margin:clamp(16px,3vh,48px) auto 0; text-align:left; padding:0; }
/* THREE PER ROW, so the five Values read as 3 + 2 — the Vision & Mission shape, which is the
   proportion these cards were designed at. The basis is an exact third of the row rather than a
   round number, so the wrap point is arithmetic instead of a guess that drifts with the container:
   three always fit, four never do. `max-width` then holds a card to the Vision & Mission width, so
   a wide screen widens the MARGINS, not the cards, and the two lists match.

   `min-width:0` is load-bearing: a flex item defaults to `min-width:auto`, so its min-CONTENT width
   (the longest unbreakable word — "Responsibility") silently overrides the basis when the browser
   decides where to wrap. Without it the set wrapped 4 + 1 whatever basis was set. */
.cards li { flex:1 1 calc((100% - 2 * var(--gap)) / 3); min-width:0; max-width:360px;
            list-style:none; padding:26px 28px; border:1px solid #e8e2f4; border-radius:22px; }
/* A TWO-CARD LIST IS NOT A THREE-CARD LIST. The 360px cap exists so the five Values sit as 3 + 2
   at a comfortable width — but applied to Vision & Mission it left two cards filling barely half
   the section (748px of 1440) while Our Values filled 92% of it, and made them TALLER than the
   Values cards despite being the same width, because they carry more copy. Two cards get a wider
   cap so both lists fill the row and land on the same card height. `:has()` is the whole test —
   where it is unsupported the declaration is dropped and the 360px cap applies, i.e. today's
   behaviour.
   Scoped ABOVE the phone breakpoint on purpose: `.cards:has(…) li` out-specifies the `.cards li`
   rule in the 768px block, so without the media query it beat the phone's one-card-per-row and put
   two 163px cards side by side. */
@media (min-width:769px) {
  .cards:has(> :nth-child(2):last-child) li { flex-basis:calc((100% - var(--gap)) / 2); max-width:480px; }
}
.cards h3 { margin:0 0 8px; font-weight:900; font-size:clamp(1.05rem,1.8vw,1.25rem); color:var(--ink); }
.cards p { margin:0; }

/* ===== CERTIFICATION CARDS (/certifications/) =====
   Four cards, so they want a TWO-per-row basis: the shared `.cards` basis is an exact third and
   would lay four out as 3 + 1, leaving a card-shaped hole. Same reasoning — and the same media
   query — as the two-card `:has()` rule above: this selector out-specifies nothing, but it sits
   later in the file than `.cards li`, so without `min-width:769px` it would also beat the phone
   block's one-card-per-row further down. Keep it scoped.

   The badge sits on its own white plate at a FIXED height with `object-fit:contain`, because these
   four marks have different aspect ratios (Supply Nation is a wide lozenge, the ISO marks are tall
   roundels). Matching their heights rather than their widths is what makes the row read as a set. */
@media (min-width:769px) {
  .certs li { flex-basis:calc((100% - var(--gap)) / 2); max-width:480px; }
}
/* On a wide screen all four go on ONE row. Two rows of two put the section at 989px inside a 600px
   laptop (verify:fit) while a third of the width sat empty either side — the same shape of problem
   the Values cards had, and the same answer: spend the width, win back the height. */
@media (min-width:1200px) {
  .certs li { flex-basis:calc((100% - 3 * var(--gap)) / 4); max-width:340px; }
}
.certs li { text-align:center; padding:clamp(14px,2.6vh,26px) clamp(18px,2vw,26px); }
.certs p { color:var(--body); }
.certMark { display:flex; align-items:center; justify-content:center;
            height:clamp(52px,8vh,96px); margin-bottom:clamp(9px,1.6vh,16px); }
.certMark img { max-width:100%; max-height:100%; width:auto; height:auto; object-fit:contain; }
.certKicker { font:700 clamp(0.62rem,0.85vw,0.72rem)/1.4 'Archivo'; letter-spacing:.24em;
              text-transform:uppercase; color:var(--vio); margin:0 0 6px; }
.certLink { margin-top:12px; font-weight:600; font-size:0.86rem; color:var(--muted); }

/* FIVE PROGRAM CARDS ON ONE ROW on a wide screen. At the shared 3-per-row basis they wrap 3 + 2
   and the section reached 886px inside an 800px laptop (verify:fit) — and unlike the Values, these
   cards are one short paragraph each, so a fifth column costs nothing in readability. Below 1200px
   they fall back to the Values shape, and below 769px to one per row.
   The old comment below still describes that fallback. */
@media (min-width:1200px) {
  .awList li { flex-basis:calc((100% - 4 * var(--gap)) / 5); max-width:340px; }
}
/* The cyber-awareness list is the Values shape — five cards reading 3 + 2 — so it inherits the
   shared basis untouched and only needs the heading colour that marks it as a program, not a card
   of prose. */
.awList { margin-top:clamp(20px,3.4vh,44px); }
/* A five-card row is five NARROW columns, so a two-word heading breaks after the first word and
   every card reads as a fragment ("Recognising the / attack"). `balance` splits the line by
   meaning instead of by whatever fits, which is the whole difference between this looking designed
   and looking squeezed. The cards also carry their own padding here — the shared one is tuned for
   the Values' three-per-row and leaves these feeling cramped at five. */
.awList li { padding:clamp(20px,2.4vh,30px) clamp(18px,1.6vw,26px); }
.awList h3 { color:var(--purple); text-wrap:balance; margin-bottom:0.55em;
             font-size:clamp(1rem,1.15vw,1.12rem); line-height:1.25; }
.awList p { color:var(--body); font-size:clamp(0.92rem,1vw,1rem); line-height:1.55; }

/* THE DANJOO AI NOTE. It was a `.kicker` — uppercase and letter-spaced — which is a label style,
   and this is a sentence: set that way it ran to four lines and broke the product name across two
   of them ("DANJOO / AI"), underline and all. It reads as prose now, and the name is `nowrap` so
   it can never split again. */
.aiNote { max-width:62ch; margin:clamp(18px,2.6vh,34px) auto 0; text-align:center;
          font-size:clamp(0.92rem,1.05vw,1.02rem); line-height:1.6; color:var(--muted); }
.aiNote a { color:var(--purple); font-weight:600; white-space:nowrap; }

/* ===== VISION & MISSION — two SOLID cards, straight from the DC =====
   Not the bordered white card the Values use: Vision is the brand purple and Mission the deep ink,
   both with white copy, centred, and a heavier pad. Sizes and colours are the design's own. */
.vm li { background:var(--purple); border:0; color:#fff; text-align:center;
         padding:clamp(26px,3.4vw,40px); }
.vm li + li { background:var(--ink); }
.vm h3 { color:#fff; margin:0 0 0.6em; font-size:clamp(1.3rem,2vw,1.6rem); }
.vm p { color:rgba(255,255,255,0.92); font-weight:500; font-size:clamp(0.98rem,1.35vw,1.1rem); line-height:1.6; }

/* ===== OUR VALUES — the DC's `.valCard`: the BORDER is the live psychedelic =====
   The card is a 2.5px frame of the rotating sheet with a white panel sitting on top of it, and a
   pattern-masked icon over the title. Same device as the rings, so the sheet is the shared `.psy`
   (compositor-only, sized to its own box) rather than a per-frame plane. */
.vals li { position:relative; overflow:hidden; border:0; border-radius:18px; padding:2.5px;
           background:transparent; }
/* The DC's own padding is `clamp(20px, 2.6vw, 28px)` on all four sides. The horizontal value is
   kept; the VERTICAL one gets a vh term, because the icon plus two rows of cards put this section
   over a short laptop's screen (749px in 700). Same rule as everywhere else on these pages — a
   1366x625 laptop is short, not narrow, so vw-based spacing never shrinks for it. */
.vals .valIn { position:relative; z-index:1; height:100%; border-radius:15.5px; background:#fff;
               padding:clamp(12px,2vh,28px) clamp(20px,2.6vw,28px); text-align:center; }
.vals h3 { color:var(--purple); margin:0 0 0.5em; font-size:clamp(1.05rem,1.5vw,1.2rem); }
.vals p { color:var(--body); }
/* THE PATTERN ICON IS NOT A WINDOW ONTO THE SHEET — THE ICON ITSELF TURNS (2026-08-07)
   Reported from a real iPhone: "the icon at the top of the card is not rotating." It was animating
   the whole time, and the client was still right — nothing about it read as motion. Three reasons,
   and the rebuild had introduced all three by treating this like the rings:

   1. WRONG MECHANISM. The DC does not put a plane inside the mask. `.ptnIcon` carries the pattern as
      its OWN background and rotates ITSELF, so the starburst turns with it. Ours held the mask still
      and spun a sheet behind it — the silhouette never moved, and a still silhouette is what the eye
      reads as "not rotating", whatever the pixels inside it are doing.
   2. WRONG SCALE. `background-size:120vmax` is ~1020px of pattern on a 393px phone, and this icon is
      40px. That crop is 4% of one tile — a flat smear with no feature in it to carry motion. The DC
      sizes the pattern to the ELEMENT (`200%` = a tile twice the icon), so a whole motif turns inside
      the silhouette. This is why the rings read as alive and the icon did not: they are 10x wider.
   3. WORST POSSIBLE CROP. A plane rotates about its centre and this window sat exactly ON that
      centre, the one point in the frame with no linear travel. The card border works because it
      looks at the sheet's OUTER edge, ~200px out, where the same 8.6 deg/s is real displacement.

   Also the DC's `rgba(79,27,143,0.4)` wash, which is what makes this icon read as brand purple. Ours
   came through as raw rainbow because the plane carried no tint. And the period is the DC's 9s — the
   rest of the page turns at 40s (the engine's own rate) but this is a 40px silhouette; at 40s it
   advances 8.6 deg/s across a 20px radius, i.e. 3px/s, which is below the threshold of "moving".

   Consequence worth having: no child plane means no compositor layer for the plane, so this is also
   cheaper than what it replaces. `verify:planes` has one less plane to check, by design. */
.ptnIcon { display:block;
           width:min(2.6em,4.6vh); height:min(2.6em,4.6vh); margin:0 auto clamp(6px,1.2vh,14px);
           background-image:linear-gradient(rgba(79,27,143,0.2), rgba(79,27,143,0.2)),
                            url('/assets/min/Psychedelic.webp');
           background-size:auto, 770%; background-position:center, center;
           background-repeat:repeat, repeat;
           animation:ptnspin 9s linear infinite;
           -webkit-mask-image:url('/assets/PatternMaskHard.png'); mask-image:url('/assets/PatternMaskHard.png');
           -webkit-mask-size:contain; mask-size:contain;
           -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
           -webkit-mask-position:center; mask-position:center; }
/* SPINNING AN 8-FOLD SYMMETRIC BURST ONLY READS IF THE DOTS ARE DIFFERENT COLOURS
   (client, 2026-08-07 — the third report on this one icon, and the one that explains the first two)

   `PatternMaskHard` is a radial dot burst with **8-fold rotational symmetry**. Measured against
   itself: the mask rotated 45 deg differs by 0.025 and 90 deg by 0.031, where 10-30 deg differ by
   ~0.30. Every 45 deg the dots land back exactly on their own positions, so the SHAPE never
   appears to move at all. Rotation can therefore only be seen in the COLOURS the dots carry — and
   only if those colours differ enough from each other to be tracked around the ring.

   That is the whole story of all three reports, and the trap is that the two obvious knobs each
   fix one half and break the other:
   - The original build had the colours (a plane at `120vmax` behind a STILL mask) but nothing
     moved except the colours, sliding under fixed dots. Read as: not rotating.
   - Rotating the icon rigidly with the DC's `200%` tile made the fill nearly UNIFORM violet — a
     39px icon over a 78px tile samples one flat region — so there was nothing to track and the
     symmetry hid the rest. Measured per 45 deg period: 1.8-3.0 against 6.9 for a full revolution.
     Also read as: not rotating. Tried and rejected on the way here: smaller tiles (0.8-1.3em) make
     it WORSE, because the icon then spans many tiles and averages to flat; and a second
     `background-position` drift animation, which just slid a uniform fill and changed nothing.

   Both halves together is the fix: **rotate the icon rigidly, and give it a tile big enough that
   each dot picks up a different colour.** Then the coloured arrangement turns with the dots and
   the eye can follow one red dot around. Sampled from the source art, colour variety across a
   39px icon peaks when the icon covers ~160px of the original 1400px pattern (per-channel std ~63,
   hue spread ~200) — `770%` puts it there, and being a percentage it holds that ratio when the
   `4.6vh` cap shrinks the icon on a short screen. `em` or `vmax` would not.

   The wash is `0.2`, not the DC's `0.4`: at 0.4 the dots mute back toward one violet and the
   tracking goes with them. 0.2 keeps the purple bias and the client's reference liveliness. This
   is the one knob here that is pure taste — 0.4 is the DC's number if it ever needs to go back. */
@keyframes ptnspin { to { transform:rotate(360deg); } }

/* ============================================================================================
   THE DECK KANGAROO — the escort on every culture section hand-off.

   Straight from `_renderCultOpen` in both DC exports: while a push is in flight the Kali kangaroo
   crosses the screen once, left to right, hopping three times; the outgoing section is pushed out
   past its nose (+1.55 viewport widths) and the next is dealt in behind its tail (from -1.4). It
   is hidden at rest — it exists only for the hand-off — and it faces the direction of travel, so
   scrolling back sends it home the way it came.

   SIZED TO ITSELF, like every other plane on this page. The engine's version carries a `150vmax`
   plane inside the silhouette; here the mask box is ~38vh and the `.psy` inside it inherits
   `inset:0`, so the rotating pattern costs the kangaroo's own area and not the viewport's
   diagonal squared. Same rule as the rings — see the plane note at the top of this file.

   Only under `.rvArm`: with no JS there are no hand-offs to escort, so there is no kangaroo. */
/* SIZE IT LIKE THE HERO KANGAROO, because that is what the design measures. `_renderCultOpen`
   reads `#kangaroo`'s own offsetHeight and only falls back to `0.38 * H` when there isn't one.
   This page has no hero kangaroo, so the fallback was doing the sizing — fine on a desktop, but on
   a 390x844 phone it made a 320px animal where the design draws a 106px one. `#kangaroo` is
   `27.2vmin` (26vw on the smallest phones), so this is, exactly. */
/* NO `overflow` HERE. It was added as a harmless-looking guard against the 142vmax sheet spilling
   out, and it is what drew the faint square the client kept seeing around the animal: clipping a
   2092px composited plane at a 245px box leaves a seam on the clip boundary. Measured — mean ink on
   the box outline 48.8 with it, 0.0 without, and neither `will-change` nor `.kFace`'s own overflow
   changes that number. The mask is the only clip this needs; the silhouette IS the shape. */
.kang { position:fixed; left:0; top:0; z-index:2; pointer-events:none; display:none;
        width:27.2vmin; height:27.2vmin; will-change:transform; }
@media (max-width:460px) { .kang { width:26vw; height:26vw; } }
.kFace { position:absolute; inset:0; overflow:hidden;
         -webkit-mask-size:contain; mask-size:contain;
         -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
         -webkit-mask-position:center; mask-position:center; }
.kFaceR { -webkit-mask-image:url('/assets/KangSil8.png'); mask-image:url('/assets/KangSil8.png'); }
.kFaceL { -webkit-mask-image:url('/assets/KangSil8Flip.png'); mask-image:url('/assets/KangSil8Flip.png'); opacity:0; }

/* NO OUTLINE RING. The engine draws `KangRing8.png` over the silhouette; the client rejected that
   here — "the mask is only the kangaroo". The edge against a white slide comes from the pinned
   sheet inside the mask, not from a drawn border. */

/* THE SHEET INSIDE THE KANGAROO IS PINNED TO THE VIEWPORT, NOT TO THE ANIMAL (2026-08-08)
   Client: "check our kali program slide transition that involve kangaroo mask. we want to make the
   cultural awareness slide transition like that." The push magnitudes were already identical — the
   difference was entirely here.

   `#kang4Bg` is a 150vmax plane that `_pinPlane` re-places every frame at
   `left = (W - PD)/2 - boxLeft`, i.e. its centre sits on the VIEWPORT centre wherever the kangaroo
   happens to be. So the animal is a moving WINDOW onto one big fixed rotating sheet: the pattern
   inside it stays put in world space while the silhouette travels across it. That reads as the
   world showing through the kangaroo.

   Ours was the opposite: a plane 150% of the ~101px box, centred on the animal, carrying its
   pattern along with it — and at `background-size:120vmax` (~1020px on a phone) a 101px window
   samples one flat region, so it rendered as a solid rainbow blob with no visible detail. Both
   faults, one cause.

   The plane is viewport-sized now and `reveal.js` pins it per frame, exactly like the engine. This
   is the one plane on these pages that is deliberately far larger than its window, so
   `verify:planes` exempts it BY NAME rather than by loosening the rule for everything. It is also
   why the kangaroo is `display:none` at rest rather than `visibility:hidden` — a viewport-sized
   compositor layer must not idle behind the page (same lesson as the hero in bridge.js). */
.kFace > .psy { inset:auto; width:142vmax; height:142vmax; translate:none; aspect-ratio:auto; }

/* ============================================================================================
   THE DEAL-IN — the DC deck's choreography, as compositor-only CSS.

   In the mobile DC (`Danjoo Mobile.dc.html`) each culture section is its own screen: it is dealt
   in from the LEFT behind the kangaroo's tail, rests dead-centre, and is pushed out RIGHT past
   its nose; the partner logos rise from the bottom edge after a beat; the first ring cluster
   rises from the bottom and the bottom CORNERS while the later clusters slide in from the left.
   That is the motion — the wheel-hijacking deck that drove it is not, and does not come back.

   Each element declares its entry direction with `data-rv` and is SCROLL-SCRUBBED by
   /assets/reveal.js — the same trigger model as the bridge and as the main site's engine: scroll
   position is the clock, so a flick flies and an ease-back retraces 1:1. The rules below are only
   the armed START state (so nothing flashes in place before the first frame) and the neutraliser
   `measure()` reads through; every subsequent value is written inline by the script. Deliberately
   NO `transition` here — a transition would fight the scrub.

   PROGRESSIVE ENHANCEMENT CONTRACT, same as the bridge's: the hidden state applies only under
   `html.rvArm`, which the inline head script adds when JS runs and the reader has NOT asked for
   reduced motion — and which that same script drops again if reveal.js never reports in. Without
   the class every element renders in its final place from first paint.
   ============================================================================================ */
main { overflow-x:clip; }   /* the off-screen start positions must never widen the page */

.rvArm [data-rv] { opacity:0; }
.rvArm [data-rv="l"]  { transform:translateX(-38vw); }   /* in from the left, behind the tail */
.rvArm [data-rv="r"]  { transform:translateX(38vw); }
.rvArm [data-rv="up"] { transform:translateY(44vh); }    /* up from the bottom edge */
.rvArm [data-rv="bl"] { transform:translate(-52vw,36vh); }   /* up from the bottom corners */
.rvArm [data-rv="br"] { transform:translate(52vw,36vh); }
/* measure() reads untransformed geometry through this */
/* MEASURE WITH EVERY TRANSFORM OFF, INCLUDING THE SLIDE'S OWN (2026-08-08).
   This neutralised the elements but not the deck slide they sit on, and on a pinned stage the
   slides are parked at -1.4W and below the fold until they are dealt. So anything measuring an
   element's position INSIDE a slide was reading it off-stage: the ring exits computed their
   clearing distances from left = -1582, which flipped every direction (the left pair travelled
   right, the trio travelled up). Home is the only position worth measuring from — with the
   transform off, all six slides sit exactly where each one rests. */
.rvMeasure [data-rv] { transform:none !important; }
.rvMeasure .sec.deck { transform:none !important; opacity:1 !important; visibility:visible !important; }

/* ===== Mobile: one section per screen, in the mobile DC's geometry =====
   Sizes are lifted from the `@media (max-width:768px)` block of Danjoo Mobile.dc.html so the
   page reads at the same scale the designers set for a phone. */
@media (max-width:768px) {
  .sec { padding-top:clamp(64px,12vh,110px); padding-bottom:clamp(64px,12vh,110px); }
  .sec + .sec { padding-top:clamp(64px,12vh,110px); }
  .sec h2, .sec > p { max-width:88vw; }
  /* a third of a phone screen is not a card — one per row here, whatever the desktop basis says */
  .cards li { flex-basis:100%; max-width:none; }
  .sec > p br { display:none; }

  .partners { gap:0; margin-top:clamp(26px,7vw,44px); }
  .partners .disc { width:clamp(101px,31.2vw,135px); height:clamp(101px,31.2vw,135px); }
  .partners .disc > .in { inset:6px; }
  .partners .disc img { width:calc(100% - 16px); height:calc(100% - 16px); }

  .devices { gap:10px; margin-top:34px; }
  .dev { width:clamp(31px,8.9vw,41px); height:clamp(31px,8.9vw,41px); }

  .photos { gap:0; margin-top:36px; }
  .photos .disc { width:min(38vw,150px); height:min(38vw,150px); }

  /* ===== the phone's rings — the DC's clusters (#cpS5Grp) =====
     FIRST the settled, script-free layout: the three clusters stacked down the page in the DC's
     geometry. This is what a reader gets with no JS or with reduced motion, and it is correct on
     its own — the carousel below is an upgrade on top of it, never a prerequisite. */
  .cars { --rs:min(35.8vw,146px); display:flex; flex-direction:column; align-items:center;
          gap:clamp(26px,7vw,44px); margin-top:clamp(26px,7vw,44px); }
  .car { display:flex; flex-wrap:wrap; justify-content:center; align-items:center;
         width:calc(2 * var(--rs) - 10px); }
  .cars .b4 { display:none; }
  .cars .b1 { display:block; }                             /* the pyramid: 1 over 2 */
  .cars .disc { width:var(--rs); height:var(--rs); margin:0 -5px; }
  /* the phone's ring band is a fixed 9px, not the desktop fraction — the DC draws it that way */
  .cars .disc > .in { inset:9px; }
  /* the pyramid's two lower rings tuck up under the top one (the DC's -0.2 × ring - 10px) */
  .cars [data-rv-m="bl"], .cars [data-rv-m="br"] { margin-top:calc(-0.2 * var(--rs) - 10px); }
  .cars .lbl { font-size:clamp(0.58rem,2.52vw,0.7rem); line-height:1.45; }

  /* THE CYBER CARDS GO TWO-UP ON A PHONE (client, 2026-08-08). The shared phone rule is one card
     per row — right for the Values, whose cards carry a full sentence each — but these five are a
     heading and one short line, and stacked they made the slide 938px tall inside an 844px screen.
     A deck slide is clipped by its pin, so that is not a scrollbar, it is content simply cut off:
     the fifth card was sliced in half and sat under the step hint. Two-up fits them in three rows
     with the type dropped a notch, which is still comfortably readable at this size. */
  /* A PINNED SLIDE CANNOT SPEND 12vh ON PADDING. The phone block gives every `.sec` 12vh top and
     bottom — fine for a document you scroll through, but a deck slide is clipped to exactly one
     screen, so on a 640px phone that is 154px of the 640 gone before any content. Deck slides get
     a smaller, vh-aware pad; they are centred in the pin anyway, so the air is still there. */
  /* A deck slide has TWO fixed things over it, and must clear both: the settled title band at the
     top and the step hint at the foot. Neither is in flow, so neither is accounted for by anything
     else — the cyber slide's heading was sitting behind the band with only its second line showing.
     `--bandH` is written by bridge.js, so this tracks whatever the band actually measures. */
  /* The clearance under the band was `clamp(6px,1.4vh,18px)` — barely a hairline once a slide's
     content grew, and the client read it as the title being pressed against the band (2026-08-11:
     *"too close with the border, need to push down a little bit"*). Freeing the card-box air above
     paid for a real gap. */
  .rvArm .deckPin .sec.deck { padding-top:calc(var(--bandH, 118px) + clamp(14px,2.8vh,34px));
                              padding-bottom:max(clamp(28px,6vh,72px), var(--hintSpace));
                              /* and a real gutter — 6vw put the copy 23px off each edge, which on a
                                 390px screen reads as text pressed against the glass */
                              padding-left:clamp(26px,8vw,60px); padding-right:clamp(26px,8vw,60px);
                              /* `safe`, because CENTRING OVERFLOWS BOTH WAYS. The padding above
                                 reserves the band, but padding is not a floor: the moment a slide's
                                 content is taller than the box, `center` pushes half the excess out
                                 of the TOP — straight behind the band, which is what put the cyber
                                 heading under it on the client's iPhone even after the box was
                                 measured clear here. `safe center` degrades to flex-start exactly
                                 when it overflows, so the heading can never go up there; any excess
                                 goes out of the bottom, where the reader can still see it coming.
                                 Same tool and same reason as `#ffCol` and `#modalScrim` on the main
                                 site. Plain `center` first as the fallback. */
                              justify-content:center; justify-content:safe center; }
  .rvArm .deckPin .sec.deck > h2, .rvArm .deckPin .sec.deck > p { max-width:100%; }

  /* THE CARD LISTS PAGE ONE AT A TIME ON A PHONE (client, 2026-08-08: "make the card slide to
     right one by one like carousel, so the overall can fit nicely on the mobile version").

     Everything before this tried to make four credentials and five cyber cards share one phone
     screen — stacked, then in rows, then two-up with the type stepped down twice. Each pass fitted
     the box and still read as cramped, because the problem was never the styling: a deck slide is
     exactly one screen, and that much content does not belong on one screen at this width. Paging
     is the answer the page already had — it is what the benefit rings do — and it lets the type go
     back UP, because only one card is ever on screen.

     Same machinery as the rings, no new code: `[data-rvcar]` on the list, `data-rvspan` on the
     slide, and reveal.js slides page `i` to `-(i - cf) * 1.12W`. The extra rail steps are emitted
     phone-only alongside the ring ones. */
  /* `width:100%` NEXT TO THE CAP, and it is load-bearing — the same trap this file already
     documents for `.people`. `.cards` is a flex child of `.sec` carrying `margin: … auto 0`, and
     auto inline margins disable `align-self:stretch`, so a capped block sizes to FIT-CONTENT. With
     every card absolutely positioned, fit-content is zero: the list collapsed to nothing and the
     cards rendered one word per line on top of the heading. */
  .rvArm .awList, .rvArm .certs { display:block; position:relative; margin-left:auto;
                                  margin-right:auto; width:100%; max-width:330px; }
  /* EACH LIST IS SIZED TO ITS OWN TALLEST CARD, NOT TO THE TALLEST LIST ON THE SITE (2026-08-11).
     Both shared one `clamp(250px,36vh,330px)`, which is the CREDENTIALS card's requirement — it
     carries a logo, a kicker, a title, body copy and a register link. Measured at 390x844: the
     credentials need 269px of that 304px box, and the cyber modules (a title and one line) need
     **112**. So the cyber slide was carrying 192px of dead air, and on a pinned slide that air
     comes straight out of the clearance under the title band: its `<h2>` cleared the band by 23px
     headless and disappeared behind it on the client's iPhone (2026-08-11: *"in mobile this title
     is gone"*), which is also why the same slide read as cramped.
     This is the rule `.rvArm .cars` already follows — its height is `1.8 * --rs`, the pyramid it
     actually holds. Numbers below are the measured content plus room to breathe; re-measure with
     the card-height probe if the copy changes rather than nudging them. */
  .rvArm .certs  { height:clamp(250px,36vh,330px); }   /* tallest card measured 269px @390w */
  .rvArm .awList { height:clamp(140px,18vh,190px); }   /* tallest card measured 112px @390w */
  /* `transform`, not the `translate` PROPERTY — the rail writes `transform:translate(calc(-50% +
     Xpx), -50%)` and expects to replace this default outright, exactly as `.car` does. Setting the
     separate `translate` property instead leaves both in force, so the centring is applied twice
     and every page sits a full card-width to the left of where it should be. */
  /* EVERY PAGE THE SAME SIZE (client: "all cards need to be the same size", and the cyber card
     "need to be higher so it will look like a proper card"). Absolutely positioned cards each took
     their own content height, so the credentials stepped between 223px and 271px as they paged —
     the frame appearing to resize around the content is the opposite of a card. They fill the
     stage instead, and the content centres inside them, so the frame is constant and only what is
     printed on it changes. */
  .rvArm .awList > li, .rvArm .certs > li {
    position:absolute; left:50%; top:50%; transform:translate(-50%,-50%);
    width:100%; height:100%; margin:0;
    display:flex; flex-direction:column; justify-content:center;
    /* THE SHIMMER IS A REPAINT, NOT A MOVE (client, 2026-08-11: "the text above the cards still
       shaky when cards move"). Measured across the whole card rail: the heading's and the copy's
       top, left, transform, opacity and background-position are all CONSTANT while the cards
       step — the text does not move by a pixel. What it does is get re-rasterised, because an
       un-promoted element animating its transform/opacity dirties the region and its static
       neighbours are repainted with it, and text re-antialiased every frame reads as a shimmer.
       Promoting the thing that MOVES confines the animation to its own layer and leaves the text
       alone. It is the moving element that gets `will-change`, never the text — and only these,
       because a layer costs w*h*4 bytes (see the texture-budget note in CLAUDE.md) and these are
       ~330x300 boxes, a handful per page. */
    will-change:transform, opacity; }
  /* one card on screen means it can breathe again — these are the desktop sizes, not the stepped
     down ones the two-up attempt needed */
  .rvArm .awList > li { padding:clamp(16px,2.4vh,24px) 18px; }
  .rvArm .awList h3 { font-size:1.02rem; line-height:1.25; margin-bottom:0.45em; }
  .rvArm .awList p { font-size:0.9rem; line-height:1.5; }
  .rvArm .certs > li { padding:clamp(14px,2vh,20px) 16px; text-align:center; }
  .rvArm .certs > li > .certMark { height:clamp(40px,6vh,56px); margin:0 auto 8px; }
  .rvArm .certs h3 { font-size:1rem; line-height:1.2; margin:0 0 0.35em; }
  .rvArm .certs p { font-size:0.86rem; line-height:1.45; }
  .rvArm .certKicker { margin:0 0 4px; letter-spacing:.18em; font-size:0.6rem; }
  .rvArm .certLink { margin-top:8px; font-size:0.8rem; }
  /* the rail owns these cards' opacity from here, so the armed start state must not linger */
  .rvArm .awList > li[data-rv], .rvArm .certs > li[data-rv] { transform:none; }

  /* A PINNED SLIDE CANNOT SPEND 12vh ON PADDING. The phone block gives every `.sec` 12vh top and
     bottom — fine for a document you scroll through, but a deck slide is clipped to exactly one
     screen, so on a 640px phone that is 154px of the 640 gone before any content. Deck slides get
     a smaller, vh-aware pad; they are centred in the pin anyway, so the air is still there. */
  /* A deck slide has TWO fixed things over it, and must clear both: the settled title band at the
     top and the step hint at the foot. Neither is in flow, so neither is accounted for by anything
     else — the cyber slide's heading was sitting behind the band with only its second line showing.
     `--bandH` is written by bridge.js, so this tracks whatever the band actually measures. */
  /* The clearance under the band was `clamp(6px,1.4vh,18px)` — barely a hairline once a slide's
     content grew, and the client read it as the title being pressed against the band (2026-08-11:
     *"too close with the border, need to push down a little bit"*). Freeing the card-box air above
     paid for a real gap. */
  .rvArm .deckPin .sec.deck { padding-top:calc(var(--bandH, 118px) + clamp(14px,2.8vh,34px));
                              padding-bottom:max(clamp(28px,6vh,72px), var(--hintSpace));
                              /* and a real gutter — 6vw put the copy 23px off each edge, which on a
                                 390px screen reads as text pressed against the glass */
                              padding-left:clamp(26px,8vw,60px); padding-right:clamp(26px,8vw,60px);
                              /* `safe`, because CENTRING OVERFLOWS BOTH WAYS. The padding above
                                 reserves the band, but padding is not a floor: the moment a slide's
                                 content is taller than the box, `center` pushes half the excess out
                                 of the TOP — straight behind the band, which is what put the cyber
                                 heading under it on the client's iPhone even after the box was
                                 measured clear here. `safe center` degrades to flex-start exactly
                                 when it overflows, so the heading can never go up there; any excess
                                 goes out of the bottom, where the reader can still see it coming.
                                 Same tool and same reason as `#ffCol` and `#modalScrim` on the main
                                 site. Plain `center` first as the fallback. */
                              justify-content:center; justify-content:safe center; }
  .rvArm .deckPin .sec.deck > h2, .rvArm .deckPin .sec.deck > p { max-width:100%; }

  /* THE CREDENTIALS GO TWO-UP AND COMPACT ON A PHONE (client, 2026-08-08: "this part need to be
     re adjust and re design"). There are FOUR of these, not three, and stacked as full-width
     centred columns — a 96px logo over an eyebrow, a title and a paragraph — they ran to 1120px
     inside an 844px screen, cut off at BOTH ends by the pin.

     A horizontal row per card was tried first and is not enough: four cards still need ~235px each
     against a ~700px budget. Two-up halves the row count, and the mark comes down to a badge, which
     is what actually buys the height back. The copy is untouched — nothing here is hidden to make
     it fit, it is set smaller and given a narrower measure.

     (Grid, not a wrapping flex row: the mark and the four text elements are siblings with no
     wrapper, so in a flex row each text element takes its own share of the line and they sit beside
     each other before wrapping.) */
  .certs { --gap:10px; }
  .certs li { flex-basis:calc((100% - var(--gap)) / 2); max-width:none;
              padding:clamp(10px,1.5vh,15px) 12px; text-align:center; }
  .certs li > .certMark { height:38px; margin:0 auto clamp(5px,0.8vh,9px); }
  .certs h3 { font-size:0.9rem; line-height:1.2; margin:0 0 0.3em; }
  .certs p { font-size:0.74rem; line-height:1.4; }
  .certKicker { margin:0 0 3px; letter-spacing:.16em; font-size:0.58rem; }
  .certLink { margin-top:6px; font-size:0.72rem; }

  /* Vertical rhythm inside a pinned slide is the last place left to win height once the band and
     the hint have taken theirs. The shared paragraph and list margins are tuned for a document
     that can be as tall as it likes; a slide cannot. */
  /* Room between the title and the copy. These were tightened when every slide was fighting for
     height; now that the long lists page, the text slides have room to spare and were reading as
     cramped — the heading sat straight on top of the paragraph. */
  .rvArm .deckPin .sec.deck > p { margin-top:clamp(14px,2.4vh,26px); }
  .rvArm .deckPin .sec.deck > p + p { margin-top:clamp(10px,1.6vh,18px); }
  .rvArm .deckPin .sec.deck > h2 { margin-bottom:clamp(2px,0.6vh,8px); }
  .rvArm .deckPin .sec.deck .cards { margin-top:clamp(12px,2vh,24px); }

  .awList { --gap:8px; }
  .awList li { flex-basis:calc((100% - var(--gap)) / 2); max-width:none;
               padding:clamp(9px,1.4vh,14px) 12px; }
  .awList h3 { font-size:0.86rem; line-height:1.22; margin-bottom:0.3em; }
  .awList p { font-size:0.77rem; line-height:1.38; }
  .aiNote { font-size:0.77rem; line-height:1.45; margin-top:clamp(10px,1.6vh,20px); }

  /* THEN the carousel: the clusters become pages stacked on one spot and reveal.js slides them
     across one per screen of scroll.

     THE PIN MOVED OUT OF HERE (2026-08-08). This section used to sticky-pin its own content and
     own its own `.carStop` ruler. It is a slide on the deck stage now, so the OUTER pin holds it
     still and its three cluster pages are steps of the OUTER rail — see `[data-rvspan]`. A pin
     inside a pin does not work here anyway: the stage transforms the slide, which makes it the
     containing block for any `position:sticky` inside it, and the inner rail loses its ruler. */
  .rvArm .cars { display:block; position:relative; margin-left:auto; margin-right:auto;
                 height:calc(1.8 * var(--rs) - 10px); }   /* the tallest cluster: the pyramid */
  /* Same reason as the card pages: the cluster is what moves, so the cluster carries the layer and
     the heading above it stops being repainted every frame. (Client: "the text still shaky when the
     rings move".) The rings inside already have their own layers for the rotating sheet. */
  .rvArm .car { position:absolute; left:50%; top:50%; transform:translate(-50%,-50%);
                will-change:transform, opacity; }
  /* The rail owns every ring in here, so none of them may keep an armed start offset. Those
     offsets are keyed on `data-rv` — the DESKTOP direction — and on the phone the carousel is
     the one moving things: without this, ring 4 sat a third of a screen to the right and the
     bottom trio sat below the fold, so a "pair" showed up as one ring. */
  .rvArm .cars [data-rv] { transform:none; }
  .carPin > h2, .carPin > p { max-width:88vw; }
}

/* A SHORT PHONE, NOT JUST A NARROW ONE. A deck slide is clipped to exactly one screen, so on a
   360x640 handset the cyber slide still ran 84px past the bottom and lost the last card — the same
   "short, not narrow" trap the Values hit on a 1366x625 laptop, and the reason so much of this file
   carries a vh term. Everything here is one notch down from the phone sizes and nothing else on
   the page is touched. */
@media (max-width:768px) and (max-height:700px) {
  .sec.deck .psyText { font-size:0.84rem; line-height:1.4; }
  .sec.deck > p { font-size:0.8rem; line-height:1.4; }
  .sec.deck h2 { font-size:1.32rem; }
  .aiNote { font-size:0.72rem; line-height:1.34; margin-top:clamp(6px,1vh,12px); }
}

@media (prefers-reduced-motion:reduce) { * { transition:none !important; animation:none !important; } }
