GPT-6 Astra, oltre i benchmark: cosa riesce davvero a costruire

Anatomia 3D, motori, videogiochi e siti completi: abbiamo provato GPT-6 Astra per capire dove arriva davvero, senza nasconderne limiti e compromessi


GPT-6 Astra, oltre i benchmark: cosa riesce davvero a costruire

Abbiamo dato a GPT-6 Astra un solo prompt e abbiamo aspettato. Dopo 19 minuti e 50 secondi sullo schermo c’era un sito funzionante dedicato a Terra, Venere e Marte, con video di sfondo, animazioni, navigazione e layout responsive. Non perfetto, ma molto più vicino a un prodotto reale che a una semplice demo generata al volo.

È il modo più concreto che abbiamo trovato per capire GPT-6 Astra senza partire dai benchmark. Nel nostro primo articolo sul modello avevamo raccontato soprattutto la corsa agli agenti AI. Dopo averlo provato, il punto ci sembra ancora più chiaro: OpenAI ha costruito Astra per lavori complessi dall’inizio alla fine, non soltanto per rispondere meglio in chat.

Un prompt, poi venti minuti di attesa

Il nostro test era volutamente semplice nell’idea e piuttosto complicato nell’esecuzione: una pagina chiamata SpaceEdu con tre pianeti, video a tutto schermo, elementi 3D laterali, animazioni d’ingresso, menu responsive e regole molto precise per dimensioni e comportamento.

Il prompt non è stato scritto da noi da zero. Abbiamo ripreso quello pubblicato da un designer e lo abbiamo usato come base. È una specifica lunghissima, quasi un piccolo documento tecnico: un buon promemoria del fatto che anche il prompting sta cambiando e che spesso un’AI può aiutare a preparare istruzioni migliori per un’altra AI. Qui trovate il prompt completo, così potete copiarlo e provarlo.

Abbiamo lanciato Astra nella modalità Ultra. La prima generazione ha richiesto 19 minuti e 50 secondi e ha prodotto una pagina già utilizzabile, ma con alcune parti ancora incomplete. Con una seconda richiesta abbiamo aggiunto le pagine informative dei tre pianeti: altri tre minuti circa. Poi abbiamo esaurito la quota disponibile nella nostra finestra di utilizzo, quindi il test si è fermato lì.

Anatomia, motori e mondi 3D

Il nostro è un esempio piccolo rispetto a ciò che alcuni utenti stanno pubblicando. Ashe Magalhaes ha mostrato Human Atlas, un sito 3D che scompone l’anatomia maschile in 2.234 elementi modellati. Dilum Sanjaya ha invece chiesto ad Astra una visualizzazione interattiva di un motore V8, con componenti in movimento e dati mostrati attorno al modello.

Tom Krcha ha usato Astra con Three.js per generare treni 3D direttamente da codice TypeScript, senza file di modelli 3D tradizionali. Un altro esperimento diventato virale, pubblicato da Anshu, mostra invece un gioco 3D costruito in circa 45 minuti usando anche Blender e generazione di immagini come riferimento visivo.

Sono demo, non prodotti finiti. E questa distinzione conta. Lo stesso Anshu ha poi spiegato che per arrivare a qualcosa di davvero pronto servono ancora iterazioni, ottimizzazione e test. Il punto interessante non è quindi che un singolo prompt sostituisca improvvisamente un team di sviluppo. È quanto si sia alzato il livello del primo risultato ottenibile da una persona che sa descrivere bene ciò che vuole.

Il salto è nel fare

È qui che Astra ci è sembrato diverso durante la prova. Non perché “scrive codice meglio” in senso astratto, ma perché riesce a mantenere per molti minuti un obiettivo, produrre file, controllare ciò che ha costruito e continuare a intervenire sul progetto.

È anche la direzione su cui si stanno muovendo Anthropic con Claude Fable 5.1 e gli altri modelli di frontiera: meno chatbot che rispondono a una domanda, più agenti AI capaci di portare avanti un lavoro su più passaggi. OpenAI descrive Astra proprio come un modello per computer use, browser, software engineering e attività professionali end-to-end.

AGI? Meglio andarci piano

Jensen Huang, CEO di NVIDIA, è andato molto oltre. Commentando Astra su X ha scritto “AGI has arrived” e ha sostenuto che il modello sia stato addestrato su oltre 100.000 sistemi NVIDIA Grace Blackwell NVLink72, aggiungendo che altre 400.000 GPU sarebbero in arrivo.

È una dichiarazione notevole, ma non una certificazione tecnica. AGI, Artificial General Intelligence, resta una definizione discussa e non esiste un test universalmente accettato che permetta di stabilire che sia stata raggiunta. Inoltre Huang guida l’azienda che fornisce una parte fondamentale dell’infrastruttura usata per addestrare e far funzionare questi modelli.

