/* ============================================================
   QVO.TECH — service portal, single self-contained page.
   Canvas-driven fake-3D portal window, video preloader
   sequence, travel transitions. Five chapters, one planet
   approach clip per QVO service:
     1 WEB (Websites) -> planet-1  (intro + landing)
     2 AI  (AI agents) -> planet-2
     3 APPS (Custom apps) -> planet-3
     4 POS (Point of sale) -> planet-4
     5 CONTACT -> planet-5
   Every clip opens on a full planet in space, approaches and
   lands on its surface; the settled background is always the
   clip's own last frame (planet-N-end.jpg), so there is never
   a black frame. Brand: QVO navy #01344c + white accents. The
   QVO mark is the real vector extracted from the brand PDF,
   embedded as a data URI so the page stays a single file.
   ============================================================ */

/* ---- fonts: local() only, so no failed font network requests ---- */
@font-face{
  font-family:'SF Pro'; font-style:normal; font-weight:400; font-display:swap;
  src:local('SF Pro Text'),local('SF Pro Display'),local('SF Pro'),local('Helvetica Neue'),local('Arial');
}
@font-face{
  font-family:'SF Pro'; font-style:normal; font-weight:700; font-display:swap;
  src:local('SF Pro Text Bold'),local('SF Pro Display Bold'),local('SF Pro Bold'),local('Helvetica Neue Bold'),local('Arial Bold'),local('Arial');
}
@font-face{
  font-family:'SF Pro Thin'; font-style:normal; font-weight:100; font-display:swap;
  src:local('SF Pro Thin'),local('SF Pro Text Thin'),local('SF Pro Display Thin'),local('Helvetica Neue Thin'),local('Arial');
}
/* tall condensed display face — falls back to an installed condensed face
   so the giant title keeps its geometry when Aalto is absent */
@font-face{
  font-family:'Aalto'; font-style:normal; font-weight:400; font-display:swap;
  src:local('Aalto'),local('Arial Narrow'),local('Haettenschweiler'),local('Impact'),local('Roboto Condensed'),local('Oswald');
}

:root{
  font-family:'SF Pro',Arial,sans-serif;
  color:#fff;
  background:#01344c;
  font-synthesis:none;
}