Astra continua anche a poter sbagliare, interpretare male una richiesta o produrre qualcosa che richiede controllo umano. Nel nostro stesso test il primo risultato aveva bisogno di un secondo passaggio. È forse proprio questo il modo più utile di guardarlo: non come una macchina che sa già fare tutto, ma come uno strumento che in pochi minuti riesce a portare una persona molto più avanti di quanto fosse realistico fino a poco tempo fa.

Mostra e copia il prompt completo usato per SpaceEdu
Build a single, self-contained HTML file: a full-viewport space-themed hero section
for a site called "SpaceEdu". No build step, no frameworks, no external JS. All CSS in
one <style> block in <head>, all JS in one <script> block before </body>. It must be
pixel-faithful to the spec below and fully mobile responsive. All text in italian. 

════════════════════════════════════════════════════════════════════════
1. CONCEPT
════════════════════════════════════════════════════════════════════════
A cinematic hero. A looping video of a single planet fills the whole viewport as the
background. Centred over it: eyebrow "PLANET", a huge serif planet name, a short cyan
rule, a paragraph, and a glossy white pill button. Flanking the button, cropped by the
left and right screen edges, sit two transparent planet cut-outs with serif labels.

Three planets exist: EARTH, VENUS, MARS. Exactly one is "featured" (its clip is the
background, its name is the headline). The other two occupy the left and right slots.
Clicking a side planet makes it featured; the two slots then re-fill with the other
two. Earth is featured on load. The rotation is fully reversible.

════════════════════════════════════════════════════════════════════════
2. ASSETS — use these exact URLs
════════════════════════════════════════════════════════════════════════
Background clips (10s, 16:9, 1276x720, silent, loop):
  EARTH https://d8j0ntlcm91z4.cloudfront.net/user_38xzZboKViGWJOttwIXH07lWA1P/hf_20260827_202422_3ffb4889-c520-432d-8458-038009eb40df.mp4
  VENUS https://d8j0ntlcm91z4.cloudfront.net/user_38xzZboKViGWJOttwIXH07lWA1P/hf_20260827_202422_b211cd74-013b-4dd3-bfd0-64491d8696fa.mp4
  MARS  https://d8j0ntlcm91z4.cloudfront.net/user_38xzZboKViGWJOttwIXH07lWA1P/hf_20260827_202422_51eae59a-2459-4c84-907c-cc5edfe5fea7.mp4

Poster / still for each clip (also used as the .sky background-image fallback):
  EARTH https://d8j0ntlcm91z4.cloudfront.net/user_38xzZboKViGWJOttwIXH07lWA1P/hf_20260827_202133_508c64b8-a31e-4290-bdfc-1187df70e0a6.png
  VENUS https://d8j0ntlcm91z4.cloudfront.net/user_38xzZboKViGWJOttwIXH07lWA1P/hf_20260827_202133_cf55d1d8-7b59-4a64-80da-d72052ae974e.png
  MARS  https://d8j0ntlcm91z4.cloudfront.net/user_38xzZboKViGWJOttwIXH07lWA1P/hf_20260827_202133_0ba6de7c-285d-43dc-b7ab-8c54c73707cb.png

Transparent planet cut-outs, 2048x2048 PNG with alpha (the side-slot artwork):
  EARTH https://d8j0ntlcm91z4.cloudfront.net/user_38xzZboKViGWJOttwIXH07lWA1P/hf_20260827_202005_3346cc4d-ec3b-44ab-825c-b18e49f5021a.png
  VENUS https://d8j0ntlcm91z4.cloudfront.net/user_38xzZboKViGWJOttwIXH07lWA1P/hf_20260827_202012_640b239a-d08a-4200-adb2-741bbe129ac8.png
  MARS  https://d8j0ntlcm91z4.cloudfront.net/user_38xzZboKViGWJOttwIXH07lWA1P/hf_20260827_202018_3d559490-f613-4ed7-a3bb-3b7e9fc90fb8.png

Favicon: use the Earth cut-out URL above, type image/png.

════════════════════════════════════════════════════════════════════════
3. FONTS — load from Google Fonts
════════════════════════════════════════════════════════════════════════
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
Load: Prata 400; Hanken Grotesk 400,500,600,700; Poppins 500,600.
Stacks:
  --font-serif : 'Prata', Georgia, serif
  --font-body  : 'Hanken Grotesk', system-ui, -apple-system, 'Segoe UI', sans-serif
  --font-logo  : 'Poppins', sans-serif
Body text uses --font-body. Headline and the two planet labels use --font-serif.
The wordmark uses --font-logo.

════════════════════════════════════════════════════════════════════════
4. DESIGN SYSTEM — the single most important rule
════════════════════════════════════════════════════════════════════════
The layout is NOT built from rem/%/flex guesswork. It is a fixed composition measured
from a 1353 x 1163 reference, expressed in ONE custom property --u = one design pixel.
EVERY length in the file is calc(N * var(--u)). Do not substitute px values.

*, *::before, *::after { box-sizing:border-box; margin:0; padding:0 }

:root{
  --dw:1353; --dh:1163; --gutter:25;
  --u:max(min(.72px, calc(100vh / 700)), min(calc(100vw / 1353), calc(100vh / 1163)));
  --dh-px:calc(1163 * var(--u));
  --vshift:calc(max(0px, (100vh - var(--dh-px))) * .42);
  --ink:#ffffff; --cyan:#79dce8; --cyan-logo:#5fd0e1;
  --rule:rgba(255,255,255,.23); --btn-ink:#071227;
}
@supports (height:100dvh){
  :root{
    --u:max(min(.72px, calc(100dvh / 700)), min(calc(100vw / 1353), calc(100dvh / 1163)));
    --vshift:calc(max(0px, (100dvh - var(--dh-px))) * .42);
  }
}
html,body{height:100%}
body{background:#04101f; color:var(--ink); font-family:var(--font-body);
  -webkit-font-smoothing:antialiased; -moz-osx-font-smoothing:grayscale;
  text-rendering:geometricPrecision; overflow:hidden}

--vshift pushes everything below the nav down on taller-than-design viewports so the
composition stays balanced. --gutter offsets the content column left by half its value
so the optical centre sits at x=664, not 676.

════════════════════════════════════════════════════════════════════════
5. MARKUP
════════════════════════════════════════════════════════════════════════
<div class="stage">                     position:fixed; inset:0; overflow:hidden;
                                        isolation:isolate
  <div class="sky">                     absolute; inset:0; z-index:0;
                                        background-image:url(EARTH STILL);
                                        background-size:cover; background-position:center;
                                        background-repeat:no-repeat
    3 x <video data-planet="earth|venus|mars">
        Earth:  class="is-active" autoplay muted loop playsinline preload="auto"
                src=EARTH CLIP  poster=EARTH STILL  aria-hidden="true"
        Venus & Mars: muted loop playsinline preload="none"
                data-src=CLIP (NOT src)  poster=STILL  aria-hidden="true"
  </div>
  <div class="ui">                      absolute; inset:0; z-index:3
    <header class="navbar">
      <div class="navrow">
        <a class="logo" href="#">space<i>edu</i></a>
        <nav class="links" id="site-nav">
          <a href="#" aria-current="page">Planets</a>
          <a href="#">Tution</a>            (keep this spelling)
          <a href="#">Tutorials</a>
          <a href="#">Blog</a>
          <a class="enroll" href="#">Enroll</a>
        </nav>
        <button class="burger" type="button" aria-label="Open navigation"
                aria-expanded="false" aria-controls="site-nav">
          <span></span><span></span><span></span>
        </button>
      </div>
    </header>
    <div class="copy">
      <div class="col eyebrow"><span class="ent-mask"><span class="ent-line">PLANET</span></span></div>
      <h1 class="col title"><span class="ent-mask"><span class="ent-line">EARTH</span></span></h1>
      <div class="col rule"><span></span></div>
      <p class="col lede">Learn more about the fascinating details that we call our home,
         Planet Earth. Course enrollment 
starts today. Early Bird tickets typically
         last a week, don&rsquo;t miss out!</p>
      <div class="col cta">
        <button class="planet planet-l" type="button" data-slot="l">
          3 x <img data-planet="earth|venus|mars" alt="" src="CUTOUT URL">
        </button>
        <button class="planet planet-r" type="button" data-slot="r">
          3 x <img data-planet="earth|venus|mars" alt="" src="CUTOUT URL">
        </button>
        <a href="#">GET STARTED</a>
        <span class="label label-l"></span>     (empty — filled by script)
        <span class="label label-r"></span>
      </div>
    </div>
  </div>
  <button class="scroll" type="button" aria-label="Scroll to next section">
    <svg viewBox="0 0 26 33" fill="none" aria-hidden="true">
      <path d="M13 1.5 V31.5 M1.9 20.4 L13 31.5 L24.1 20.4" stroke="#ffffff"
            stroke-width="3" stroke-linecap="square" stroke-linejoin="miter"/>
    </svg>
  </button>
</div>

CRITICAL: all three cut-outs are present in BOTH slots as sibling <img> tags. Switching
reveals one with a class — it must never reassign img.src, or the browser keeps painting
the old planet until the new 2048px file downloads.

════════════════════════════════════════════════════════════════════════
6. CSS — exact values
════════════════════════════════════════════════════════════════════════
BACKDROP
.sky video{position:absolute; inset:0; width:100%; height:100%; object-fit:cover;
  display:block; opacity:0; transition:opacity .22s linear}