*{box-sizing:border-box}
html,body{margin:0;width:100%;height:100%;overflow:hidden}
.experience{position:relative;width:100%;height:100%;min-height:540px;overflow:hidden;background:#01344c}
.backgrounds,.background,.shade,.transition-layer,.transition-layer video{position:absolute;inset:0;width:100%;height:100%}
.background{object-fit:cover;opacity:0;transition:opacity .8s ease}
.background.is-visible{opacity:1}
/* bottom scrim: keeps the giant title, the facts and the CTA readable over the
   brightest landing stills (orange haze / lava / storm). Same stops as the
   canvas-side drawShade() so the frozen frame and the DOM backdrop agree. */
.shade{z-index:1;background:linear-gradient(to bottom,rgba(1,52,76,0) 44%,rgba(1,52,76,.55) 70%,rgba(1,52,76,.92) 100%);pointer-events:none}
.chrome{position:absolute;z-index:4;transition:opacity .45s ease,filter .45s ease}
.experience.is-transitioning .chrome{opacity:0;filter:blur(8px);pointer-events:none}

/* ---- header ---- */
.header{left:clamp(18px,1.95vw,28px);right:clamp(18px,1.95vw,28px);top:clamp(18px,3.1vh,28px);
        display:flex;justify-content:space-between;align-items:center}
.header-actions{margin-left:auto;display:flex;align-items:center}
.nav{display:flex;align-items:center;height:42px;padding:4px 5px;border:1px solid rgba(255,255,255,.45);
     background:rgba(1,52,76,.62);backdrop-filter:blur(12px);border-radius:999px}
.nav a{color:#fff;text-decoration:none;padding:8px 19px;border-radius:999px;line-height:1}
.nav a.active{background:#fff;color:#01344c}
.menu{display:inline-flex;align-items:center;min-height:44px;height:44px;padding:0 20px;border:0;border-radius:999px;background:#fff;color:#01344c;cursor:pointer;text-decoration:none}

/* ---- sound control -------------------------------------------------------
   One control owns both audio sources: the chapter clip's own track and the
   synthesized cabin bed. It is deliberately NOT inside .header, whose opacity
   is 0 until .intro-ready — the intro autoplays muted (browser policy), so the
   way to turn sound on has to be visible from the first paint, during the
   intro, and through travel transitions. Brand-navy glass to match the chrome,
   44px target, and the dot + label both state the current state.

   PLACEMENT — a reserved lane, not a floating coordinate. The control owns the
   band directly under the header row (--sound-lane-top = the header's own 44px
   row + one 8px gap) and --sound-w is the footprint it is guaranteed there. The
   only other thing that can reach that band is the portal caption, and it does
   on short viewports, where the portal rides up to 36-44%: there the caption
   reserves --sound-reserve on its own right edge, so the two are separated by
   construction instead of by whatever gap happens to be left over. Measured
   before that reservation (375x667, worst-case caption "Next: [05] CONTACT"):
   the caption ended at x=311.2 and the control began at x=319 — 7.8px of
   clearance inside a shared band, held up only by the control having lost its
   words; a longer chapter name or a different font put the two on top of each
   other. min-width (not width) + nowrap so a font with wider metrics grows the
   pill instead of wrapping the label, and the reservation is measured from the
   declared footprint, never from whatever the text happened to render at. */
:root{
  --sound-right:clamp(18px,1.95vw,28px);                  /* right gutter */
  --sound-lane-top:calc(clamp(18px,3.1vh,28px) + 52px);   /* header row + 8px */
  --sound-w:114px;       /* roomy footprint: state dot + "Sound off" both fit */
  --sound-compact:44px;  /* the control's 44px target in glyph form */
  --sound-reserve:calc(var(--sound-compact) + 12px);      /* caption keeps clear */
}
.sound{position:fixed;z-index:23;top:var(--sound-lane-top);right:var(--sound-right);
  display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:44px;height:44px;
  min-width:var(--sound-w);padding:0 16px;white-space:nowrap;
  border:1px solid rgba(255,255,255,.45);border-radius:999px;background:rgba(1,52,76,.88);
  backdrop-filter:blur(12px);color:#fff;font:inherit;font-size:15px;line-height:1;cursor:pointer;
  transition:background .25s ease,color .25s ease,border-color .25s ease}
.sound:before{content:'';width:8px;height:8px;border-radius:50%;background:rgba(255,255,255,.45);
  transition:background .25s ease}
.sound:hover{background:rgba(1,52,76,.96)}
.sound[aria-pressed="true"]{background:#fff;color:#01344c;border-color:#fff}
.sound[aria-pressed="true"]:before{background:#01344c}
.sound:focus-visible{outline:2px solid #fff;outline-offset:3px}
.sound-icon{display:none;width:22px;height:22px;flex:0 0 auto}
.sound[aria-pressed="false"] .sound-waves{display:none}
.sound[aria-pressed="true"] .sound-slash{display:none}
@media(max-width:420px){:root{--sound-w:96px}.sound{font-size:13px;gap:6px;padding:0 12px}}
/* Reserved-lane guard (measured). The portal caption shares the top band on
   every viewport too short to sit the portal centred — at 375x667 the caption
   sits at y=77.7 with its right edge at x=311.2 while the control's lane starts
   at x=319, and at 540x720 the control's own left edge was x=408 against a
   caption ending at x=400: 8px of clearance inside a shared band, with nothing
   enforcing it (an earlier ~96px text pill had landed straight on the caption,
   whose contrast then measured 1.05:1 against the pill's white border). So on
   short viewports the control keeps its 44px target, its lane and its
   accessible name, but drops the words for a speaker glyph — waves when on, a
   cross when off — and the caption reserves the lane's footprint with
   --sound-reserve, which is derived from the same --sound-compact that sizes
   the control. -640 rather than -520 so the guard covers every width whose
   portal rides up (540x720 included), and declared AFTER the 420px rule so the
   compact values win where both match. */
@media(max-width:640px) and (max-height:760px){
  :root{--sound-w:var(--sound-compact);--sound-right:12px}
  .sound{width:var(--sound-compact);padding:0;gap:0;border-radius:50%}
  .sound:before{display:none}
  .sound-icon{display:block}
  .sound .sound-label{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
}
/* Measured lane reservation (see syncSoundLane()). Only where the caption's own
   line really does land level with the control lane — a viewport too short to
   sit the portal centred — is the caption laid out short of the control's
   footprint. Everywhere else it keeps its flush alignment with the window, so a
   blanket breakpoint guess would have pulled the desktop and tall-phone captions
   off the alignment they are composed on. */
.portal-wrap.sound-lane .portal-heading{margin-right:var(--sound-reserve)}

/* ---- service list (one row per chapter; every row is a real control) ---- */
.planet-list{left:clamp(18px,1.95vw,28px);top:50%;transform:translateY(-43%);
             display:flex;flex-direction:column;gap:4px;font-size:16px}
.planet-item{display:flex;align-items:center;min-height:44px;padding:0 2px;margin:0;border:0;border-radius:6px;
             background:none;color:#fff;font:inherit;font-size:16px;text-align:left;cursor:pointer;
             opacity:.9;transition:opacity .2s ease}
.planet-item:hover{opacity:1}
.planet-item:focus-visible{outline:2px solid #fff;outline-offset:3px}
.planet-item.active{font-size:18px;font-weight:700;gap:8px;opacity:1}
.planet-item.active:before{content:'';width:16px;height:16px;border-radius:50%;background:#fff}

/* ---- portal ---- */
.portal-wrap{left:50%;top:50%;width:min(320px,31vw);transform:translate(-50%,-54%);perspective:none}
.portal-heading{display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;font-size:16px;opacity:0}
.portal-heading strong{font-size:18px;margin-left:8px}
.portal{position:relative;display:block;width:100%;aspect-ratio:320/350;padding:0;overflow:visible;border:0;
        border-radius:90px;background:transparent;cursor:pointer;box-shadow:none;transform:none!important}
.portal img{visibility:hidden;position:absolute;width:1px;height:1px;pointer-events:none}
.portal-canvas{position:fixed;inset:0;z-index:3;width:100%;height:100%;pointer-events:none}
/* The square portal is the only way forward, so it now says so in words. This
   pill sits on the inside bottom edge of the window in the same navy glass as
   the rest of the chrome, states the action and names the destination, and is
   real markup inside the portal button — the same Tab stop, the same action, and
   readable with no animation at all, at every viewport and under reduced motion.
   pointer-events:none because the whole portal is the target; this is its label.
   The radius is 90px rather than the element's own 70px at <=640 because the
   mask the canvas draws uses a hard-coded 90px radius: the pill frames the
   window that is actually on screen, not the DOM box. */
.portal-cta{position:absolute;left:50%;bottom:26px;transform:translateX(-50%);
  display:inline-flex;align-items:center;gap:7px;height:34px;padding:0 15px;
  border:1px solid rgba(255,255,255,.5);border-radius:999px;background:rgba(1,52,76,.9);
  backdrop-filter:blur(12px);color:#fff;font:inherit;font-size:15px;line-height:1;white-space:nowrap;
  pointer-events:none}
.portal-cta-arrow{font-size:15px}
.portal-cta-next{font-weight:700}
/* Finite introductory edge gleam: two restrained cycles that end on their own
   ("both" lands back on opacity 0) and are retired for good on the visitor's
   first interaction. It is a supporting cue only — the pill above is what
   actually states the portal is tappable — and it never runs under reduced
   motion, where the pill alone carries the affordance. */
.portal:after{content:'';position:absolute;inset:0;border-radius:90px;opacity:0;pointer-events:none;
  box-shadow:0 0 0 1px rgba(255,255,255,.55),0 0 34px 6px rgba(255,255,255,.16)}
.portal-wrap.is-gleaming .portal:after{animation:portal-gleam 2.6s cubic-bezier(.22,1,.36,1) 2 both}
@keyframes portal-gleam{0%,100%{opacity:0}30%{opacity:1}62%{opacity:.2}}
/* landscape phones: the portal window is small and the facts column starts just
   right of it, so the pill anchors to the window's inner left instead of the
   centre (measured: centred it would reach x=492 against facts at x=495) */
@media(orientation:landscape) and (max-height:560px){
  /* the window is small here and can sit partly below the fold (measured on the
     public build at 844x390: the intro places it at y=108 with height 328 inside
     a 390px viewport, so a pill anchored to its bottom edge lands off-screen).
     Anchor to the window's inner top-left instead — on-screen in both the intro
     and the settled chapter, and clear of the facts column that starts at x=495
     (centred it would have reached x=492). */
  .portal-cta{left:20px;right:auto;top:44px;bottom:auto;transform:none;height:30px;padding:0 11px;font-size:12px}
  .portal-cta-arrow{font-size:12px}
}

/* ---- planet content ---- */
/* --title-offset is the h1's horizontal offset from the content column: the
   spec's -32px bleed is kept for the desktop art direction, phones drop it to
   0 so the first letter of WEB/AI/APPS/POS/CONTACT is never clipped by the
   viewport edge (it feeds the base transform AND the title-rise keyframes). */
.planet-content{--title-offset:-32px;left:clamp(28px,4vw,58px);right:clamp(28px,3vw,44px);bottom:clamp(24px,3vh,30px);
                display:flex;justify-content:space-between;align-items:flex-end;gap:40px}
.planet-content .planet-title{font-family:Aalto,'Arial Narrow',sans-serif;
  font-size:min(clamp(96px,16.5vw,236px),24vh);
  font-weight:400;line-height:.84;margin:0 0 .05em;letter-spacing:0;transform:translateX(var(--title-offset,-32px));opacity:0}
/* CONTACT is the only long title: it keeps the same art direction but is
   scaled so it can never collide with the facts column or the viewport edge.
   The min() height term caps every title at ~24vh of ink so the rasterised
   glyphs always clear the bottom edge, not just the line box. */
.planet-content .planet-title.is-long{font-size:min(clamp(64px,10.6vw,158px),19vh)}
.planet-content dl{width:min(447px,34vw);margin:0;font-size:16px}
.fact{display:grid;grid-template-columns:138px 1fr;gap:18px;padding:8px 0;
      border-bottom:1px solid rgba(255,255,255,.48);opacity:0}
.fact:last-child{border-bottom:0}
.fact dt{font-weight:700}
.fact dd{margin:0}
.fact dd a{color:#fff;text-decoration:underline;text-underline-offset:3px}
/* the WhatsApp fact is a real link: give it a full 44px touch target without
   changing the row rhythm */
.fact dd a.tap{display:inline-flex;align-items:center;min-height:44px;margin:-12px 0}

/* ---- misc ---- */
.transition-layer{display:none}
.scene-canvas{display:none}
.loading{position:absolute;z-index:12;left:50%;bottom:30px;transform:translateX(-50%);opacity:0;
         font-size:12px;letter-spacing:.14em;text-transform:uppercase;transition:opacity .2s}
.experience.is-loading .loading{opacity:.8}
.experience.is-transitioning .header,.experience.is-transitioning .planet-list{opacity:1;filter:none;pointer-events:auto}
.experience.is-transitioning .shade{opacity:1}
.experience.is-committing .background{transition:none!important}

/* ---- preloader ---- */
.preloader{position:absolute;inset:0;z-index:20;background:#01344c;overflow:hidden}
.preloader video{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
.preloader-shade{position:absolute;inset:auto 0 0;height:35%;background:linear-gradient(to bottom,rgba(1,52,76,0),#01344c)}
.preloader.is-background{z-index:0}
.preloader.is-background .preloader-shade{opacity:0}
/* the intro hands off on its own final frame: the clip's last frame and
   planet-1-end.jpg are the same picture, so this fade is invisible */
.preloader.is-fading{opacity:0;transition:opacity .3s ease}
.preloader.is-gone{display:none}
.floating-logo{position:fixed;z-index:22;left:50%;top:50%;width:59px;height:58px;transform:translate(-50%,-50%);
  transition:left .6s cubic-bezier(.16,1,.3,1),top .6s cubic-bezier(.16,1,.3,1),width .6s cubic-bezier(.16,1,.3,1),height .6s cubic-bezier(.16,1,.3,1),transform .6s cubic-bezier(.16,1,.3,1)}
.floating-logo.is-docked{left:clamp(18px,1.95vw,28px);top:clamp(18px,3.1vh,28px);width:37px;height:36px;transform:none}
.floating-logo.is-settled{z-index:5}
/* the authentic QVO mark (real vector from the brand PDF, embedded) plus the
   QVO.TECH wordmark, which fades in once the mark has docked top-left */
.logo-mark{display:block;width:100%;height:100%;object-fit:contain}
.logo-word{position:absolute;left:100%;top:50%;margin-left:13px;transform:translateY(-50%);
  font-size:15px;font-weight:700;letter-spacing:.01em;white-space:nowrap;color:#fff;opacity:0;
  transition:opacity .25s ease .2s}
.floating-logo.is-docked .logo-word{opacity:1}
.preloader-count{position:fixed;z-index:22;left:50%;bottom:clamp(22px,3.1vh,28px);display:flex;align-items:flex-end;
  gap:4px;transform:translateX(-50%);font-family:'SF Pro',Arial,sans-serif;line-height:1}
.preloader-count>span:first-child{font-family:'SF Pro Thin','SF Pro',Arial,sans-serif;font-size:64px;font-weight:100}
.preloader-count .percent{font-size:24px;padding-bottom:4px}
.preloader-count.is-leaving{animation:preload-count-out .35s cubic-bezier(.22,1,.36,1) both}
@keyframes preload-count-out{to{opacity:0;filter:blur(7px);transform:translate(-50%,-34px)}}

/* ---- custom cursor ---- */
/* Native modal dialogs sit above the custom cursor in the browser's top layer.
   Keep the system cursor available until every dialog is closed. */
html.cc-on:not(:has(dialog[open])),html.cc-on:not(:has(dialog[open])) :is(body,button,a,.portal){cursor:none!important}
html:has(dialog[open]) .custom-cursor{visibility:hidden}
/* the native cursor is hidden by design, so keyboard focus needs its own
   visible affordance */
.portal:focus-visible,.menu:focus-visible,.nav a:focus-visible,.planet-item:focus-visible{outline:2px solid #fff;outline-offset:4px}
.custom-cursor{display:block;position:fixed;left:0;top:0;z-index:100;width:1px;height:1px;
  pointer-events:none;opacity:0;transition:opacity .2s ease}
.custom-cursor.is-visible{opacity:1}
.cursor-dot,.cursor-orbit{position:absolute;left:0;top:0;border-radius:50%;transform:translate(-50%,-50%)}
.cursor-dot{width:12px;height:12px;background:#fff}
.cursor-orbit{width:36px;height:36px;border:1px solid #fff;background:rgba(255,255,255,.4);
  transition:transform .3s cubic-bezier(.22,1,.36,1)}
.custom-cursor.is-enter .cursor-orbit{transform:translate(-50%,-50%) scale(1.16)}
.cursor-label{position:absolute;top:26px;left:0;transform:translateX(-50%) translateY(-4px);
  font:16px/1.2 'SF Pro',Arial,sans-serif;white-space:nowrap;opacity:0;
  transition:opacity .2s ease,transform .3s cubic-bezier(.22,1,.36,1)}
.custom-cursor.is-enter .cursor-label{opacity:1;transform:translateX(-50%) translateY(0)}

/* ---- entrance / switch animations ---- */
body:not(.intro-ready) .header,
body:not(.intro-ready) .planet-list,
body:not(.intro-ready) .planet-content .planet-title,
body:not(.intro-ready) .planet-content .fact{opacity:0}
.intro-ready .header{animation:reveal-down .4s cubic-bezier(.22,1,.36,1) both}
.intro-ready .planet-list{animation:reveal-side .4s cubic-bezier(.22,1,.36,1) .1s both}
.content-revealing .planet-title{animation:title-rise .45s cubic-bezier(.16,1,.3,1) both}
.content-revealing .fact{animation:fact-rise .3s cubic-bezier(.22,1,.36,1) both}
.content-revealing .fact:nth-child(1){animation-delay:.1s}
.content-revealing .fact:nth-child(2){animation-delay:.16s}
.content-revealing .fact:nth-child(3){animation-delay:.22s}
.content-revealing .fact:nth-child(4){animation-delay:.28s}
.mask-revealing .portal-heading{animation:portal-caption .4s cubic-bezier(.22,1,.36,1) both}
.planet-list.is-switching .planet-item{animation:menu-row .58s cubic-bezier(.22,1,.36,1) both}
.planet-list.is-switching .planet-item:nth-child(1){animation-delay:.02s}
.planet-list.is-switching .planet-item:nth-child(2){animation-delay:.05s}
.planet-list.is-switching .planet-item:nth-child(3){animation-delay:.08s}
.planet-list.is-switching .planet-item:nth-child(4){animation-delay:.11s}
.planet-list.is-switching .planet-item:nth-child(5){animation-delay:.14s}
.planet-list.is-switching .planet-item.active:before{animation:active-dot .55s cubic-bezier(.22,1,.36,1) .18s both}

@keyframes reveal-down{from{opacity:0;filter:blur(8px);transform:translateY(-18px)}to{opacity:1;filter:blur(0);transform:translateY(0)}}
@keyframes reveal-side{from{opacity:0;filter:blur(8px);transform:translate(-20px,-43%)}to{opacity:1;filter:blur(0);transform:translate(0,-43%)}}
@keyframes title-rise{from{opacity:0;filter:blur(12px);transform:translate(var(--title-offset,-32px),42px)}to{opacity:1;filter:blur(0);transform:translate(var(--title-offset,-32px),0)}}
@keyframes fact-rise{from{opacity:0;filter:blur(7px);transform:translateY(18px)}to{opacity:1;filter:blur(0);transform:translateY(0)}}
@keyframes portal-caption{from{opacity:0;filter:blur(7px);transform:translateY(22px)}to{opacity:1;filter:blur(0);transform:translateY(0)}}
@keyframes menu-row{from{opacity:.35;transform:translateX(-8px)}to{opacity:1;transform:translateX(0)}}
@keyframes active-dot{from{opacity:0;transform:scale(0)}to{opacity:1;transform:scale(1)}}

/* ---- responsive ---- */
@media(max-width:900px){
  .nav{display:none}
  .portal-wrap{width:min(300px,48vw)}
  /* the -32px bleed is wider than the 4vw gutter below 900px, which pushed the
     h1 box past the left edge; -18px keeps the bleed look inside the gutter */
  .planet-content{--title-offset:-18px}
  .planet-content .planet-title{font-size:min(clamp(80px,15.5vw,150px),22vh)}
  .planet-content .planet-title.is-long{font-size:min(clamp(52px,8.8vw,98px),17vh)}
  .planet-content dl{width:43vw}
  .fact{grid-template-columns:110px 1fr}
  .planet-list{font-size:14px}
  .planet-item{font-size:14px}
  .planet-item.active{font-size:16px}
}
@media(max-width:640px){
  .experience{min-height:600px}
  .planet-list{display:none}
  .portal-wrap{top:44%;width:min(260px,66vw)}
  .planet-content{--title-offset:0px;left:18px;right:18px;bottom:18px;display:block}
  /* block layout: the h1 sits directly above the facts, so its margin-bottom is
     the lane that keeps the rasterised glyph bottoms out of the first fact row */
  .planet-content .planet-title{font-size:min(clamp(72px,21vw,132px),16vh);margin-bottom:.16em}
  .planet-content .planet-title.is-long{font-size:min(clamp(46px,13vw,84px),13vh)}
  .planet-content dl{width:100%;font-size:13px}
  .fact{grid-template-columns:92px 1fr;padding:5px 0}
  .portal-heading{font-size:14px}
  .portal-heading strong{font-size:16px}
  .menu{height:44px;padding:0 18px}
  .portal{border-radius:70px}
}
@media(max-width:640px) and (max-height:720px){
  /* Smallest possible breakpoint correction (documented deviation):
     at top:44% on phones shorter than ~720px the portal box crossed the giant
     title on 375x667 (measured 11581px2 overlap). Only vertical rhythm + the
     title cap move; every other spec value is untouched. */
  .portal-wrap{top:36%}
  .planet-content .planet-title{font-size:min(clamp(64px,19vw,120px),14vh);margin-bottom:.14em}
  .planet-content .planet-title.is-long{font-size:min(clamp(44px,12vw,80px),12vh)}
  .planet-content dl{font-size:12px}
  .fact{padding:4px 0}
}
@media(prefers-reduced-motion:reduce){
  *{transition-duration:.01ms!important;animation-duration:.01ms!important}
  .portal{transform:none!important}
  /* the gleam is decorative: under reduced motion it never appears at all */
  .portal-wrap.is-gleaming .portal:after{animation:none!important;opacity:0!important}
}

/* ============================================================
   CUSTOM APPS — "Build your app" trigger + interactive demo sheet.

   The trigger is a real button that exists only in the apps chapter
   (visibility, not display, so it never shifts the chapter layout it
   is measured against). Its placement is measured free space:
     - phones (<=640): the top-right of the apps content block, in the
       band the APPS title leaves empty to its right, above the first
       fact row. Sized so the widest fallback title face still clears it.
     - wider viewports: right-aligned directly above the content block,
       in the free band the centred portal window leaves.
   The sheet opens BELOW the sound control's reserved lane and under it in
   z-order (21 vs the control's 23): by construction the two never share a
   band, and the way to turn sound on stays visible and tappable while the
   sheet is open. The panel is a flex column — pinned head, scrolling body,
   pinned actions — so the close control and the actions are always on
   screen at 320x568 without the body's scroll ever hiding them.
   ============================================================ */
.build-cta{position:absolute;top:0;right:0;z-index:6;display:inline-flex;align-items:center;gap:7px;
  min-height:44px;height:44px;padding:0 16px;border:1px solid rgba(255,255,255,.5);border-radius:999px;
  background:rgba(1,52,76,.9);backdrop-filter:blur(12px);color:#fff;font:inherit;font-size:14px;line-height:1;
  white-space:nowrap;cursor:pointer;opacity:0;visibility:hidden;pointer-events:none;
  transition:opacity .4s ease,background .25s ease}
.build-cta:hover{background:rgba(1,52,76,1)}
.build-cta:focus-visible{outline:2px solid #fff;outline-offset:3px}
.build-cta-arrow{font-size:15px;line-height:1}
body.intro-ready .experience[data-chapter="apps"] .build-cta{opacity:1;visibility:visible;pointer-events:auto}
/* the trigger arrives with the chapter's own content reveal */
.content-revealing .build-cta{animation:fact-rise .6s cubic-bezier(.22,1,.36,1) both}
@media(min-width:641px){.build-cta{top:auto;bottom:100%;margin-bottom:14px}}
@media(max-width:360px){.build-cta-arrow{display:none}}

/* ---- the sheet ---- */
.builder{position:fixed;inset:0;z-index:21;display:flex;flex-direction:column;align-items:center}
.builder[hidden]{display:none}
.builder-scrim{position:absolute;inset:0;background:rgba(0,15,23,.72);opacity:0;transition:opacity .26s ease}
.builder.is-open .builder-scrim{opacity:1}
.builder-panel{position:relative;display:flex;flex-direction:column;min-height:0;
  width:min(880px,calc(100% - 48px));
  margin-top:calc(var(--sound-lane-top) + 56px);
  max-height:calc(100% - var(--sound-lane-top) - 66px);
  border:1px solid rgba(255,255,255,.4);border-radius:24px;background:rgba(1,29,43,.94);
  backdrop-filter:blur(18px);box-shadow:0 30px 80px rgba(0,0,0,.5);overflow:hidden;
  opacity:0;transform:translateY(16px);transition:opacity .26s ease,transform .3s cubic-bezier(.22,1,.36,1)}
.builder.is-open .builder-panel{opacity:1;transform:none}
.builder-panel:focus{outline:none}
.builder-panel:focus-visible{outline:2px solid #fff;outline-offset:3px}
.builder-head{display:flex;align-items:flex-start;gap:12px;padding:16px 16px 0 20px}
.builder-kicker{margin:0 0 2px;font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:rgba(255,255,255,.68)}
.builder-title{margin:0;font-size:21px;font-weight:700;letter-spacing:-.01em;line-height:1.15}
.builder-close{margin-left:auto;flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;
  width:44px;height:44px;border:1px solid rgba(255,255,255,.45);border-radius:50%;
  background:rgba(255,255,255,.08);color:#fff;cursor:pointer;transition:background .2s ease}
.builder-close:hover{background:rgba(255,255,255,.2)}
.builder-close:focus-visible{outline:2px solid #fff;outline-offset:3px}
.builder-scroll{flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;
  -webkit-overflow-scrolling:touch;padding:12px 20px 6px}
.builder-sub{margin:0 0 12px;font-size:13.5px;line-height:1.45;color:rgba(255,255,255,.88)}
.builder-steps{display:flex;flex-wrap:wrap;gap:6px;margin:0 0 9px;padding:0;list-style:none}
.builder-step{display:inline-flex;align-items:center;gap:6px;padding:5px 10px;border:1px solid rgba(255,255,255,.3);
  border-radius:999px;font-size:12px;line-height:1;color:rgba(255,255,255,.72);
  transition:background .2s ease,color .2s ease,border-color .2s ease}
.builder-step-n{display:inline-flex;align-items:center;justify-content:center;width:16px;height:16px;
  border-radius:50%;background:rgba(255,255,255,.22);font-size:10px;font-weight:700}
.builder-step.is-done{border-color:rgba(255,255,255,.75);color:#fff}
.builder-step.is-on{background:#fff;border-color:#fff;color:#01344c;font-weight:700}
.builder-step.is-on .builder-step-n{background:#01344c;color:#fff}
/* the four steps fit on one line from ~428px up (measured with probe-steps.cjs);
   below that they wrap, so lay them out as an even 2x2 — the row then ends on a
   full line instead of leaving a lonely fourth chip */
@media(max-width:427px){
  .builder-steps{display:grid;grid-template-columns:1fr 1fr}
  .builder-step{justify-content:flex-start}
}
.builder-note{margin:0 0 12px;font-size:12.5px;line-height:1.4;color:rgba(255,255,255,.92)}
.builder-note strong{font-weight:700}
.builder-body{display:grid;grid-template-columns:1.35fr 1fr;gap:14px;align-items:start}
/* the demo app window — every figure in it is scripted sample data, and it says
   so in the window chrome and on its own footer line */
.builder-preview{border:1px solid rgba(255,255,255,.22);border-radius:16px;background:rgba(255,255,255,.06);overflow:hidden}
.builder-preview-bar{display:flex;align-items:center;gap:8px;padding:8px 10px;background:rgba(255,255,255,.08);
  border-bottom:1px solid rgba(255,255,255,.16)}
.builder-preview-dots{display:inline-flex;gap:4px}
.builder-preview-dots i{width:7px;height:7px;border-radius:50%;background:rgba(255,255,255,.35)}
.builder-preview-name{font-size:12px;font-weight:700}
.builder-preview-tag{margin-left:auto;padding:3px 8px;border-radius:999px;background:rgba(255,255,255,.16);
  font-size:10px;letter-spacing:.07em;text-transform:uppercase}
.builder-tabs{display:flex;gap:6px;padding:8px 10px 0}
.builder-tab{padding:4px 9px;border-radius:8px;background:rgba(255,255,255,.07);color:rgba(255,255,255,.5);
  font-size:11px;line-height:1.2;transition:background .2s ease,color .2s ease}
.builder-tab.is-on{background:#fff;color:#01344c;font-weight:700}
.builder-screen{padding:10px;min-height:120px;font-variant-numeric:tabular-nums}
.builder-row{display:flex;align-items:center;gap:8px;margin-bottom:6px;padding:6px 8px;border-radius:8px;
  background:rgba(255,255,255,.06);font-size:11.5px;line-height:1.3;
  opacity:0;transform:translateY(6px);transition:opacity .28s ease,transform .28s ease}
.builder-row:last-child{margin-bottom:0}
.builder-screen.is-in .builder-row{opacity:1;transform:none}
.builder-screen.is-in .builder-row:nth-child(2){transition-delay:.06s}
.builder-screen.is-in .builder-row:nth-child(3){transition-delay:.12s}
.builder-screen.is-in .builder-row:nth-child(4){transition-delay:.18s}
.builder-row .push{margin-left:auto}
.builder-row b{font-weight:700}
.builder-pill{padding:2px 7px;border-radius:999px;background:rgba(255,255,255,.14);font-size:10px;white-space:nowrap}
.builder-pill.is-ok{background:rgba(255,255,255,.9);color:#01344c;font-weight:700}
.builder-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:6px;
  min-height:112px;padding:10px;border:1px dashed rgba(255,255,255,.3);border-radius:10px;
  color:rgba(255,255,255,.7);font-size:11.5px;line-height:1.4;text-align:center}
.builder-tiles{display:grid;grid-template-columns:repeat(3,1fr);gap:8px}
.builder-tile{padding:8px;border-radius:10px;background:rgba(255,255,255,.07)}
.builder-tile-v{display:block;font-size:15px;font-weight:700;line-height:1.2}
.builder-tile-l{display:block;font-size:10px;line-height:1.3;color:rgba(255,255,255,.72)}
.builder-bars{display:flex;align-items:flex-end;gap:5px;height:34px;margin-top:10px}
.builder-bar{flex:1;border-radius:3px 3px 0 0;background:rgba(255,255,255,.38);transform:scaleY(.12);
  transform-origin:bottom;transition:transform .42s cubic-bezier(.22,1,.36,1)}
.builder-screen.is-in .builder-bar{transform:none}
.builder-note-row{margin:8px 0 0;font-size:10.5px;color:rgba(255,255,255,.68)}
.builder-checks-h{margin:0 0 7px;font-size:12px;font-weight:700}
.builder-checks{margin:0;padding:0;list-style:none}
.builder-check{display:flex;align-items:flex-start;gap:7px;margin-bottom:6px;font-size:11.5px;line-height:1.35;
  opacity:.35;transition:opacity .24s ease}
.builder-check:last-child{margin-bottom:0}
.builder-check.is-ok{opacity:1}
.builder-check-mark{flex:0 0 auto;display:inline-flex;align-items:center;justify-content:center;width:15px;height:15px;
  border-radius:50%;background:#fff;color:#01344c;font-size:10px;font-weight:700;opacity:0;transition:opacity .24s ease}
.builder-check.is-ok .builder-check-mark{opacity:1}
.builder-preview-foot{margin:0;padding:7px 10px;border-top:1px solid rgba(255,255,255,.16);
  background:rgba(255,255,255,.04);font-size:10.5px;color:rgba(255,255,255,.68)}
.builder-modules{display:grid;grid-template-columns:1fr;gap:8px;margin:0;padding:0;list-style:none}
.builder-module{display:flex;flex-direction:column;gap:3px;align-items:flex-start;width:100%;min-height:44px;
  padding:10px 12px;border:1px solid rgba(255,255,255,.28);border-radius:14px;background:rgba(255,255,255,.06);
  color:#fff;font:inherit;text-align:left;cursor:pointer;
  transition:background .22s ease,border-color .22s ease,opacity .22s ease}
.builder-module:hover{background:rgba(255,255,255,.13)}
.builder-module:focus-visible{outline:2px solid #fff;outline-offset:2px}
.builder-module.is-next{border-color:#fff}
.builder-module.is-done{border-color:rgba(255,255,255,.8);background:rgba(255,255,255,.16)}
/* a card that is already added stays at full strength; only the ones still
   locked behind the current step are dimmed, so "done" reads as confirmed */
.builder-module[disabled]{cursor:default}
.builder-module[disabled]:not(.is-done){opacity:.5}
.builder-module-head{display:flex;align-items:center;gap:8px;width:100%}
.builder-module-name{font-size:13.5px;font-weight:700;line-height:1.2}
.builder-module-state{margin-left:auto;font-size:10px;letter-spacing:.07em;text-transform:uppercase;
  color:rgba(255,255,255,.66);white-space:nowrap}
.builder-module.is-done .builder-module-state{color:#fff}
.builder-module-line{font-size:11px;line-height:1.35;color:rgba(255,255,255,.74)}
.builder-actions{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:12px 20px 16px;
  border-top:1px solid rgba(255,255,255,.16);background:rgba(1,23,35,.72)}
.builder-btn{display:inline-flex;align-items:center;justify-content:center;gap:8px;min-height:44px;padding:0 18px;
  border:1px solid rgba(255,255,255,.5);border-radius:999px;background:transparent;color:#fff;
  font:inherit;font-size:14px;font-weight:700;line-height:1;text-decoration:none;cursor:pointer;
  transition:background .22s ease,color .22s ease,box-shadow .3s ease}
.builder-btn:hover{background:rgba(255,255,255,.14)}
.builder-btn:focus-visible{outline:2px solid #fff;outline-offset:3px}
.builder-btn[disabled]{opacity:.45;cursor:default}
.builder-btn.is-wa{margin-left:auto;background:#fff;border-color:#fff;color:#01344c}
.builder-btn.is-wa:hover{background:rgba(255,255,255,.86)}
.builder-btn.is-wa.is-ready{box-shadow:0 0 0 3px rgba(255,255,255,.22)}
@media(max-width:640px){
  .builder-panel{width:calc(100% - 20px);border-radius:20px}
  .builder-head{padding:13px 12px 0 16px}
  .builder-title{font-size:19px}
  .builder-scroll{padding:10px 16px 4px}
  .builder-body{grid-template-columns:1fr;gap:10px}
  .builder-modules{grid-template-columns:repeat(3,1fr);gap:6px}
  .builder-module{padding:8px 6px;border-radius:12px;align-items:center;text-align:center}
  .builder-module-head{flex-direction:column;gap:2px}
  .builder-module-state{margin-left:0}
  .builder-module-line{display:none}
  .builder-screen{min-height:112px}
  .builder-actions{padding:10px 16px 14px}
  .builder-btn{padding:0 14px}
  .builder-btn.is-wa{flex:1 1 100%;margin-left:0}
}
@media(orientation:landscape) and (max-height:560px){
  .builder-body{grid-template-columns:1fr 1fr;gap:12px}
  .builder-modules{grid-template-columns:repeat(3,1fr)}
  .builder-module{padding:8px 6px;align-items:center;text-align:center}
  .builder-module-head{flex-direction:column;gap:2px}
  .builder-module-state{margin-left:0}
  .builder-module-line{display:none}
  .builder-screen{min-height:98px}
}
/* smallest phones: .experience's own 600px min-height is taller than a 568px
   viewport, so the bottom-anchored content column was laid out against a 600px
   box and ran 32px past the fold (measured at 320x568: last fact row bottom
   582). The box is taken to the real viewport here and the display title is
   capped, which is what buys back the clearance to the portal window. Scoped to
   short, narrow phones, so no other viewport is laid out differently. */
@media(max-width:360px) and (max-height:620px){
  .experience{min-height:100vh;min-height:100dvh}
  .planet-content .planet-title{font-size:min(clamp(52px,17vw,120px),13vh);margin-bottom:.1em}
  .planet-content dl{font-size:11.5px}
  .fact{padding:3px 0}
}
/* my own motion, collapsed: the stagger delays must not survive as delays */
@media(prefers-reduced-motion:reduce){
  .builder-panel,.builder-scrim{transition-duration:.01ms!important;transform:none!important}
  .builder-row,.builder-bar,.builder-check,.builder-check-mark,.builder-tab,.builder-module,.builder-module-state,.builder-tile{transition-duration:.01ms!important;transition-delay:0ms!important;animation-delay:0ms!important}
  .content-revealing .build-cta{animation:none!important;opacity:1}
}

/* Screen-reader / crawler heading for the rendered portal. The visible chapter
   titles (WEB, AI, APPS...) are decorative h2s; the page needs one real h1. */
.visually-hidden{position:absolute!important;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