.sky video.is-active{opacity:1}
@media (prefers-reduced-motion:reduce){ .sky video{display:none} }

SIDE PLANET SLOTS (two slots, not two planets)
.planet{position:absolute; z-index:-1; padding:0; border:0; background:none;
  -webkit-appearance:none; appearance:none; line-height:0; cursor:pointer;
  pointer-events:auto;                      /* .copy disables pointers wholesale */
  transition:transform .45s cubic-bezier(.22,1,.36,1)}
.planet img{display:none; width:100%; height:auto}
.planet img.is-shown{display:block}
.planet:hover{transform:scale(1.045)}
.planet:active{transform:scale(.99)}
.planet-l{width:calc(143 * var(--u)); left:calc(-69.5 * var(--u)); top:calc(-38.3 * var(--u))}
.planet-r{width:calc(143 * var(--u)); right:calc(-64.7 * var(--u)); top:calc(-40.6 * var(--u))}
@media (prefers-reduced-motion:reduce){
  .planet{transition:none} .planet:hover,.planet:active{transform:none} }
(The cut-outs fill ~97% of their square canvas; these box sizes make the rendered
 sphere 139u across with its centre 2u from the left edge / 6.8u from the right.)

LAYOUT
.copy{position:absolute; inset:0; transform:translateY(var(--vshift)); pointer-events:none}
.copy a{pointer-events:auto}
.col{position:absolute; left:0; right:calc(var(--gutter) * var(--u)); text-align:center}

NAV
.navbar{position:absolute; top:0; left:0; right:0; z-index:6; height:calc(88 * var(--u))}
.navbar::after{content:""; position:absolute; left:calc(25 * var(--u));
  right:calc(49 * var(--u)); top:calc(86 * var(--u)); height:calc(2 * var(--u));
  background:var(--rule)}
.navrow{position:absolute; left:calc(25 * var(--u)); right:calc(49 * var(--u)); top:0;
  height:calc(86 * var(--u)); display:flex; align-items:center; justify-content:space-between}
.logo{font-family:var(--font-logo); font-weight:600; font-size:calc(18.4 * var(--u));
  letter-spacing:calc(-0.15 * var(--u)); line-height:1; color:#fff; text-decoration:none;
  white-space:nowrap; position:relative; top:calc(-1 * var(--u))}
.logo i{font-style:normal; color:var(--cyan-logo)}
.burger{display:none; position:relative; width:calc(46 * var(--u)); height:calc(46 * var(--u));
  padding:0; border:0; background:none; cursor:pointer; flex-direction:column;
  align-items:center; justify-content:center; gap:calc(6 * var(--u)); flex:none}
.burger span{display:block; width:calc(24 * var(--u)); height:calc(2 * var(--u));
  border-radius:calc(2 * var(--u)); background:#fff;
  transition:transform .28s cubic-bezier(.4,0,.2,1), opacity .18s linear}
.navrow[data-open="true"] .burger span:nth-child(1){transform:translateY(calc(8 * var(--u))) rotate(45deg)}
.navrow[data-open="true"] .burger span:nth-child(2){opacity:0}
.navrow[data-open="true"] .burger span:nth-child(3){transform:translateY(calc(-8 * var(--u))) rotate(-45deg)}
.links{display:flex; align-items:center}
.links a{position:relative; display:flex; align-items:center; height:calc(86 * var(--u));
  font-size:calc(18.4 * var(--u)); font-weight:400; line-height:1; color:#fff;
  text-decoration:none; padding:0 calc(17.5 * var(--u));
  letter-spacing:calc(-1 * var(--u)); white-space:nowrap}
.links a[aria-current="page"]::after{content:""; position:absolute; left:0; right:0;
  top:calc(83.5 * var(--u)); height:calc(4 * var(--u)); background:var(--cyan);
  border-radius:calc(1 * var(--u))}
.links a:nth-child(3){letter-spacing:calc(-1.75 * var(--u))}
.links a:nth-child(2){margin-left:calc(24 * var(--u))}
.links a:nth-child(3){margin-left:calc(26 * var(--u))}
.links a:nth-child(4){margin-left:calc(27 * var(--u))}
.links a.enroll{margin-left:calc(28.5 * var(--u)); width:calc(107 * var(--u));
  height:calc(38 * var(--u)); border-radius:calc(19 * var(--u)); display:flex;
  align-items:center; justify-content:center; font-size:calc(17 * var(--u));
  font-weight:600; letter-spacing:calc(-0.75 * var(--u)); line-height:1;
  color:var(--btn-ink); text-decoration:none; padding-top:calc(2 * var(--u));
  background:linear-gradient(180deg,#ffffff 0%,#d8ecfe 9%,#dceefe 72%,#f4f9ff 100%);
  box-shadow:0 calc(3 * var(--u)) calc(14 * var(--u)) rgba(255,255,255,.22),
             inset 0 0 0 calc(1.5 * var(--u)) rgba(255,255,255,.92);
  flex:none}

HERO COPY
.eyebrow{top:calc(188.8 * var(--u)); font-size:calc(30.45 * var(--u)); font-weight:500;
  line-height:1; letter-spacing:calc(4 * var(--u)); text-indent:calc(4 * var(--u)); color:#fff}
h1.title{top:calc(268 * var(--u)); font-family:var(--font-serif); font-weight:400;
  font-size:calc(112.4 * var(--u)); line-height:1; letter-spacing:calc(2 * var(--u));
  text-indent:calc(2 * var(--u)); color:#fff}
.rule{top:calc(405.5 * var(--u)); height:calc(5 * var(--u)); font-size:0; line-height:0;
  padding-right:calc(2 * var(--u))}
.rule span{display:inline-block; vertical-align:top; width:calc(100 * var(--u));
  height:100%; border-radius:calc(2.5 * var(--u)); background:var(--cyan)}
p.lede{top:calc(433.2 * var(--u)); font-size:calc(18.36 * var(--u)); font-weight:400;
  line-height:calc(30 * var(--u)); color:rgba(255,255,255,.95)}

CTA ROW
.cta{top:calc(597 * var(--u)); height:calc(66 * var(--u)); isolation:isolate}
.cta a{display:inline-flex; align-items:center; justify-content:center;
  width:calc(216 * var(--u)); height:calc(66 * var(--u)); border-radius:calc(33 * var(--u));
  font-size:calc(17 * var(--u)); font-weight:700; letter-spacing:0; text-indent:0;
  line-height:1; color:var(--btn-ink); text-decoration:none; padding-top:calc(2 * var(--u));
  background:linear-gradient(180deg,#ffffff 0%,#d6e8f8 4%,#d9ecfe 72%,#ffffff 100%);
  box-shadow:0 calc(10 * var(--u)) calc(18 * var(--u)) calc(-8 * var(--u)) rgba(255,255,255,.50),
             0 0 calc(26 * var(--u)) rgba(255,255,255,.18),
             inset 0 0 0 calc(2 * var(--u)) rgba(255,255,255,.9)}
.label{position:absolute; top:calc(33 * var(--u)); font-family:var(--font-serif);
  font-weight:400; font-size:calc(17.8 * var(--u)); letter-spacing:calc(4.6 * var(--u));
  line-height:1; color:#fff; white-space:nowrap}
.label.label-l{left:calc(111 * var(--u))}
.label.label-r{right:calc(104 * var(--u))}

SCROLL CONTROL
.scroll{position:absolute; z-index:4; left:50%;
  margin-left:calc((-48 - (var(--gutter) / 2)) * var(--u)); bottom:calc(68 * var(--u));
  width:calc(96 * var(--u)); height:calc(96 * var(--u)); border-radius:50%;
  background:rgba(24,30,42,.70); -webkit-backdrop-filter:blur(calc(6 * var(--u)));
  backdrop-filter:blur(calc(6 * var(--u))); border:0; display:flex; align-items:center;
  justify-content:center; cursor:pointer}
.scroll svg{width:calc(20 * var(--u)); height:calc(25 * var(--u)); display:block}

FOCUS
.scroll:focus-visible,.links a:focus-visible,.enroll:focus-visible,
.cta a:focus-visible,.logo:focus-visible{
  outline:calc(2 * var(--u)) solid var(--cyan); outline-offset:calc(3 * var(--u))}
.planet:focus-visible{outline:calc(2 * var(--u)) solid var(--cyan);
  outline-offset:calc(6 * var(--u)); border-radius:50%}

════════════════════════════════════════════════════════════════════════
7. RESPONSIVE — six tiers, in this exact source order
════════════════════════════════════════════════════════════════════════
A) NAV COLLAPSE  @media (max-width:1030px), (max-height:620px)
Threshold is derived: below ~1030px the desktop labels fall under ~14px with sub-40px
touch targets. The SAME <a> elements re-compose into a panel — never duplicate markup.
  .navrow{left:calc(25 * var(--u)); right:calc(25 * var(--u))}
  .burger{display:flex}
  .links{position:absolute; top:calc(96 * var(--u)); right:0; z-index:5;
    width:min(calc(324 * var(--u)), calc(100vw - 50 * var(--u)));
    flex-direction:column; align-items:stretch; padding:calc(12 * var(--u));
    border-radius:calc(20 * var(--u)); background:rgba(9,21,42,.84);
    backdrop-filter:blur(calc(18 * var(--u)));
    border:calc(1 * var(--u)) solid rgba(255,255,255,.14);
    box-shadow:0 calc(18 * var(--u)) calc(44 * var(--u)) rgba(2,8,20,.55);
    opacity:0; visibility:hidden; transform:translateY(calc(-10 * var(--u)));
    transition:opacity .24s ease, transform .28s cubic-bezier(.4,0,.2,1), visibility .28s}
  .navrow[data-open="true"] .links{opacity:1; visibility:visible; transform:none}
  .links a:nth-child(n){margin-left:0; height:auto; justify-content:flex-start;
    padding:calc(14 * var(--u)) calc(16 * var(--u)); font-size:calc(19 * var(--u));
    letter-spacing:calc(-.6 * var(--u))}
  .links a + a{border-top:calc(1 * var(--u)) solid rgba(255,255,255,.08)}
  .links a[aria-current="page"]::after{top:auto; bottom:calc(9 * var(--u));
    left:calc(16 * var(--u)); right:auto; width:calc(26 * var(--u)); height:calc(3 * var(--u))}
  .links a.enroll{margin:calc(14 * var(--u)) 0 calc(2 * var(--u)); width:100%;
    height:calc(46 * var(--u)); border-radius:calc(23 * var(--u)); justify-content:center;
    padding-top:calc(2 * var(--u)); font-size:calc(18 * var(--u));
    letter-spacing:calc(-.4 * var(--u)); border-top:0}
  .links a.enroll::after{content:none}

B) TABLET  @media (min-width:580px) and (max-width:1030px) and (min-height:621px)
The hero is a centred symmetric composition — it is re-proportioned, NOT stacked. The
flanking planet / button / planet row is the section's signature and must survive.
  :root{--gutter:0;
    --u:min(max(min(.85px, calc(100vh / 780)), min(calc(100vw / 900), calc(100vh / 1163))), 1.15px);
    --dh-px:calc(1120 * var(--u));
    --vshift:calc(max(0px, (100vh - var(--dh-px))) * .38)}
  (repeat inside @supports (height:100dvh) with 100dvh)
  .navbar::after{left:calc(25 * var(--u)); right:calc(25 * var(--u))}
  p.lede br{display:none}
  p.lede{max-width:calc(620 * var(--u)); margin-left:auto; margin-right:auto; text-wrap:pretty}
  .label.label-r{right:calc(116 * var(--u))}

C) PHONE  @media (max-width:579px), (max-height:620px)
Below the nav the interface FLOWS (flex column) instead of being absolutely pinned, so
it adapts to however many lines the copy wraps onto.
  :root{--gutter:0;
    --u:max(min(.92px, calc(100vh / 620)), min(calc(100vw / 430), calc(100vh / 880)));
    --dh-px:calc(880 * var(--u));
    --vshift:calc(max(0px, (100vh - var(--dh-px))) * .34)}
  (repeat inside @supports (height:100dvh))
  .navbar{height:calc(76 * var(--u))}
  .navbar::after{left:calc(24 * var(--u)); right:calc(24 * var(--u)); top:calc(74 * var(--u))}
  .navrow{left:calc(24 * var(--u)); right:calc(24 * var(--u)); height:calc(74 * var(--u))}
  .links{top:calc(84 * var(--u))}
  .copy{top:calc(76 * var(--u)); display:flex; flex-direction:column; align-items:center;
    padding:calc(56 * var(--u)) calc(26 * var(--u)) 0}
  .col{position:static; width:100%; right:auto}
  .eyebrow{font-size:calc(20 * var(--u)); letter-spacing:calc(5 * var(--u)); text-indent:calc(5 * var(--u))}
  h1.title{font-size:calc(86 * var(--u)); margin-top:calc(15 * var(--u))}
  .rule{margin-top:calc(20 * var(--u)); height:calc(5 * var(--u)); padding-right:0}
  .rule span{width:calc(84 * var(--u))}
  p.lede{margin-top:calc(19 * var(--u)); font-size:calc(15.5 * var(--u));
    line-height:calc(26 * var(--u)); max-width:calc(400 * var(--u)); text-wrap:balance}
  p.lede br{display:none}
  .cta{top:auto; margin-top:calc(32 * var(--u)); height:calc(60 * var(--u)); position:relative}
  .cta a{width:calc(190 * var(--u)); height:calc(60 * var(--u)); border-radius:calc(30 * var(--u))}
  .label{top:50%; transform:translateY(-50%); font-size:calc(16 * var(--u));
    letter-spacing:calc(4 * var(--u))}
  .label.label-l{left:calc(56 * var(--u))}
  .label.label-r{right:calc(60 * var(--u))}
  .planet-l{width:calc(89 * var(--u)); left:calc(-44.5 * var(--u)); top:calc(-17 * var(--u))}
  .planet-r{width:calc(89 * var(--u)); right:calc(-44.5 * var(--u)); top:calc(-18.3 * var(--u))}
  .scroll{width:calc(74 * var(--u)); height:calc(74 * var(--u));
    margin-left:calc(-37 * var(--u)); bottom:calc(49 * var(--u))}
  .scroll svg{width:calc(15.5 * var(--u)); height:calc(19.5 * var(--u))}

D) @media (max-height:660px){ .scroll{display:none} }   /* no room for it */

E) SHORT  @media (max-height:620px)
  :root{--u:max(min(.85px, calc(100vh / 470)), min(calc(100vw / 640), calc(100vh / 560)));
    --dh-px:calc(560 * var(--u)); --vshift:0px}
  .copy{padding-top:calc(26 * var(--u))}
  h1.title{font-size:calc(64 * var(--u)); margin-top:calc(8 * var(--u))}
  .rule{margin-top:calc(12 * var(--u))}
  p.lede{margin-top:calc(12 * var(--u))}
  .cta{margin-top:calc(20 * var(--u))}
  .label{top:50%; transform:translateY(-50%); font-size:calc(15 * var(--u))}
  .label.label-l{left:calc(58 * var(--u))}
  .label.label-r{right:calc(58 * var(--u))}
@media (min-width:821px) and (max-height:660px){ .copy{transform:none} }

F) NARROW PHONE  @media (max-width:500px)   — declared LAST so it wins inside tier E
Below ~500px the label / button / label triad can no longer share a line, so the two
labels drop to their own band beneath the button, still left- and right-anchored.
  .label{top:calc(100% + 34 * var(--u)); transform:none; font-size:calc(15 * var(--u))}
  .label.label-l{left:calc(6 * var(--u))}
  .label.label-r{right:calc(6 * var(--u))}

@media (prefers-reduced-motion:reduce){ .links,.burger span{transition:none} }

════════════════════════════════════════════════════════════════════════
8. ENTRANCE ANIMATION — runs once, then deletes itself
════════════════════════════════════════════════════════════════════════
Motion language: DRAW hairlines scale from centre · REVEAL type rises out of a mask ·
RISE copy lifts a short distance · SETTLE pills arrive with a small scale resolve ·
FADE the full-bleed backdrop resolves in (it fades, never translates — a translate
would drag a bare edge into frame).

In <head>, BEFORE the stylesheet, a blocking inline script so the opening frame is
composed rather than flashing the finished page:
  (function(){try{
    if(!window.matchMedia||!matchMedia('(prefers-reduced-motion: reduce)').matches){
      document.documentElement.classList.add('anim');
    }
  }catch(e){}})();

@keyframes ent-reveal{from{transform:translateY(115%)}to{transform:translateY(0)}}
@keyframes ent-rise{from{opacity:0;transform:translateY(var(--rise,10px))}to{opacity:1;transform:none}}
@keyframes ent-settle{from{opacity:0;transform:translateY(var(--rise,8px)) scale(.965)}to{opacity:1;transform:none}}
@keyframes ent-draw{from{transform:scaleX(0)}to{transform:scaleX(1)}}
@keyframes ent-fade{from{opacity:0}to{opacity:1}}

.anim{--e-expo:cubic-bezier(.16,1,.3,1); --e-quint:cubic-bezier(.22,1,.36,1);
      --e-slow:cubic-bezier(.4,0,.2,1)}
.ent-mask,.ent-line{display:block}
.anim .ent-mask{overflow:hidden; padding-top:.18em; margin-top:-.18em}
.anim .ent-line{transform:translateY(115%); will-change:transform}
.anim .navbar::after,.anim .links a[aria-current="page"]::after,.anim .rule span{transform:scaleX(0)}
.anim .logo,.anim .links a,.anim .burger,.anim p.lede,.anim .label,.anim .cta a,
.anim .scroll,.anim .sky{opacity:0}
.anim .sky{will-change:opacity}

Sequence (all `both` fill):
  .anim.play .sky                               ent-fade   1.6s  --e-slow   .25s
  .anim.play .navbar::after                     ent-draw    .9s  --e-expo   .12s
  .anim.play .logo              --rise 8u       ent-rise    .6s  --e-quint  .20s
  .anim.play .links a           --rise 8u       ent-rise   .55s  --e-quint
    nth-child(1) .26s · (2) .31s · (3) .36s · (4) .41s
  .anim.play .links a.enroll, .burger  --rise 8u  ent-settle .6s --e-quint  .46s
  .anim.play .links a[aria-current]::after      ent-draw   .55s  --e-expo   .60s
  .anim.play .eyebrow .ent-line                 ent-reveal .75s  --e-expo   .30s
  .anim.play h1.title .ent-line                 ent-reveal .95s  --e-expo   .44s
  .anim.play .rule span                         ent-draw   .65s  --e-expo   .78s
  .anim.play p.lede             --rise 12u      ent-rise    .7s  --e-quint  .86s
  .anim.play .label.label-l     --rise 8u       ent-rise    .6s  --e-quint 1.04s
  .anim.play .label.label-r     --rise 8u       ent-rise    .6s  --e-quint 1.10s
  .anim.play .cta a             --rise 8u       ent-settle  .7s  --e-quint 1.14s
  .anim.play .scroll                            ent-settle  .6s  --e-quint 1.42s

Driver script: if <html> lacks .anim, return. Otherwise wait for document.fonts.ready
(with a 500ms guard timeout so a font stall cannot block the page), then two nested
requestAnimationFrame calls, then add .play. After 2150ms remove BOTH .anim and .play,
leaving the page in its authored static state with no timers or residual transforms.

════════════════════════════════════════════════════════════════════════
9. PLANET SWITCHER — JS behaviour
════════════════════════════════════════════════════════════════════════
const ORDER = ['earth','venus','mars'];
Per planet store: name (uppercase), cut-out URL, still URL, and lede HTML:
  earth: "Learn more about the fascinating details that we call our home, Planet Earth.
          Course enrollment 
starts today. Early Bird tickets typically last a week,
          don&rsquo;t miss out!"
  venus: "The hottest world in our solar system, wrapped in clouds of sulfuric acid.
          Course enrollment 
starts today. Early Bird tickets typically last a week,
          don&rsquo;t miss out!"
  mars:  "The rust-red desert world, home to the tallest volcano we know of. Course
          enrollment 
starts today. Early Bird tickets typically last a week,
          don&rsquo;t miss out!"

show(next):
  1. bail if unknown or already featured
  2. for each clip: if it is `next` and has no src but has data-src, assign src from
     data-src (first-use fetch). Toggle .is-active on the matching clip; .play() it
     (catch the rejected promise); .pause() every other clip.
  3. set .sky background-image to that planet's still — keeps reduced-motion users,
     who never see the clip, on the correct planet
  4. set the h1's .ent-line textContent to the planet name
  5. set p.lede innerHTML to that planet's lede
  6. rest = ORDER minus `next`; slot L gets rest[0], slot R gets rest[1]. For each slot:
     toggle .is-shown on the img whose data-planet matches — DO NOT touch img.src —
     set the button's data-planet, set aria-label "Show <NAME>", set the label text.

warm(planet): if that clip has no src but has data-src, set preload='auto', assign
src, call load(). Bind it to pointerenter AND focus on both buttons so the fetch starts
on intent rather than on click. After first paint, requestIdleCallback(warm all,
{timeout:4000}) — fall back to setTimeout 2500ms — to pull the remaining clips down.

Bind click on both buttons -> show(this.dataset.planet). Call show('earth') once at the
end of setup to establish the initial state.

════════════════════════════════════════════════════════════════════════
10. BURGER MENU — JS behaviour
════════════════════════════════════════════════════════════════════════
set(open) writes navrow.dataset.open, the button's aria-expanded, and its aria-label
("Open navigation" / "Close navigation"). Initialise with set(false). Toggle on button
click (stopPropagation). Close on: a document click outside the menu and button, Escape
(then return focus to the button), and any click on a link inside the menu.

════════════════════════════════════════════════════════════════════════
11. ACCESSIBILITY
════════════════════════════════════════════════════════════════════════
· Side planets are real <button type="button"> with a dynamic aria-label — keyboard
  operable, focus-visible ringed in --cyan.
· Videos are aria-hidden="true", muted, playsinline (required for iOS autoplay).
· Cut-out <img> tags carry alt="" — they are decorative; the labels carry the meaning.
· prefers-reduced-motion: no entrance sequence at all (the .anim class is never added),
  clips hidden in favour of the still, no planet hover/press transforms, no nav
  transitions.
· .copy spans the whole frame with pointer-events:none so it can never swallow nav
  clicks; only .copy a and .planet re-enable pointers.

════════════════════════════════════════════════════════════════════════
12. ACCEPTANCE CHECKS
════════════════════════════════════════════════════════════════════════
· Loads with EARTH featured, VENUS left, MARS right. Only the Earth clip has a src.
· Clicking a side planet swaps the visible cut-out in the SAME FRAME (measurable at
  <2ms) — no flash of the previous planet.
· Earth -> Venus -> Mars -> Earth returns to the exact initial state.
· Backdrop crossfades in .22s; headline, lede, both cut-outs and both labels all update
  together.
· body never scrolls horizontally at any width from 320px to 2560px.
· Entrance runs once; afterwards <html> carries neither .anim nor .play.