  :root {
    --void: #05070d;
    --void-2: #0b1018;
    --ink: #e8eef7;
    --ink-dim: #7d8aa0;
    /* Nog een stap zwakker dan --ink-dim, voor dingen die er wel moeten zijn maar geen
       aandacht mogen trekken: de uitklap-chevron, de neutrale statusstip, uitlegtekst.
       Ontbrak tot 2026-07-30 volledig terwijl vier regels hem al gebruikten. Een
       ongedefinieerde custom property is ongeldig bij het berekenen, dus `color` erfde
       van de parent en `background` viel terug op transparant: de neutrale stip in de
       verbindingenlijst was onzichtbaar. */
    --ink-faint: #525d72;
    --hair: #1c2433;
    --quake: #ff6b3d;
    --volcano: #ffd23f;
    --storm: #4dd0e1;
    --ice: #b3e5fc;
    --wildfire: #ff3b30;
    --aqi: #a78bfa;
    --lightning: #bcd6ff;
    --aurora: #1fbf5a;
    --accent: #ff6b3d;

    /* ---- De onderrand: knoppenrij, tijdrij, en wat daarboven stopt ---------
       (sessie 24, uitgebreid in 25)

       ONDERAAN STAAN TWEE DINGEN DIE ALTIJD ZICHTBAAR ZIJN: de knoppenrij en de
       tijdrij. Dat is sinds sessie 25 een belofte en geen toevalligheid — tot
       dan verborg `body.sheet-open` het hele tijdeiland zodra er een paneel
       openstond, en op een telefoon stond het lagenpaneel bij het opstarten
       open. GEMETEN op 375x812: het eiland had daar `display: none` en hoogte 0.
       De tijdslider was er dus niet, en dat is precies waarom hij over het hoofd
       werd gezien.

       Twee GEMETEN maten en twee lijnen die eruit volgen:

         --dock-h      39 px   11 px padding boven en onder, 15 px icoon, 2 px rand
         --time-ctl-h  40 px   de tijdrij: afspeelknoppen en schuiver, en niets
                               daarboven. Op desktop én op 375 px even hoog.

         --dock-lift    de lijn boven de knoppenrij  -> daar staat de tijdrij
         --panel-lift   waar een paneel stopt

       `--panel-lift` is op een breed scherm gelijk aan `--dock-lift`, en dat is
       niet slordig maar uitgerekend: de tijdrij staat daar op `--dock-bottom`
       (28) en is 40 hoog, dus hij eindigt op 68 — ónder `--dock-lift` (81).
       Links overlapt hij sowieso niet: `.panel` loopt tot x=308 en het eiland
       begint op 316. Pas onder 640 px wordt het eiland schermbreed, en dáár
       stopt een paneel op de rand van de BEDIENING — hij mag dan over het
       zwevende moment en over de tijdlijn heen. Zie de mobiele media-query.

       Waarom variabelen en geen ingetikte getallen: `.panel`, `.nav-panel`,
       `.readout` en `.qlist` moeten allemaal dezelfde onderrand aanhouden, en
       twee media queries verzetten de rij. Ingetikte reeksen lopen stil
       uiteen — dat is in sessie 24 al één keer gebeurd, toen `.time-island` en
       `.readout` op een eigen `bottom: 28px` bleven staan terwijl de rij naar
       12 px ging. */
    /* De ruimte tussen de blokken in de masthead, en die tussen de knoppen
       rechtsboven onderling. De tweede is kleiner: drie iconen horen als één
       groep te lezen. Zie .qlist-open. */
    --masthead-gap: 18px;
    --icon-gap: 6px;
    --dock-bottom: 28px;
    --dock-h: 39px;
    --dock-gap: 14px;
    /* DE HOOGTE VAN DE BEDIENING: de afspeelknoppen en de schuiver, en niets
       daarboven. Dat onderscheid is nieuw sinds sessie 31 en het is de hele
       reden dat er op een telefoon weer scene te zien is.

       Tot dan stond hier `--time-h`: "de hoogte van álles wat op de
       tijdverdieping staat". Op 375 px was dat 74 — 40 (rij) + 6 + 28, waarbij
       die 28 de hoogste stand van het zwevende moment is: daar verschijnt een
       resetknop zodra het moment verschoven is (gemeten in sessie 26). In de
       magnetosfeer kwam de tijdlijn van 132 px er nog bij, en dus 214.

       Een paneel reserveerde die hele stapel, en dat kostte de halve scene voor
       dingen die je achter dat paneel toch niet leest. Nu reserveert het alleen
       de BEDIENING — die moet bereikbaar blijven — en bedekt het de rest. */
    --time-ctl-h: 40px;
    --time-gap: 10px;

    /* ---- HET TERRA MODEL-VENSTER (B4, sessie 31) --------------------------
       De hoogte staat hier omdat twee dingen hem lezen: de doos van het canvas,
       en de plek waar het zwevende moment overheen klimt (zie de mobiele
       media-query). Een paneel reserveert hem NIET — dat stopt op de rand van
       de bediening en mag de tijdlijn bedekken.

       De acht kleuren dragen een voorvoegsel en de drie inktwaarden niet: `--ink`
       en familie zijn van de hele app, maar een globale `--model` of `--edge` zou
       in een app met dertig lagen vroeg of laat iets anders gaan betekenen. De
       waarden komen uit de PoC, waar ze als SET zijn afgestemd — losse
       vervangingen breken de contrastverhoudingen die daar gemeten zijn.

       De drieslag MEASURED · DERIVED · MODEL is de kern: wat een instrument zag,
       wat daar rekenkundig uit volgt, en wat een model beweert. Drie kleuren die
       je uit elkaar moet kunnen houden zonder legenda erbij te houden. */
    --msps-h: 178px;
    --msps-gap: 8px;                 /* de gap van .msphere-time zelf */
    --msphere-measured: #4fd1c5;
    --msphere-derived:  #6ea8ff;
    --msphere-model:    #b98cff;
    --msphere-goes:     #ffd166;
    --msphere-open:     #f2a33c;
    /* DE DRIE LIJNSTATEN, EN DIT IS DE ENIGE PLEK WAAR ZE STAAN (sessie 32).
       `Core.Build` levert een KLASSE per lijn (0/1/2) en nooit een kleur, dus
       een themawissel raakt geen enkele berekening. `--msphere-open` erboven
       doet dubbel dienst: dezelfde oranje in de tijdlijn en op een open lijn,
       en dat is geen toeval maar dezelfde uitspraak — zuidwaarts weer, open
       veld.

       `--msphere-unres` deelt de familie met `closed`: de verwantschap met cyaan
       hoort zichtbaar te zijn, want onopgelost gaat over de INTEGRATIE en niet
       over een derde fysische toestand. Het betekenisonderscheid wordt gedragen
       door de streepjes, de dunnere lijn en een lagere dekking — niet door de
       kleur.

       LICHTER DAN DE POC (#588f9c), en dat is een meting en geen smaak. Die
       waarde is gekozen op de POC's eigen achtergrond; op Terra's zwart haalde
       zijn ZUIDELIJKE tint contrast 3,15 terwijl alle andere lijnen boven de 7,5
       zitten — onzichtbaar, en daardoor niet meer te onderscheiden van de
       gestippelde lijn die dezelfde familie draagt (Terry, sessie 32). Op deze
       waarde is de donkerste tint 8,93, en ligt zuid-dicht 0,338 van
       noord-onopgelost in plaats van 0,194. Drie dempingen zijn genoeg. */
    --msphere-closed:   #4fd1c5;
    --msphere-unres:    #93b9c4;
    --msphere-mp:       #6ea8ff;
    --msphere-saa:      #d4564f;
    --msphere-edge:     rgba(140,170,210,0.16);
    /* INKT VOOR TEKST DIE OP EEN BAND LIGT. De IMF-sector is een balkenlane die
       zijn volle hoogte vult, dus het lane-label ligt altijd op oranje of op
       blauw — en `--ink-dim` (#7d8aa0) is op allebei onleesbaar. Gemeten
       contrast van deze waarde: 9,2 op #f2a33c en 7,9 op #6ea8ff, tegen 1,6 en
       1,3 voor de oude. Waar de sector NIET gemeten is staat er geen band en is
       deze inkt op zijn beurt donker-op-donker; daar tekent chart.js zijn eigen
       arcering met de zin "no Bx or Bt", dus die plek is niet stil. */
    --msphere-band-ink: #0a1220;
    /* THE SUN'S TIME STRIP (js/ui/solar-time.js). One lane where the
       magnetosphere has six, so it stands lower. The flux takes the
       magnetosphere's measured teal on purpose: the same colour says the same
       thing in both strips. The dashed mark for the image on screen takes the
       warm tone of the sun state's own labels. */
    --sols-h: 110px;
    --sols-gap: 8px;
    --sols-week-h: 24px;             /* the slider under the sun's lane */
    --solar-measured: var(--msphere-measured);
    --solar-class: rgba(160,180,210,0.42);
    --solar-onscreen: #ffb347;
    --solar-flag: #d4564f;
    --solar-film: #7fd1c7;
    /* The flare classes, weakest to strongest: a quiet grey for A and B, then
       blue, orange and red. None of them is the dashed amber of the image on
       screen, which is a different kind of mark. The flare card takes its colour
       from here too, so a class looks the same on the lane and on the card. */
    --solar-flare-b: #6b7a90;
    --solar-flare-c: #6ea8ff;
    --solar-flare-m: #ff8a3d;
    --solar-flare-x: #ff4d5e;
    --dock-lift: calc(var(--dock-bottom) + var(--dock-h) + var(--dock-gap));
    --panel-lift: var(--dock-lift);
    /* VRIJE RUIMTE BOVEN EEN PANEEL, en dit getal is een BEGINWAARDE.

       38 px was geraden, en het klopte niet: GEMETEN op 1281x881 loopt de
       masthead tot 84 px en het merk met de wereldklok van 18 tot 66, terwijl
       het paneel op 38 begon en 28 tot 308 breed is. De legenda van de
       magnetosfeer maakt het paneel zo hoog dat hij die grens ook echt haalt, en
       dan staat de klok achter een paneel (Terry, sessie 32).

       Een paneel hoort onder de masthead te beginnen, en hoe hoog die is weet
       alleen de masthead zelf — hij groeit met zijn padding, zijn lettergrootte
       en zijn ondertitel. Daarom MEET index.html hem en schrijft hij deze
       variabele op `documentElement` (zie borgPanelTopGap). Deze waarde blijft
       staan als vangnet voor het moment vóór die meting, en als beginstand voor
       de standalone waar de meting om welke reden dan ook niet zou lopen.

       LET OP: dit is een custom property, en die wordt opgelost waar hij
       GEDECLAREERD staat. Overschrijven moet dus op `:root` gebeuren en niet op
       `<body>` — dat kostte in sessie 31 al 93 px paneeloverlap. */
    --panel-top-gap: 38px;

    /* ---- DE Z-LADDER (sessie 25) ----------------------------------------
       Tot deze sessie stonden zeven vensters allemaal op 10 of 11, en won dus
       de DOM-volgorde. Dat is geen ordening maar een toevalligheid, en met een
       model waarin panelen elkaar niet meer sluiten maar stapelen, moet de
       volgorde een besluit zijn:

         labels   de beving-labels horen BIJ DE BOL, niet bij de app. Ze stonden op
                  een ingetikte 9, dus boven het tijdeiland, en dekten daar de
                  datumvelden af. Nu onderaan: ze mogen alles wat je bedient laten
                  staan, want er komt vanzelf een volgende in beeld
         eiland   het uitklapdeel hoort ACHTER de panelen weg te schuiven
         paneel   de werkbladen: Layers, Navigate, Details
         chroom   bel, bevingenlijst, instellingen — altijd bereikbaar
         detail   wat je aantikte moet leesbaar zijn, ook met de lijst open
                  (Terry's keuze: de lijst is de weg waarlangs je hier komt)
         dock     de enige weg terug; mag onder niets liggen
         toast    ongewijzigd, staat al op 40 in zijn eigen regel */
    --z-labels: 6;
    --z-island: 8;
    --z-panel: 10;
    --z-chrome: 35;
    --z-detail: 40;
    --z-dock: 45;
    /* Boven het laadscherm (50) en het bewerkscherm (60), want dit is het
       eerste wat een nieuwe bezoeker ziet en er hoort niets overheen te
       kunnen. Die twee staan met een ingetikt getal in hun eigen regel;
       deze staat hier omdat hij twee klanten heeft die gelijk moeten
       lopen — het welkomstscherm en de rondleiding erna. */
    --z-onboard: 70;
  }

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

  /* Het grijze vlak dat een tik op een knop even oplicht. Op een aanraakscherm leest
     dat als een storing, niet als feedback; de knoppen hebben hun eigen :active-stijl. */
  * { -webkit-tap-highlight-color: transparent; }

  html, body {
    height: 100%;
    background: var(--void);
    color: var(--ink);
    font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
    overflow: hidden;
    /* Geen doorschuiven aan de randen: geen pull-to-refresh en geen elastische
       bounce die de hele app een stukje van het scherm af duwt. */
    overscroll-behavior: none;
    /* Knijp-zoom en dubbeltik-zoom van de BROWSER uit, schuiven blijft mogelijk.
       Bewust niet `none`: de effectieve touch-action is de doorsnede van het element
       en al zijn voorouders, dus `none` hier zou het scrollen in het paneel en de
       bottom sheet ook onmogelijk maken. `pan-x pan-y` laat precies het schuiven over
       dat de scrollende delen nodig hebben en niets anders. */
    touch-action: pan-x pan-y;
  }

  /* Op een aanraakscherm is er geen muis om nauwkeurig mee te selecteren, en dan is
     press-and-hold vrijwel altijd per ongeluk: je krijgt een selectie of het
     deel-menu van iOS over de bol heen. */
  @media (pointer: coarse) {
    html {
      -webkit-user-select: none;
      user-select: none;
      -webkit-touch-callout: none;   /* geen "kopieer / deel"-menu bij press-and-hold */
    }
  }

  /* ---- SELECTEREN KAN ALLEEN IN EEN PANEEL (sessie 25) -------------------
     OOK OP DESKTOP. Hier stond dat slepen met een muis "een bewuste handeling" is,
     en dat klopt niet voor deze app: draaien aan de bol ís slepen. Ging je daarbij
     langs een beving-label, dan selecteerde je de tekst erin — en die selectie bleef
     staan tot je ergens anders klikte.

     Dus omgekeerd opgezet: niets is selecteerbaar, behalve waar er iets te LEZEN
     valt. Dat is precies de scheiding uit deze sessie — de scene en zijn labels zijn
     iets waar je aan draait, een paneel en het detailvenster zijn iets wat je leest
     en waar je een coördinaat of een bronnaam uit wilt kunnen kopiëren.

     De uitzondering staat op de INHOUD en niet op de hele container: een paneelkop,
     een tab en een schakelaar zijn knoppen, en die hoeven niet selecteerbaar te zijn.
     `.acc-head`, `.elayer` en `.tab` zetten hem verderop zelf al op `none`. */
  html { -webkit-user-select: none; user-select: none; }
  .panel-body,
  .settings-body,
  .nav-body,
  .qlist-body,
  .notif-list,
  .readout,
  .ti-panel {
    -webkit-user-select: text;
    user-select: text;
  }

  /* De bol handelt draaien én knijpen zelf af (OrbitControls). Hier mág `none`, want
     onder de bol valt niets te scrollen: elk gebaar dat hier begint is voor de globe.
     Dit is wat knijpen uitsluitend laat in- en uitzoomen op de aarde. */
  #globe { position: fixed; inset: 0; touch-action: none; }

  /* ---- Header ---- */
  .masthead {
    position: fixed;
    top: 0; left: 0; right: 0;
    padding: 18px 28px;
    display: flex;
    align-items: flex-start;
    /* AS A VARIABLE, because the two icon buttons on the right take this gap
       back with a negative margin to stand as one group. Typed in twice, the
       phone's smaller gap made that margin larger than the gap itself and the
       icons overlapped by two pixels. */
    gap: var(--masthead-gap);
    z-index: 10;
    pointer-events: none;
    background: linear-gradient(to bottom, rgba(5,7,13,0.85), transparent);
  }
  /* Wordmark en klok op één regel, ondertitel eronder (2026-07-31). */
  .brand { display: flex; flex-direction: column; gap: 3px; }
  .brand-row { display: flex; align-items: baseline; gap: 14px; }
  .wordmark {
    font-size: 13px;
    font-weight: 700;
    letter-spacing: 0.38em;
    text-transform: uppercase;
  }
  .wordmark .dot { color: var(--accent); }
  .tagline {
    font-size: 11px;
    color: var(--ink-dim);
    letter-spacing: 0.04em;
  }
  .clock {
    font-variant-numeric: tabular-nums;
    font-size: 11px;
    color: var(--ink-dim);
    letter-spacing: 0.08em;
    pointer-events: none;
  }

  /* ---- Meldingen: bel-knop + geschiedenis-paneel ---- */
  .notif-btn {
    margin-left: auto;
    pointer-events: auto;
    position: relative;
    display: flex; align-items: center;
    background: rgba(11,16,24,0.72);
    backdrop-filter: blur(14px);
    border: 1px solid var(--hair);
    border-radius: 9px;
    padding: 7px 9px;
    color: var(--ink-dim);
    cursor: pointer;
    transition: color 0.18s, border-color 0.18s;
  }
  .notif-btn:hover { color: var(--ink); border-color: var(--ink-dim); }
  /* LINKS VAN DE BEL EN NIET RECHTSBOVEN (sessie 31, Terry). Hij stond op
     `right: -6px` en stak daarmee over de knop ernaast — de bel is de eerste van
     drie in dezelfde hoek, dus rechts van hem is altijd bezet. Links is dat
     nooit: daar loopt de masthead door tot de wordmark, en die eindigt op 375 px
     al op 115 terwijl de knoppen op 260 beginnen. */
  .notif-badge {
    position: absolute; top: -6px; left: -6px;
    min-width: 16px; height: 16px;
    padding: 0 4px;
    border-radius: 8px;
    background: var(--accent);
    color: #1a0d06;
    font-size: 9px; font-weight: 700;
    display: flex; align-items: center; justify-content: center;
  }
  /* EN HIJ GING NOOIT UIT. `updateNotifBadge()` zet keurig `hidden` bij nul
     ongelezen meldingen, maar de regel hierboven zet een expliciete
     `display: flex` — en display in een klasse verslaat het hidden-attribuut.
     Gevolg: er stond permanent een oranje "0" naast de bel, en niemand zag waar
     dat vandaan kwam omdat de JS het goed deed.

     Dezelfde val als bij .dock-btn, .ti-panel, .nav-panel, .view-switch,
     .antipode-exit en .time-reset. Zesde keer in dit bestand. */
  .notif-badge[hidden] { display: none; }
  .notif-panel {
    position: fixed;
    top: 62px; right: 24px;
    z-index: var(--z-chrome);
    width: 300px;
    max-height: 52vh;
    display: none;
    background: rgba(11,16,24,0.92);
    backdrop-filter: blur(16px);
    border: 1px solid var(--hair);
    border-radius: 14px;
    overflow: hidden;
    box-shadow: 0 12px 40px rgba(0,0,0,0.55);
  }
  .notif-head {
    display: flex; justify-content: space-between; align-items: center;
    padding: 12px 16px;
    border-bottom: 1px solid var(--hair);
    font-size: 10px; font-weight: 600; letter-spacing: 0.18em; text-transform: uppercase; color: var(--ink-dim);
  }
  .notif-head button { background: none; border: none; color: var(--ink-dim); font-family: inherit; font-size: 10px; text-transform: none; letter-spacing: 0.02em; cursor: pointer; }
  .notif-head button:hover { color: var(--accent); }

  /* ---- Instellingen, rechtsboven (sessie 25) ----------------------------
     Dezelfde hoek en dezelfde vorm als het meldingenpaneel — het zijn allebei
     app-chroom en ze zitten in dezelfde groep, dus ze horen er ook hetzelfde uit
     te zien. Wat afwijkt is de hoogte: hier zitten drie accordions in plus de
     bronvermelding, dus 52vh zou te krap zijn. */
  .settings-panel {
    position: fixed;
    top: 62px; right: 24px;
    z-index: var(--z-chrome);
    width: 300px;
    max-height: 74vh;
    max-height: 74dvh;
    display: none;
    flex-direction: column;
    background: rgba(11,16,24,0.92);
    backdrop-filter: blur(16px);
    border: 1px solid var(--hair);
    border-radius: 14px;
    overflow: hidden;
    box-shadow: 0 12px 40px rgba(0,0,0,0.55);
  }
  .settings-head {
    display: flex; justify-content: space-between; align-items: center;
    padding: 12px 16px;
    border-bottom: 1px solid var(--hair);
    font-size: 10px; font-weight: 600; letter-spacing: 0.18em; text-transform: uppercase; color: var(--ink-dim);
    flex-shrink: 0;
  }
  .settings-close {
    background: none; border: 0; padding: 2px 6px;
    border-radius: 7px;
    color: var(--ink-dim);
    font-family: inherit; font-size: 16px; line-height: 1;
    cursor: pointer;
    transition: color 0.15s, background 0.15s;
  }
  .settings-close:hover { color: var(--ink); background: rgba(255,255,255,0.08); }
  /* De inhoud scrolt, de kop niet — zelfde opzet als .panel-head / .panel-body. */
  .settings-body { overflow-y: auto; overscroll-behavior: contain; touch-action: pan-y; padding: 0 16px 14px; min-height: 0; }
  /* De eerste accordion heeft geen scheidingslijn nodig; de kop levert die al. */
  .settings-body > .acc:first-child { border-top: 0; }
  .notif-list { max-height: calc(52vh - 45px); overflow-y: auto; padding: 6px; touch-action: pan-y; }
  .notif-empty { padding: 28px 16px; text-align: center; font-size: 11px; color: var(--ink-dim); }
  .notif-item { display: flex; align-items: center; gap: 10px; padding: 9px 10px; border-radius: 9px; cursor: pointer; }
  .notif-item:hover { background: rgba(255,255,255,0.04); }
  .notif-item .nd { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; box-shadow: 0 0 7px currentColor; }
  .notif-item .nt { flex: 1; min-width: 0; }
  .notif-item .nt b { display: block; font-size: 12px; font-weight: 500; color: var(--ink); }
  .notif-item .nt span { display: block; font-size: 10px; color: var(--ink-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .notif-item .ntime { font-size: 10px; color: var(--ink-dim); font-variant-numeric: tabular-nums; flex-shrink: 0; }

  /* ---- Mode switch: zwevend eiland midden bovenaan (sessie 9) ----
     Segmented toggle: béide weergaven staan er altijd, de actieve is ingedrukt. Dezelfde
     bouw als de tabbalk in het paneel (dun kader, ingedrukte staat lichter), zodat het
     als één taal leest. Was tot 2026-07-31 één knop die van label wisselde; dan kon je
     niet zien dát er een tweede weergave bestond. */
  .mode-switch {
    position: fixed;
    top: 16px; left: 50%;
    transform: translateX(-50%);
    z-index: 11;              /* boven de masthead-gradient */
    pointer-events: auto;
    display: flex;
    gap: 3px;
    padding: 3px;
    background: rgba(11,16,24,0.72);
    backdrop-filter: blur(14px);
    border: 1px solid var(--hair);
    border-radius: 10px;
  }
  .mode-opt {
    display: flex;
    align-items: center;
    gap: 7px;
    padding: 6px 13px;
    background: transparent;
    border: none;
    border-radius: 7px;
    color: var(--ink-dim);
    font-family: inherit;
    font-size: 11px;
    font-weight: 500;
    letter-spacing: 0.04em;
    cursor: pointer;
    white-space: nowrap;
    transition: color 0.15s, background 0.15s;
  }
  .mode-opt:hover { color: var(--ink); }
  .mode-opt.active {
    color: var(--ink);
    background: rgba(255,255,255,0.10);
    font-weight: 600;
  }
  .mode-opt .mode-icon { font-size: 13px; line-height: 1; }

  /* DE CAMERASTAND DEELT DE GLEUF MET DE BOLWEERGAVE (sessie 31), dus hij deelt
     ook de vormtaal — `.mode-switch` staat er al op. Wat hij nodig heeft is de
     eigen regel voor `hidden`: `.mode-switch` zet een expliciete `display: flex`
     en die verslaat het attribuut. Dezelfde val als bij .dock-btn, .ti-panel en
     .nav-panel, en hij is in dit bestand nu vier keer ingelopen. */
  .view-switch[hidden] { display: none; }

  /* ---- De uitgang uit de doorkijk (sessie 31) ----
     Zwevend boven de tijdverdieping, in de taal van de knoppenrij: dun kader,
     blur, één handeling. Hij staat alleen op een smal scherm — daar is het
     detailvenster schermbreed en bedekt het de bol waar je doorheen kijkt.

     Hij houdt dezelfde onderrand aan als een paneel (`--panel-lift`): het is een
     vel dat over de scene ligt en het hoort op dezelfde lijn te stoppen. */
  .antipode-exit {
    position: fixed;
    left: 50%;
    transform: translateX(-50%);
    bottom: calc(var(--panel-lift) + env(safe-area-inset-bottom, 0px));
    z-index: var(--z-chrome);
    display: none;
    align-items: center;
    gap: 7px;
    padding: 8px 14px;
    background: rgba(11,16,24,0.82);
    backdrop-filter: blur(14px);
    border: 1px solid var(--accent);
    border-radius: 999px;
    color: var(--ink);
    font-family: inherit;
    font-size: 12px;
    font-weight: 500;
    cursor: pointer;
    pointer-events: auto;
    white-space: nowrap;
  }
  .antipode-exit svg { width: 15px; height: 15px; flex: 0 0 auto; }
  /* Eigen regel omdat de klasse hierboven `display` zet — zelfde val als bij
     .dock-btn, .ti-panel, .nav-panel en .view-switch. Vijf keer nu. */
  .antipode-exit[hidden] { display: none; }

  /* EN ZE STAAN NOOIT TEGELIJK. Een state met camerastanden verbergt de
     bolweergave: de magnetosfeer deed dat al, Space komt er nu bij. Daar is de
     bol helemaal weg (een heliocentrisch beeld heeft hem niet nodig), dus
     Realistic/Expert was er een schakelaar zonder zichtbaar gevolg.

     De zon blijft ongemoeid (Terry): die state heeft geen standen, dus de gleuf
     valt daar vanzelf terug op de bolweergave.

     Op de ID, om de reden die bij body.msphere-on staat. */
  body.space-on #mode-switch { display: none; }

  /* ---- Legend / layer toggles ---- */
  /* Flexkolom met een vaste kop en één scrollend middendeel (sessie 9, 2026-07-30).
     `overflow: hidden` op de buitenkant is wat de inhoud binnen de afgeronde hoeken
     houdt: zonder dat schuift tekst tijdens het scrollen over de rand van het glas.
     De padding zit daarom op .panel-head en .panel-body, niet meer hier. */
  .panel {
    position: fixed;
    left: 28px;
    /* BOVEN DE KNOPPENRIJ, niet erop (sessie 24). Stond op 28 px — dezelfde
       waarde als de rij — dus het paneel liep tot achter de knoppen door en de
       rij tekende er bovenop. Zichtbaar als een knop die in het paneel lijkt te
       zweven. Zie `--panel-lift` in :root voor de opbouw van dit getal. */
    bottom: var(--panel-lift);
    z-index: var(--z-panel);
    /* Breder en iets lager sinds sessie 17: de tabs dragen meer bediening dan bij
       248 px comfortabel paste. Alleen desktop — de query op 640 px zet `width: auto`
       en een eigen max-height, dus die blijft ongemoeid. */
    width: 280px;
    /* GEMETEN op een iPhone (iOS 18.7, landscape, 2026-07-31): `100vh` gaf daar 402 px
       terwijl de zichtbare hoogte 292 px was. Met `bottom: 28px` groeide het paneel dus
       tot 82 px BOVEN de schermrand, en daarmee stond de hele `.panel-head` (tabs plus
       inklappijl) buiten beeld. Vandaar `dvh`, dat de balken van de browser wél meetelt.
       De `vh`-regel blijft eronder staan als fallback voor browsers zonder `dvh`.

       De aftrek is de onderrand PLUS de vrije ruimte bovenaan, en niet meer een
       ingetikte 66: de onderrand is sinds sessie 24 een variabele omdat het
       paneel boven de knoppenrij moet stoppen. */
    max-height: calc(100vh - var(--panel-lift) - var(--panel-top-gap));
    max-height: calc(100dvh - var(--panel-lift) - var(--panel-top-gap));
    display: flex;
    flex-direction: column;
    overflow: hidden;
    background: rgba(11,16,24,0.72);
    backdrop-filter: blur(14px);
    border: 1px solid var(--hair);
    border-radius: 14px;
  }
  .panel-head {
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 14px 14px 12px;
    /* Dekkend genoeg dat de scrollende inhoud er niet doorheen schemert. Een geneste
       backdrop-filter werkt niet binnen een geclipte ouder die er zelf al een heeft,
       dus dit is een gewone, bijna ondoorzichtige laag bovenop het glas. */
    background: rgba(11,16,24,0.94);
    border-bottom: 1px solid var(--hair);
  }
  .panel-body {
    flex: 1 1 auto;
    overflow-y: auto;
    overscroll-behavior: contain;
    /* `touch-action: none` staat op html; hier geven we alleen het verticaal scrollen
       terug. Niet `auto`, want dan komt de knijp-zoom van de browser ook terug. */
    touch-action: pan-y;
    padding: 16px 18px 18px;
  }

  /* ---- DE SCROLLBALK, OVERAL DEZELFDE (sessie 25) ------------------------
     Dit stond alleen op `.panel-body`, en dat viel niet op zolang dat het enige
     scrollende venster was. Met instellingen rechtsboven, een Details-paneel, de
     bevingenlijst en de meldingen erbij hadden vier vensters ineens de systeembalk
     — breed, lichtgrijs, en op macOS met een eigen achtergrond die door het glas
     heen sneed.

     Twee notaties naast elkaar, en dat is geen dubbelop: `scrollbar-width`/
     `scrollbar-color` is de standaard (Firefox, en Chrome sinds 121), de
     `::-webkit-`-regels dekken Safari en oudere Chromium. Wie hier een vijfde
     scrollend venster bij zet, hoort in deze lijst. */
  .panel-body,
  .settings-body,
  .nav-body,
  .qlist-body,
  .notif-list,
  .readout,
  .sk-inspector {
    scrollbar-width: thin;
    scrollbar-color: var(--hair) transparent;
  }
  .panel-body::-webkit-scrollbar,
  .settings-body::-webkit-scrollbar,
  .nav-body::-webkit-scrollbar,
  .qlist-body::-webkit-scrollbar,
  .notif-list::-webkit-scrollbar,
  .readout::-webkit-scrollbar,
  .sk-inspector::-webkit-scrollbar { width: 6px; }
  .panel-body::-webkit-scrollbar-track,
  .settings-body::-webkit-scrollbar-track,
  .nav-body::-webkit-scrollbar-track,
  .qlist-body::-webkit-scrollbar-track,
  .notif-list::-webkit-scrollbar-track,
  .readout::-webkit-scrollbar-track,
  .sk-inspector::-webkit-scrollbar-track { background: transparent; }
  .panel-body::-webkit-scrollbar-thumb,
  .settings-body::-webkit-scrollbar-thumb,
  .nav-body::-webkit-scrollbar-thumb,
  .qlist-body::-webkit-scrollbar-thumb,
  .notif-list::-webkit-scrollbar-thumb,
  .readout::-webkit-scrollbar-thumb,
  .sk-inspector::-webkit-scrollbar-thumb { background: var(--hair); border-radius: 3px; }
  .panel-body::-webkit-scrollbar-thumb:hover,
  .settings-body::-webkit-scrollbar-thumb:hover,
  .nav-body::-webkit-scrollbar-thumb:hover,
  .qlist-body::-webkit-scrollbar-thumb:hover,
  .notif-list::-webkit-scrollbar-thumb:hover,
  .readout::-webkit-scrollbar-thumb:hover,
  .sk-inspector::-webkit-scrollbar-thumb:hover { background: var(--ink-faint); }
  .panel h2 {
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.22em;
    text-transform: uppercase;
    color: var(--ink-dim);
    margin-bottom: 14px;
  }

  /* ---- Inklapbaar paneel ---- */
  /* Zit sinds sessie 9 in de vaste kop als flex-item, niet meer absoluut gepositioneerd
     over de tabbalk heen. Hoogte gelijk aan de tabbalk zodat ze uitlijnen. */
  /* HET KRUISJE STAAT ALTIJD RECHTS IN DE KOP (sessie 34, Terry), ongeacht wat
     ervoor staat. De kop draagt een titel, een tabrij en dan deze knop, en die
     eerste twee groeien met hun inhoud: bij een korte titel en weinig tabs
     kroop het kruisje mee naar het midden. `margin-left: auto` en niet
     `justify-content: space-between` op de kop, want dat laatste zou ook de
     titel en de tabrij uit elkaar duwen. */
  .panel-head .panel-collapse { margin-left: auto; }

  .panel-collapse {
    flex: 0 0 auto;
    width: 30px; height: 30px;
    display: flex; align-items: center; justify-content: center;
    background: rgba(255,255,255,0.04);
    border: 1px solid var(--hair);
    border-radius: 7px;
    color: var(--ink-dim);
    font-size: 16px; line-height: 1;
    cursor: pointer;
    transition: color 0.15s, background 0.15s;
    /* Het raakvlak is groter dan de knop, zie ::after hieronder. */
    position: relative;
  }
  /* HET TREFFERVLAK IS 44x44, DE KNOP BLIJFT 30x30 (sessie 27, Terry's melding dat
     het kruisje op een telefoon niet reageerde). Gemeten op 375x812: dit was met 30 px
     het kleinste doel in het paneel, terwijl de knoppenrij die bij hem wél werkt
     43x39 is en een laagrij 321x30. Apple houdt 44 aan, Google 48; een duim die 8 px
     naast het kruisje landt raakt `.panel-head`, en daar hangt niets aan.
     Via een pseudo-element en niet via padding of een grotere knop: dan verandert er
     niets aan de opmaak van de kop, die op vier plekken op deze maat is uitgelijnd.
     -8 EN NIET -7: het bevattende blok van een absoluut kind is de PADDINGbox, en die
     is 28 px omdat de knop een rand van 1 px heeft. Met -7 werd het 42 en niet 44 —
     gemeten, want zelf uitgerekend had ik het mis. */
  .panel-collapse::after { content: ''; position: absolute; inset: -8px; }
  /* ALLEEN WAAR ER ECHT EEN AANWIJZER IS. Op een aanraakscherm laat iOS de eerste tik
     soms opgaan aan het activeren van de hover-stand in plaats van aan de klik — de
     tweede tik doet het dan pas. Dat is de andere verklaring voor dezelfde klacht. */
  @media (hover: hover) {
    .panel-collapse:hover { color: var(--ink); background: rgba(255,255,255,0.08); }
  }
  /* ---- De knoppenrij (sessie 24) ----------------------------------------
     Was één knop `.panel-open` die op `display: none` ging zodra het paneel
     openging. Met twee panelen kan dat niet meer: een rij die verdwijnt zodra
     je hem gebruikt, laat je niet naar het andere paneel. De rij blijft dus
     staan en de knop van het open paneel krijgt `.active`.

     GEEN `transform` VOOR DE PLAATSING. Die zit hier op `left`/`bottom`, en
     dat is bewust: de media queries verzetten die twee, en een transform
     eronder is de val die bij `.mode-switch` en `#toasts` al twee keer heeft
     toegeslagen — daar ís `translateX(-50%)` de centrering, dus het opheffen
     van `left` schuift het element een halve schermbreedte weg. */
  .dock {
    position: fixed;
    left: 28px;
    bottom: var(--dock-bottom);
    z-index: var(--z-dock);
    display: flex;
    align-items: center; gap: 8px;
  }
  /* De tweede hoek (sessie 26): Back to Earth en Navigate. `left: auto` is niet
     overbodig — zonder dat blijft de 28px van hierboven staan en wordt het blok
     schermbreed, waarna `right` niets meer doet. */
  .dock-right { left: auto; right: 28px; }
  .dock-btn {
    display: flex;
    align-items: center; gap: 8px;
    background: rgba(11,16,24,0.82);
    backdrop-filter: blur(14px);
    border: 1px solid var(--hair);
    border-radius: 12px;
    padding: 11px 13px;
    color: var(--ink);
    font-family: inherit; font-size: 12px; font-weight: 500;
    letter-spacing: 0.02em;
    cursor: pointer;
    transition: border-color 0.18s, background 0.18s, color 0.18s;
  }
  .dock-btn svg { width: 15px; height: 15px; flex: 0 0 auto; }
  .dock-btn:hover { border-color: var(--ink-dim); }
  .dock-btn.active { border-color: var(--accent); color: var(--accent); }
  /* Het label verschijnt op de knop van het OPEN paneel — zelfde afspraak als
     bij `.tab-label` in de paneelkop, en om dezelfde reden: drie labels naast
     elkaar passen niet op een telefoon, één wel. */
  .dock-label { display: none; }
  .dock-btn.active .dock-label { display: inline; }
  /* Back to Earth is geen schakelaar maar een handeling, en hij staat er alleen
     wanneer er een state actief is. Zijn label blijft dus altijd staan — een
     naamloos pijltje is geen uitweg die je durft te nemen. */
  .dock-back .dock-label { display: inline; }
  .dock-back { border-color: rgba(255,255,255,0.22); }
  .dock-back:hover { border-color: var(--ink); }
  /* `hidden` heeft hier een eigen regel nodig: `.dock-btn` zet een expliciete
     `display: flex` en die verslaat het attribuut. Zelfde val als bij
     `.time-reset` en `.ti-panel` — zie de noot daar. */
  .dock-btn[hidden] { display: none; }

  /* ---- Het Details-paneel (sessie 25) ------------------------------------
     Dezelfde plek, breedte en glastaal als `.panel` — het is het vierde paneel
     in dezelfde hoek en het hoort er niet anders uit te zien. Het erft de kop,
     de tabs en de accordeon-stijlen doordat het `.panel-head`, `.tabs`, `.tab`,
     `.tabpanel` en `.panel-body` gebruikt.

     LET OP: de tab-CSS was op `.panel .tab` gescopet. Die selectors zijn hier
     uitgebreid met `.detail-panel`, want dit paneel heeft er ook drie. Wie er
     een derde paneel met tabs bij zet, moet ze weer meenemen — of de scoping
     helemaal laten vallen. */
  .detail-panel {
    position: fixed;
    left: 28px;
    bottom: var(--panel-lift);
    z-index: var(--z-panel);
    width: 280px;
    max-height: calc(100vh - var(--panel-lift) - var(--panel-top-gap));
    max-height: calc(100dvh - var(--panel-lift) - var(--panel-top-gap));
    display: flex;
    flex-direction: column;
    overflow: hidden;
    background: rgba(11,16,24,0.72);
    backdrop-filter: blur(14px);
    border: 1px solid var(--hair);
    border-radius: 14px;
  }
  /* Eigen regel omdat de klasse hierboven `display: flex` zet — zelfde val als
     bij .dock-btn, .ti-panel en .nav-panel. */
  .detail-panel[hidden] { display: none; }

  /* ---- Het Navigate-paneel ----------------------------------------------
     Boven de knoppenrij, in dezelfde glastaal als `.panel`. Smaller, want het
     draagt een handvol rijen en geen accordeons.

     RECHTSONDER SINDS SESSIE 26, boven zijn eigen knop — en daarmee in dezelfde
     hoek als `.readout`. Die twee kunnen er niet allebei staan: `--z-panel` (10)
     ligt onder `--z-detail` (40), dus dit paneel zou onder het venster verdwijnen.
     Het WIJKT daarom, en komt terug zodra het venster sluit. Die afspraak staat in
     `index.html` bij navYield()/navRestore(), want het is een gerichte relatie —
     het venster laat dit paneel wijken, niet andersom — en dat past niet in de
     groepen van `js/ui/panels.js`, die per definitie wederzijds zijn. */
  .nav-panel {
    position: fixed;
    right: 28px;
    bottom: var(--panel-lift);   /* zelfde onderrand als .panel — zie :root */
    z-index: var(--z-panel);
    width: 214px;
    max-height: calc(100dvh - var(--panel-lift) - var(--panel-top-gap));
    display: flex;
    flex-direction: column;
    overflow: hidden;
    background: rgba(11,16,24,0.72);
    backdrop-filter: blur(14px);
    border: 1px solid var(--hair);
    border-radius: 14px;
  }
  .nav-panel[hidden] { display: none; }
  .nav-head {
    flex: 0 0 auto;
    display: flex; align-items: center; justify-content: space-between;
    padding: 12px 12px 10px 14px;
    background: rgba(11,16,24,0.94);
    border-bottom: 1px solid var(--hair);
    font-size: 11px; font-weight: 600;
    letter-spacing: 0.08em; text-transform: uppercase;
    color: var(--ink-dim);
  }
  .nav-close {
    background: none; border: 0; padding: 2px 6px;
    border-radius: 7px;
    color: var(--ink-dim);
    font-size: 16px; line-height: 1;
    cursor: pointer;
    transition: color 0.15s, background 0.15s;
  }
  .nav-close:hover { color: var(--ink); background: rgba(255,255,255,0.08); }
  .nav-body { flex: 1 1 auto; overflow-y: auto; padding: 8px; }
  .nav-state {
    display: flex; align-items: center; gap: 10px;
    width: 100%;
    background: none; border: 1px solid transparent;
    border-radius: 10px;
    padding: 9px 10px;
    color: var(--ink-dim);
    font-family: inherit; font-size: 12.5px; font-weight: 500;
    text-align: left;
    cursor: pointer;
    transition: color 0.15s, background 0.15s, border-color 0.15s;
  }
  .nav-state:hover { color: var(--ink); background: rgba(255,255,255,0.06); }
  .nav-state.active { color: var(--accent); border-color: var(--accent); }
  .nav-icon { display: flex; flex: 0 0 auto; }
  .nav-icon svg { width: 16px; height: 16px; }
  /* HET BIJSCHRIFT BIJ DE STAND WAAR JE IN ZIT, en sinds sessie 31 niet meer bij
     een rij knoppen eronder — die staan nu in het chroom bovenaan. De tekst blijft
     hier omdat hij de STATE beschrijft ("Locked to the GSM frame") en niet de
     knop, en omdat een tooltip op een telefoon geen weg is om hem te lezen. */
  .nav-note {
    margin: 0; padding: 2px 2px 6px;
    font-size: 10.5px; line-height: 1.45;
    color: var(--ink-faint);
  }
  .layer {
    display: flex;
    align-items: center;
    gap: 11px;
    padding: 7px 0;
    cursor: pointer;
    user-select: none;
    transition: opacity 0.18s;
  }
  .layer.off { opacity: 0.32; }

  /* ---- Vergrendelde lagen (2026-07-31) --------------------------------------
     Een laag die een eigen achterkant nodig heeft. Bewust nét iets minder gedimd
     dan een uitgezette laag: dit is geen storing en ook geen keuze die de bezoeker
     heeft gemaakt, maar een uitnodiging. De rij is niet aan te klikken; het slotje
     ernaast wel, en dat klapt de uitleg open. */
  .layer.locked { cursor: default; opacity: 0.5; }
  .layer.locked:hover { opacity: 0.62; }
  .layer.locked .count { display: none; }
  .lock-btn {
    display: inline-flex; align-items: center; justify-content: center;
    width: 20px; height: 20px; padding: 0; margin: -4px 0;
    background: transparent; border: none; cursor: pointer;
    color: var(--ink-faint);
    flex-shrink: 0;
  }
  .lock-btn svg { width: 13px; height: 13px; }
  .lock-btn:hover, .lock-btn.open { color: var(--accent); }
  .lock-detail p {
    font-size: 11px; line-height: 1.55; color: var(--ink-dim); margin: 0 0 9px;
  }
  .lock-detail a {
    font-size: 11px; color: var(--accent); text-decoration: none;
  }
  .lock-detail a:hover { text-decoration: underline; }
  .layer .swatch {
    width: 9px; height: 9px;
    border-radius: 50%;
    flex-shrink: 0;
    box-shadow: 0 0 8px currentColor;
  }
  .layer .name { font-size: 13px; font-weight: 500; flex: 1; }
  .layer .count {
    font-size: 11px;
    color: var(--ink-dim);
    font-variant-numeric: tabular-nums;
  }
  .sw-quake { color: var(--quake); background: var(--quake); }
  .sw-volcano { color: var(--volcano); background: var(--volcano); }
  .sw-storm { color: var(--storm); background: var(--storm); }
  .sw-ice { color: var(--ice); background: var(--ice); }
  .sw-wildfire { color: var(--wildfire); background: var(--wildfire); }
  .sw-aqi { color: var(--aqi); background: var(--aqi); }
  .sw-lightning { color: var(--lightning); background: var(--lightning); }
  .sw-aurora { color: var(--aurora); background: var(--aurora); }

  /* ---- De lagenlijst als legenda (2026-07-31) -------------------------------
     Een nieuwe bezoeker ziet gekleurde vormen op een bol en weet niet wat ze
     betekenen. De kleur stond al in het paneel, de vórm niet. Daarom neemt elk
     bolletje nu de vorm aan van de indicator in de actieve weergave. Geen losse
     legenda-component: het paneel ís de legenda (zie beslissing 22).

     Met `clip-path` en niet met SVG, zodat er geen markup bijkomt en de kleur
     gewoon uit de bestaande `background` blijft komen. Iets grotere maat, want een
     driehoek of zeshoek van 9 px leest niet.

     REALISTISCH: alleen de lagen die daar écht een vorm hebben wijken af van de
     stip. Aardbevingen, luchtkwaliteit, bliksem en bosbranden zijn er gloed, en
     een rond bolletje is daar dus de eerlijke weergave. */
  .layer .swatch { width: 11px; height: 11px; transition: clip-path 0.15s ease; }

  /* Let op de `.layer` ervoor: zonder dat wint `.layer .swatch` met zijn twee
     klassen van een losse `.sw-*`, blijft border-radius op 50% staan en worden de
     punten van de driehoek uit een cirkel geknipt. */
  /* vulkaan: piramide-piek van opzij, en in deskundig het geologische ▲ */
  .layer .sw-volcano { border-radius: 0; clip-path: polygon(50% 2%, 97% 96%, 3% 96%); }
  /* zee-ijs: zeshoekige kristalplaat, in beide weergaven ⬡ */
  .layer .sw-ice { border-radius: 0; clip-path: polygon(25% 3%, 75% 3%, 100% 50%, 75% 97%, 25% 97%, 0 50%); }
  /* storm: het oog van de cycloon; de spiraalarmen zelf zijn op 11 px niet leesbaar */
  .layer .sw-storm {
    background: transparent;
    box-shadow: 0 0 8px currentColor, inset 0 0 0 3px currentColor;
  }
  /* aurora: de ovaal zelf. Een RING, want dat is de vorm op de bol — en platgedrukt,
     zodat hij niet te verwarren is met het ronde oog van de storm hierboven. */
  .layer .sw-aurora {
    background: transparent;
    border-radius: 50% / 34%;
    box-shadow: 0 0 7px currentColor, inset 0 0 0 2.5px currentColor;
  }

  /* DESKUNDIG: de vakgebied-symbolen uit js/symbols-expert.js */
  body.mode-expert .layer .sw-quake {      /* ring: de magnitude-footprint */
    background: transparent;
    box-shadow: 0 0 8px currentColor, inset 0 0 0 2.5px currentColor;
  }
  body.mode-expert .layer .sw-aqi {        /* ■ EPA-bandkleur */
    border-radius: 1px;
  }
  body.mode-expert .layer .sw-wildfire {   /* ◆ ruit */
    border-radius: 0;
    clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
  }
  body.mode-expert .layer .sw-lightning {  /* ϟ schicht */
    border-radius: 0;
    clip-path: polygon(46% 0, 100% 0, 60% 40%, 96% 40%, 22% 100%, 44% 52%, 4% 52%);
  }
  .layer .aqi-refresh {
    display: inline-flex; align-items: center; justify-content: center;
    width: 20px; height: 20px; padding: 0; margin: -4px 0;
    background: transparent; border: none; cursor: pointer;
    color: var(--ink-dim); border-radius: 5px; flex-shrink: 0;
    transition: color 0.15s, background 0.15s;
  }
  .layer .aqi-refresh:hover { color: var(--aqi); background: rgba(167,139,250,0.14); }
  .layer .aqi-refresh svg { width: 13px; height: 13px; display: block; }
  .layer .aqi-refresh.spin svg { animation: aqi-spin 0.8s linear infinite; }
  @keyframes aqi-spin { to { transform: rotate(360deg); } }

  /* ---- Tab-navigatie ---- */
  /* De titel in de paneelkop. Staat er alleen wanneer de tabrij verborgen is, en
     dat gebeurt op twee manieren: op een smal scherm (de media query onderaan) en
     buiten de aarde (het blok hieronder). */
  .panel-title {
    display: none;
    /* `flex: 1` duwt de sluitknop naar rechts: de kop is een flexrij zonder
       `space-between`, dus zonder dit staat het kruisje pal naast de titel. */
    flex: 1;
    font-size: 11px; font-weight: 600;
    letter-spacing: 0.08em; text-transform: uppercase;
    color: var(--ink-dim);
  }
  /* BUITEN DE AARDE BLIJFT ER ÉÉN TAB OVER, EN EEN TABRIJ MET ÉÉN TAB IS RUIS
     (sessie 36, Terry).

     De tekentab hoort bij de aarde als kaart en verdwijnt in de drie andere
     weergaven; wat overblijft is Layers, en die vult het paneel toch al helemaal.
     Precies dezelfde keuze als op een smal scherm, waar de media query onderaan
     de rij om dezelfde reden weghaalt en de titel ervoor in de plaats zet.

     OP ZIJN ID en niet op `.panel .tabs`: een selector die de plek noemt breekt
     stil bij een verhuizing, en dat is in sessie 25 al een keer gebeurd. De
     magnetosfeer toetst verderop al op dit id — die regel staat daar en niet hier,
     tussen de rest van wat die state opruimt.

     DE TITEL STOND ER TOT NU TOE NIET BIJ. In de magnetosfeer ging de tabrij al
     weg zonder dat de titel verscheen, dus was de paneelkop leeg op het kruisje
     na — terwijl de regel hierboven belooft dat hij er dan staat. Die belofte
     wordt hier waargemaakt, voor alle drie de weergaven tegelijk. */
  /* IN SPACE VERDWIJNT HET PANEEL HELEMAAL (sessie 38, Terry), inclusief de
     dockknop waarmee je het weer zou openen. Daar valt niets te bedienen: de
     aardbol zelf is verborgen.

     SUN STOND HIER TOT SESSIE 50 BIJ. Die state had toen niets te bedienen; nu
     draagt hij de bronnen, de voorinstellingen en de gebiedenschakelaar, en zijn
     paneel staat gesnoeid onder `body.solar-on` verderop in dit bestand.

     ALLEEN VERBERGEN EN NIETS ONTHOUDEN. `panelOpenWanted` blijft staan wat het
     was, dus een paneel dat open stond komt bij terugkeer vanzelf terug en een
     paneel dat dicht stond blijft dicht. Dat scheelt de bewaar-en-teruggeef-lus
     die stateVerbergHaken voor de lagen wel nodig heeft — daar verandert de state
     de waarde, hier alleen de zichtbaarheid.

     `#panel-open` OP ZIJN ID: `.dock-btn[hidden]` zet elders een expliciete
     display, en een klasseselector zou daarvan verliezen. */
  body.space-on .panel,
  body.space-on #panel-open { display: none; }
  /* Wie het paneel houdt, houdt de tabrij niet — en heeft dus een titel nodig waar
     die stond. De zon heeft zijn eigen regel bij het solar-blok verderop; hier
     staat alleen de magnetosfeer, want Space houdt het paneel niet. */
  body.msphere-on .panel .panel-title { display: block; }
  .panel .tabs, .detail-panel .tabs {
    flex: 1 1 auto; min-width: 0;
    display: flex; gap: 4px; margin: 0; padding: 3px;
    background: rgba(255,255,255,0.04); border: 1px solid var(--hair); border-radius: 9px;
  }
  /* Vier icoon-tabs. Inactief zijn ze allemaal even breed en tonen ze alleen hun
     icoon; de actieve groeit en schuift zijn label erbij. `flex: 0 0 auto` op de
     inactieve houdt ze op een vaste maat, zodat het label van de actieve de
     resterende ruimte krijgt in plaats van alle vier iets te verbreden. */
  .panel .tab, .detail-panel .tab {
    flex: 0 0 auto;
    display: flex; align-items: center; justify-content: center; gap: 6px;
    padding: 6px 8px; font-family: inherit; font-size: 11px; font-weight: 600;
    letter-spacing: 0.04em; color: var(--ink-dim); background: transparent; border: none;
    border-radius: 6px; cursor: pointer; overflow: hidden;
    /* Zonder dit selecteert een dubbelklik op een tab het label, wat een blauwe
       vlek achterlaat in de kop. Tabs zijn knoppen, geen tekst om te kopiëren. */
    -webkit-user-select: none; user-select: none;
    transition: color 0.15s, background 0.15s, flex-grow 0.18s;
  }
  .panel .tab svg, .detail-panel .tab svg { width: 15px; height: 15px; flex-shrink: 0; display: block; }
  .panel .tab:hover, .detail-panel .tab:hover { color: var(--ink); }
  .panel .tab.active, .detail-panel .tab.active {
    flex: 1 1 auto; color: var(--ink); background: rgba(255,255,255,0.10);
  }
  /* Icoon en label wisselen elkaar af: inactief alleen het icoon, actief alleen de
     tekst. Beide met `display: none` en niet met `opacity`, want een verborgen
     element dat ruimte blijft opeisen zou de rij laten overlopen. */
  .panel .tab .tab-label, .detail-panel .tab .tab-label { display: none; white-space: nowrap; }
  .panel .tab.active .tab-label, .detail-panel .tab.active .tab-label { display: inline; }
  .panel .tab.active svg, .detail-panel .tab.active svg { display: none; }
  @media (prefers-reduced-motion: reduce) { .panel .tab, .detail-panel .tab { transition: none; } }
  .tabpanel { display: none; }
  .tabpanel.show { display: block; }

  /* ---- Uitklapbare laag-filters ---- */
  .layer .row-expand {
    display: inline-flex; align-items: center; justify-content: center;
    width: 20px; height: 20px; padding: 0; margin: -4px 0;
    background: transparent; border: none; cursor: pointer; color: var(--ink-faint);
    border-radius: 5px; flex-shrink: 0; transition: color 0.15s, transform 0.2s;
  }
  .layer .row-expand:hover { color: var(--ink); }
  .layer .row-expand.open { transform: rotate(180deg); color: var(--ink-dim); }
  .layer .row-expand svg { width: 13px; height: 13px; display: block; }
  .layer-detail { display: none; margin: 2px 0 10px; padding: 11px 12px;
    background: rgba(255,255,255,0.03); border: 1px solid var(--hair); border-radius: 8px; }
  .layer-detail.show { display: block; }

  /* Bronkeuze achter een uitklap. De ruime marges zijn er met opzet: dit blok zit
     tussen de laagnaam en de magnitudefilters in, en zonder lucht leest het als
     een derde filter in plaats van als een instelling die je zelden aanraakt. */
  .srcs-block { margin: 12px 0 18px; }
  .srcs-toggle {
    display: flex; align-items: center; gap: 7px; width: 100%;
    padding: 7px 2px;
    background: none; border: 0; cursor: pointer;
    font-family: inherit; font-size: 11px; color: var(--ink-dim);
    -webkit-user-select: none; user-select: none;
  }
  .srcs-toggle:hover { color: var(--ink); }
  .srcs-chev {
    width: 12px; height: 12px; flex-shrink: 0;
    transform: rotate(-90deg);            /* dicht: chevron wijst naar rechts */
    transition: transform 0.18s;
  }
  .srcs-toggle.open .srcs-chev { transform: rotate(0deg); }
  /* De actieve bron rechts in de kop, zodat je hem ziet zonder uit te klappen. */
  .srcs-current {
    margin-left: auto;
    font-size: 10.5px; font-weight: 600; letter-spacing: 0.06em;
    color: var(--accent);
  }
  .srcs-panel { display: none; padding-top: 4px; }
  .srcs-panel.show { display: block; }
  .srcs-note {
    margin-top: 10px;
    font-size: 11px; line-height: 1.55; color: var(--ink-faint);
  }
  @media (prefers-reduced-motion: reduce) { .srcs-chev { transition: none; } }
  /* Subkop binnen een tabpanel of een uitgeklapt filterblok. De Cosmos-tab gebruikt
     hem buiten `.layer-detail`, vandaar de tweede selector — zonder die regel viel
     hij terug op de browser-default van 18,72px en schreeuwde "Track window" over
     de rest van het paneel heen. */
  .layer-detail h3,
  .tabpanel > h3,
  .acc-body h3 {
    font-size: 9px; font-weight: 600; letter-spacing: 0.18em; text-transform: uppercase;
    color: var(--ink-dim); margin: 0 0 8px;
  }
  .layer-detail h3:not(:first-child),
  .tabpanel > h3:not(:first-child),
  .acc-body h3:not(:first-child) { margin-top: 12px; }
  /* De zone naast een kop. Geen hoofdletter-spatiëring van de kop overnemen: dit is
     een waarde en geen label, en "U T C + 1" leest niet. */
  .h3-zone { margin-left: 6px; letter-spacing: 0; text-transform: none;
    color: var(--ink-faint); font-weight: 500; }
  .aarde-empty { display: none; font-size: 11px; color: var(--ink-faint); line-height: 1.55; }
  .aarde-empty b { color: var(--ink-dim); }

  /* ---- Verbindingsstatus ---- */
  .hrow { display: flex; align-items: center; gap: 9px; padding: 7px 0; }
  .hdot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
    background: var(--ink-faint); color: var(--ink-faint); box-shadow: 0 0 7px currentColor; }
  .hdot.live { background: #4ade80; color: #4ade80; animation: pulse 2s infinite; }
  .hdot.offline { background: #f87171; color: #f87171; }
  .hdot.warn { background: #fbbf24; color: #fbbf24; animation: pulse 2s infinite; }
  .hdot.pending { animation: pulse 1.1s infinite; }
  .hmeta { flex: 1; min-width: 0; }
  .hmeta b { font-size: 12px; font-weight: 500; display: block; }
  .hmeta span { font-size: 10px; color: var(--ink-dim); display: block;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .hstat { font-size: 10px; text-transform: uppercase; letter-spacing: 0.05em; color: var(--ink-faint); flex-shrink: 0; }
  .hstat.live { color: #4ade80; }
  .hstat.offline { color: #f87171; }
  .hstat.warn { color: #fbbf24; }
  .health-test {
    margin-top: 12px; width: 100%; padding: 8px; font-family: inherit; font-size: 11.5px; font-weight: 600;
    letter-spacing: 0.02em; color: var(--ink); background: rgba(255,255,255,0.05);
    border: 1px solid var(--hair); border-radius: 7px; cursor: pointer;
    transition: background 0.15s, border-color 0.15s;
  }
  .health-test:hover { background: rgba(255,255,255,0.10); border-color: var(--ink-dim); }

  /* ---- Credits (Instellingen) ----
     Ingeklapt via native <details>: geen JS nodig en toetsenbord-toegankelijk
     uit zichzelf. Bewust klein en gedempt; het hoort er te zijn, niet op te
     vallen. Volledige verantwoording staat in ATTRIBUTION.md. */
  /* De bronvermelding. Was tot sessie 25 een `<details>` met een eigen `<summary>`,
     een eigen chevron en een eigen bovenrand — een tweede uitklapmechaniek náást de
     accordeons, binnen de body van Connections. Nu een gewone `.acc` als alle andere,
     en dit blok houdt alleen nog de opmaak van de LIJST over. Weg zijn: de
     `summary`-regels, de losse chevron via `::after`, en de eigen `margin-top`/
     `border-top` — die deden het werk van `.acc` nog eens dunnetjes over en waren de
     witruimte die eronder achterbleef. */
  .credits dl { font-size: 10px; line-height: 1.5; }
  .credits dt {
    font-size: 9px; letter-spacing: 0.08em; text-transform: uppercase;
    color: rgba(125,138,160,0.7); margin-top: 10px;
  }
  .credits dt:first-child { margin-top: 0; }
  .credits dd { color: var(--ink-dim); margin-top: 2px; }
  .credits a { color: var(--ink); text-decoration: none; border-bottom: 1px solid var(--hair); }
  .credits a:hover { color: #5fcdfb; border-bottom-color: #5fcdfb; }

  /* ---- Accordion ----
     Generiek opgezet zodat elke tab hem kan gebruiken, niet alleen Cosmos. De
     body wordt met grid-template-rows geanimeerd in plaats van met max-height:
     dat laatste vraagt een gokwaarde die bij een langere sectie afkapt of bij een
     korte een trage naloop geeft. `1fr` is exact de inhoudshoogte. */
  .acc { border-top: 1px solid var(--hair); }
  .acc:first-child { border-top: none; }
  .acc-head {
    display: flex; align-items: center; gap: 8px; width: 100%;
    background: transparent; border: none; cursor: pointer;
    padding: 12px 0 11px; color: var(--ink-dim); font: inherit;
    font-size: 10px; font-weight: 600; letter-spacing: 0.16em; text-transform: uppercase;
    text-align: left;
  }
  .acc-head:hover { color: var(--ink); }
  .acc.open > .acc-head { color: var(--ink); }
  .acc-head > span:first-child { flex-shrink: 0; }
  .acc-sub {
    margin-left: auto; font-size: 9px; font-weight: 400; letter-spacing: 0.04em;
    text-transform: none; color: var(--ink-faint); font-variant-numeric: tabular-nums;
  }
  .acc-sub.shifted { color: #ffd166; }
  .acc-chev {
    width: 13px; height: 13px; flex-shrink: 0; margin-left: auto;
    transform: rotate(-90deg); transition: transform 0.18s ease;
  }
  .acc-sub + .acc-chev { margin-left: 0; }
  .acc.open > .acc-head .acc-chev { transform: rotate(0deg); }
  .acc-body {
    display: grid; grid-template-rows: 0fr;
    transition: grid-template-rows 0.22s ease;
  }
  .acc.open > .acc-body { grid-template-rows: 1fr; padding-bottom: 4px; }
  /* De wrapper (door JS toegevoegd) moet overflow verbergen, anders steekt de
     inhoud bij 0fr gewoon buiten de nulhoogte uit. */
  .acc-inner { overflow: hidden; min-height: 0; }
  @media (prefers-reduced-motion: reduce) {
    .acc-body, .acc-chev { transition: none; }
  }

  /* De i-knop bij Moment en de chevron-toelichting bij Twilight. */
  .acc-info {
    display: inline-flex; align-items: center; justify-content: center;
    width: 16px; height: 16px; flex-shrink: 0; margin-left: auto; border-radius: 50%;
    background: transparent; border: 1px solid var(--hair); color: var(--ink-faint);
    font: inherit; font-size: 9px; font-style: italic; font-weight: 400;
    letter-spacing: 0; text-transform: none; line-height: 1;
    cursor: pointer; padding: 0;
  }
  /* Staat de i-knop in de kop, dan neemt hij de `margin-left: auto` over en moet de
     chevron zijn eigen automarge loslaten — anders duwen ze elkaar uit elkaar. */
  .acc-info + .acc-chev { margin-left: 8px; }
  .acc-info:hover, .acc-info.on { color: var(--ink); border-color: var(--ink-faint); }
  /* Een `.elayer` gebruikt dezelfde uitklap-chevron als een `.layer`, zodat het
     gebaar overal hetzelfde is. De basisregels staan bij `.layer .row-expand`. */
  .elayer .row-expand {
    background: transparent; border: none; cursor: pointer; color: var(--ink-faint);
    padding: 0; margin-left: 6px; display: flex; align-items: center;
    transition: transform 0.18s ease, color 0.15s;
    transform: rotate(-90deg);
  }
  .elayer .row-expand:hover { color: var(--ink); }
  .elayer .row-expand.open { transform: rotate(0deg); color: var(--ink-dim); }
  .elayer .row-expand svg { width: 12px; height: 12px; display: block; }
  /* De toelichting hoort visueel bij de rij erboven, niet bij een filterblok, dus
     zonder de kaderstijl van de quake-filters. */
  .acc-body .layer-detail {
    background: transparent; border: none; padding: 2px 0 8px; margin: 0;
    font-size: 10px; line-height: 1.55; color: var(--ink-faint);
  }

  /* ---- De maat langs de assen (sessie 32, blok O) --------------------------
     `display` in een klasse verslaat het `hidden`-attribuut — voor de ZEVENDE
     keer in dit bestand — dus de zichtbaarheid gaat hier via body.msphere-on, net
     als bij .msphere-time. Een tweede schakelaar in JS zou een tweede waarheid
     zijn: de body-klasse weet toch al in welke state we zijn.

     ONDER HET TIJD-EILAND (8) EN BOVEN DE SCENE. Hij draagt geen bediening en
     vangt geen kliks, dus hij hoort onderaan de z-ladder — maar wel boven de
     bol, want hij legt uit wat je daar ziet. */
  #msphere-scale {
    display: none;
    position: fixed;
    inset: 0;
    width: 100vw;
    height: 100vh;
    z-index: 6;
    pointer-events: none;
  }
  body.msphere-on #msphere-scale { display: block; }

  /* ---- De GOES-toestellen (sessie 35) --------------------------------------
     Dezelfde doos en dezelfde plek in de z-ladder als de schaal hierboven, en
     om dezelfde redenen: hij draagt geen bediening, vangt geen kliks, en legt
     uit wat je in de scene ziet. Ook hier gaat de zichtbaarheid via
     body.msphere-on en niet via een vlag in JS — `display` in een klasse
     verslaat het `hidden`-attribuut, voor de ACHTSTE keer in dit bestand.

     WAT HIER NIET STAAT IS EEN TRANSITIE OP `opacity`. De module schrijft die
     inline tijdens een standwissel, en een CSS-overgang eroverheen zou de
     kruisvervaging van de state een tweede, eigen tempo geven. */
  #msphere-craft {
    display: none;
    position: fixed;
    inset: 0;
    width: 100vw;
    height: 100vh;
    z-index: 6;
    pointer-events: none;
  }
  body.msphere-on #msphere-craft { display: block; }

  /* ---- De legenda van de magnetosfeer (sessie 32) --------------------------
     Vier groepen van rijen: een swatch, een naam, en de reden eronder. De
     swatch is een LIJN en geen vlak, want alles wat hij uitlegt is een lijn —
     op één na, de aurora, en die is dan ook het enige verloop. */
  .msp-leg { margin-top: 4px; font-size: 11.5px; line-height: 1.45; }
  .msp-leg h5 {
    margin: 12px 0 5px;
    padding-top: 8px;
    border-top: 1px solid var(--hair);
    font-size: 10px;
    font-weight: 600;
    letter-spacing: 0.11em;
    text-transform: uppercase;
    color: var(--ink-faint);
  }
  .msp-leg h5:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
  .msp-leg-row { display: flex; align-items: flex-start; gap: 9px; padding: 3px 0; }
  /* De swatch krimpt niet mee en lijnt uit op de eerste tekstregel. `flex: none`
     is hier niet cosmetisch: zonder dat perst flexbox een streep van 26 px tot
     11 zodra de reden lang wordt, en dan verschilt de sleutel per rij. */
  .msp-sw { flex: 0 0 26px; width: 26px; margin-top: 7px; }
  .msp-sw.is-line { height: 0; border-top-width: 2px; border-top-style: solid; }
  .msp-sw.is-dash { height: 0; border-top-width: 1.5px; border-top-style: dashed; }
  .msp-sw.is-bar  { height: 10px; margin-top: 3px; border-radius: 2px; }
  .msp-sw.is-ramp { height: 10px; margin-top: 3px; border-radius: 2px; }
  /* De zekerheidsklasse is een STIP en geen streep, want hij markeert geen lijn
     op het scherm maar een rij in de uitlezing. Dezelfde vorm als in de POC. */
  .msp-sw.is-dot,
  .msp-sw.is-hollow,
  .msp-sw.is-ring {
    flex-basis: 26px;
    height: 8px; margin-top: 4px;
    display: block;
    position: relative;
  }
  .msp-sw.is-dot::before,
  .msp-sw.is-hollow::before,
  .msp-sw.is-ring::before {
    content: ''; position: absolute; left: 9px; top: 0;
    width: 8px; height: 8px; border-radius: 50%;
    background: currentColor;
  }
  /* DE TWEE VARIANTEN VAN DE GOES-MARKER (sessie 35). Een legenda die de
     KLEUR verklaart maar niet de VORM, laat het onderscheid onverklaard dat
     hier het meeste zegt: hol betekent "dit toestel ligt naast het tekenvlak",
     en de ring betekent "de eigen stuurmotor stond aan tijdens deze meting".
     Ze erven de doos van .is-dot; alleen de vulling verschilt. */
  .msp-sw.is-hollow::before {
    background: none;
    border: 1.5px solid currentColor;
  }
  .msp-sw.is-ring::after {
    content: ''; position: absolute; left: 6px; top: -3px;
    width: 14px; height: 14px; border-radius: 50%;
    border: 1px solid currentColor; opacity: 0.5;
  }
  .msp-leg-text { flex: 1 1 auto; min-width: 0; }

  /* ---- ELKE UITLEG ZIT ACHTER EEN KLIK (sessie 34, Terry) ----

     De legenda draagt 22 sleutels met elk een alinea eronder, en samen was dat
     een lap tekst waarin je de sleutel zelf niet meer vond — terwijl dát is wat
     je opzoekt terwijl je naar de scene kijkt. Dicht is de legenda een LIJST:
     swatch, naam, klaar. Wie meer wil, klikt.

     DE NAAM IS DE KNOP en er staat geen apart knopje naast. Een tweede
     doelwit naast een regel die zelf al de hele breedte heeft, is een kleiner
     doelwit voor hetzelfde. */
  .msp-leg-name {
    display: flex; align-items: baseline; gap: 6px; width: 100%;
    background: transparent; border: none; padding: 0; margin: 0;
    font: inherit; font-weight: 500; color: var(--ink);
    text-align: left; cursor: pointer;
  }
  .msp-leg-name > span { flex: 1 1 auto; min-width: 0; }
  .msp-chev {
    flex: 0 0 11px; width: 11px; height: 11px;
    align-self: center; color: var(--ink-faint);
    transition: transform 0.18s ease;
  }
  .msp-leg-row.open .msp-chev { transform: rotate(180deg); }
  .msp-leg-name:hover { color: var(--ink); }
  .msp-leg-name:hover .msp-chev { color: var(--ink-dim); }
  @media (prefers-reduced-motion: reduce) { .msp-chev { transition: none; } }

  /* DICHT IS DE STAND BIJ HET BOUWEN, en de klasse zet `display` — dat verslaat
     het `hidden`-attribuut, en dat is in dit bestand inmiddels een bekende. */
  .msp-why { display: none; color: var(--ink-dim); padding: 2px 0 5px; }
  .msp-leg-row.open .msp-why { display: block; }

  /* Een regel zonder swatch: hij gaat over een KEUZE en niet over inkt. De lege
     swatch blijft wel staan, zodat de namen van álle regels in één kolom
     uitlijnen — inspringen met padding zou een tweede kolom maken die bij de
     eerste lange naam uit de pas loopt. */
  .msp-sw.is-none { visibility: hidden; }
  .msp-leg-note { padding: 4px 0 4px 35px; color: var(--ink-faint); }

  /* ---- Cosmos-readout (zon en maan) ----
     Label boven waarde, net als de credits-lijst, zodat lange waarden als
     "387.509 km" niet met het label om de 248px paneelbreedte hoeven te vechten. */
  .cosmos-readout { margin-top: 4px; font-size: 11px; line-height: 1.5; }
  .cosmos-readout dt {
    font-size: 9px; letter-spacing: 0.08em; text-transform: uppercase;
    color: rgba(125,138,160,0.7); margin-top: 11px;
  }
  .cosmos-readout dt:first-child { margin-top: 0; }
  .cosmos-readout dd {
    color: var(--ink); margin-top: 2px;
    font-variant-numeric: tabular-nums;   /* cijfers springen niet bij het bijwerken */
  }
  .cosmos-note {
    margin-top: 16px; font-size: 10px; line-height: 1.55; color: var(--ink-faint);
  }
  /* Fase-indicator naast de faseregel. Canvas is 128px voor scherpte op retina,
     maar wordt op 34px getoond — zo blijft de readout-regel één regel hoog. */
  .cosmos-readout .phase-row { display: flex; align-items: center; gap: 9px; }
  .cosmos-readout .phase-row canvas {
    width: 34px; height: 34px; flex-shrink: 0; display: block;
  }
  /* Bovenaanzicht op de noordpool. Zelfde opzet als de fase-indicator, maar iets
     groter: er staan een raster, een spoor en twee polen in en op 34px valt het
     verschil tussen die twee polen weg — en dat verschil is precies het punt. */
  .cosmos-readout .polar-row { display: flex; align-items: center; gap: 10px; }
  .cosmos-readout .polar-row canvas {
    width: 52px; height: 52px; flex-shrink: 0; display: block;
  }
  .cosmos-readout .polar-row span { color: var(--ink-dim); }
  /* De polar-motion-plot. Anders dan de twee indicatoren hierboven staat deze op
     volle paneelbreedte en op een eigen regel: er zit een spoor van honderden
     punten in plus een schaalbalk, en op 52px is dat een vlekje. Het canvas is
     256px voor scherpte op retina en schaalt naar de beschikbare breedte. */
  .cosmos-readout .pm-row { margin-top: 4px; }
  .cosmos-readout .pm-row canvas {
    width: 100%; height: auto; display: block; border-radius: 6px;
  }
  /* Een readout-waarde die een voorbehoud draagt. Zelfde geel als #time-val.shifted
     en om dezelfde reden: het zegt "wat je ziet is niet het gemetene". Die regel zat
     aan een id vast en was dus niet herbruikbaar; dit is de gedeelde vorm. */
  /* ---- Space (sessie 22) ----
     Het patroon is "klasse op body, blok verbergen wat in die stand niets
     betekent". Dat hoort bij elke volgende state herkenbaar hetzelfde te
     blijven — dit blok is de vorm om na te volgen.

     LET OP bij uitbreiden: een expliciete `display` in een klasse verslaat het
     `hidden`-attribuut, want `hidden` is niet meer dan een user-agent-regel
     `display: none`. Krijgt een element hieronder ooit een eigen `display`,
     geef het dan meteen een `[hidden]`-regel mee — die val kostte in sessie 14
     een zichtbare knop die verborgen hoorde te zijn. */
  .space-only { display: none; }
  body.space-on .space-only { display: block; }
  .space-uitleg {
    margin: 10px 0 0; font-size: 11px; line-height: 1.5; color: var(--ink-faint);
  }
  #space-btn { margin-top: 8px; }
  body.space-on #space-btn { border-color: var(--quake); color: var(--ink); }
  .space-lock {
    margin: 6px 0 0; font-size: 11px; color: var(--ink-faint);
  }
  .planet-list { margin-top: 4px; }
  /* De stip draagt de kleur van de planeet en is tegelijk de aan/uit-knop.
     Iets groter dan de standaard `.edot`: hij is hier een doelwit en niet
     alleen een teken. */
  .planet-row .edot { width: 9px; height: 9px; cursor: pointer; }
  .planet-row.focus { color: var(--ink); }
  .planet-row.focus .estat { color: var(--ink); }
  .planet-row .estat { font-variant-numeric: tabular-nums; }
  .readout-warn { color: #ffd166; }
  .readout-note { display: block; color: var(--ink-faint); margin-top: 2px; }
  /* HIER STOND DE TIJDKIEZER (weggehaald in sessie 25). Twee blokken dood gewicht:
     `#time-val` en `#time-val.shifted` dateerden van toen de kiezer nog in Settings
     zat, en die ID-selector VERSLOEG `.ti-val.shifted` op specificiteit — het amber
     dat je ziet kwam dus uit deze regel en niet uit de regel die erover ging. Het
     amber is meeverhuisd naar `.ti-val.shifted`, waar de rest van het tijdeiland
     staat. `.time-reset` hoorde bij geen enkel element meer: de knop draagt sinds
     de verhuizing `.ti-reset`.
     Les die blijft gelden: styling op een ID is een tijdbom zodra het element
     verhuist, want de oude regel blijft stil winnen. */
  .time-input {
    display: block; width: 100%; box-sizing: border-box; margin-bottom: 10px;
    background: rgba(255,255,255,0.04); border: 1px solid var(--hair); border-radius: 6px;
    color: var(--ink); font: inherit; font-size: 11px; padding: 5px 8px;
    font-variant-numeric: tabular-nums;
  }
  .time-input:focus { outline: none; border-color: var(--ink-faint); }
  /* De native kalender-knop is standaard bijna zwart op een donkere achtergrond. */
  .time-input::-webkit-calendar-picker-indicator {
    filter: invert(0.7); cursor: pointer;
  }
  .play-row { display: flex; align-items: center; gap: 6px; margin-top: 10px; }
  .play-btn {
    background: transparent; border: 1px solid var(--hair); border-radius: 5px;
    color: var(--ink-dim); font: inherit; font-size: 10px; line-height: 1;
    padding: 5px 9px; cursor: pointer; transition: color 0.15s, border-color 0.15s;
  }
  .play-btn:hover:not(:disabled) { color: var(--ink); border-color: var(--ink-faint); }
  .play-btn:disabled { opacity: 0.35; cursor: default; }
  .play-btn.active { color: #ffd166; border-color: rgba(255,209,102,0.45); }
  .play-speed-val {
    margin-left: auto; font-size: 10px; color: var(--ink-faint);
    font-variant-numeric: tabular-nums;
  }
  .speed-slider { display: block; width: 100%; margin-top: 7px; }
  .eclipse-state {
    margin: 4px 0 9px; font-size: 10px; line-height: 1.55; color: var(--ink-faint);
  }
  .eclipse-state.active {
    padding: 7px 9px; border-radius: 6px; color: var(--ink-dim);
    background: rgba(255,209,102,0.08); border: 1px solid rgba(255,209,102,0.22);
  }
  .eclipse-state.active b { color: #ffd166; }
  .eclipse-nav { display: flex; gap: 6px; }
  .eclipse-nav .play-btn { flex: 1; text-align: center; }
  .cosmos-warn {
    margin-top: 9px; padding: 7px 9px; border-radius: 6px;
    background: rgba(255,209,102,0.08); border: 1px solid rgba(255,209,102,0.22);
    font-size: 10px; line-height: 1.5; color: #e4cd9a;
  }

  /* ---- Time filter ---- */
  .panel .divider {
    height: 1px;
    background: var(--hair);
    margin: 16px 0 14px;
  }
  .panel h2:not(:first-child) { margin-top: 0; }
  .presets {
    display: flex;
    gap: 6px;
    margin-bottom: 14px;
  }
  .preset {
    flex: 1;
    background: transparent;
    border: 1px solid var(--hair);
    border-radius: 8px;
    padding: 7px 0;
    color: var(--ink-dim);
    font-family: inherit;
    font-size: 12px;
    font-weight: 500;
    cursor: pointer;
    transition: all 0.15s;
  }
  /* De trackvenster-knoppen staan er in twee sets van vier, waarvan er één verborgen
     is (zie `data-frame` in index.html). `.preset` zet zelf geen `display`, dus het
     attribuut werkt hier — maar deze regel staat er zodat dat zo blijft. Vijfde keer
     in dit project dat die val telt: .ti-panel, .ti-reset, .dock-btn, .detail-plot. */
  .preset[hidden] { display: none; }
  .preset:hover { border-color: var(--ink-dim); color: var(--ink); }
  .preset.active {
    background: var(--accent);
    border-color: var(--accent);
    color: #1a0d06;
  }
  /* Een bovengrens onder de ondergrens is niet te kiezen in plaats van achteraf
     te corrigeren — dan hoeft er nooit uitgelegd te worden waarom je keuze
     verspringt. */
  .preset:disabled {
    opacity: 0.3;
    cursor: not-allowed;
    border-color: var(--hair);
    color: var(--ink-faint);
  }
  .preset:disabled:hover { border-color: var(--hair); color: var(--ink-faint); }

  /* ---- Aardbevingenlijst (sidebar rechts) ----
     Gespiegeld aan `.panel`: zelfde maten, zelfde glas, zelfde `dvh`-truc. Zie de
     lange noot daar waarom `100vh` op een telefoon niet deugt. */
  /* VAN BOVEN NAAR BENEDEN, met dezelfde bovenkant als het meldingenpaneel.
     Eerst hing deze kolom aan de ONDERrand en groeide hij omhoog; dan loopt hij bij
     een lange lijst de bel en de meldingen in, en moest de hoogte zo streng geknepen
     worden dat er op 1080p nog vijf regels over waren (gemeten). Vanaf de bovenkant
     ligt de bovengrens vast en mag de lijst gewoon lang zijn. Het meldingenpaneel
     deelt die bovenkant en tekent er met z-index 35 overheen wanneer je het opent —
     dat is een dropdown, geen botsing. */
  .qlist {
    position: fixed;
    right: 28px;
    top: 62px;
    z-index: var(--z-chrome);
    width: 268px;
    /* Tot ongeveer twee derde van de hoogte. Daaronder blijft het beeld vrij voor
       het detailvenster, dat rechtsonder opkomt. */
    max-height: 58vh;
    max-height: 58dvh;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    background: rgba(11,16,24,0.72);
    backdrop-filter: blur(14px);
    border: 1px solid var(--hair);
    border-radius: 14px;
  }
  .qlist-head {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 14px 12px 10px 16px;
    border-bottom: 1px solid var(--hair);
  }
  .qlist-title {
    font-size: 10px; font-weight: 600; letter-spacing: 0.18em;
    text-transform: uppercase; color: var(--ink-dim);
  }
  /* Direct naast de titel, NIET tegen de rechterrand: daar zit nu het kruisje, en
     een getal ernaast leest als "sluit 323" in plaats van als een telling. */
  .qlist-count {
    font-size: 12px; font-weight: 600; color: var(--ink);
    font-variant-numeric: tabular-nums;
  }
  .qlist-collapse {
    margin-left: auto;
    background: none; border: 0; padding: 0 2px;
    color: var(--ink-dim); font-family: inherit; font-size: 17px; line-height: 1;
    cursor: pointer;
  }
  .qlist-collapse:hover { color: var(--ink); }
  .qlist-summary {
    display: block; width: 100%;
    background: none; border: 0; border-bottom: 1px solid var(--hair);
    padding: 9px 16px;
    color: var(--ink-faint); font-family: inherit; font-size: 11px; text-align: left;
    cursor: pointer;
  }
  .qlist-summary:hover { color: var(--accent); }
  .qlist-sort { margin: 12px 16px 10px; }
  .qlist-body { overflow-y: auto; overscroll-behavior: contain; padding: 0 8px 4px; }
  .qrow {
    display: grid;
    grid-template-columns: 46px 1fr;
    gap: 2px 10px;
    align-items: baseline;
    width: 100%;
    background: none; border: 0; border-radius: 8px;
    padding: 8px;
    font-family: inherit; text-align: left; cursor: pointer;
    transition: background 0.15s;
  }
  .qrow:hover { background: rgba(255,255,255,0.05); }
  .qrow-mag {
    grid-row: span 2;
    font-size: 15px; font-weight: 700; letter-spacing: -0.02em;
    font-variant-numeric: tabular-nums;
  }
  .qrow-place {
    font-size: 12px; color: var(--ink);
    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
  }
  .qrow-meta { font-size: 10.5px; color: var(--ink-faint); }
  .qlist-more { margin: 6px 16px 14px; width: calc(100% - 32px); }
  .qlist-empty { padding: 26px 16px; text-align: center; font-size: 11px; color: var(--ink-faint); }
  /* Staat in de masthead, links van de notificatiebel. Zelfde maatvoering als
     .notif-btn, zodat het één rij knoppen blijft en niet twee losse dingen. */
  /* The masthead spaces its children by `--masthead-gap`; these two buttons
     belong tight together as one group. So the margin takes that gap back and
     leaves `--icon-gap` of it — derived, so a narrower gap on a phone cannot
     turn into an overlap. */
  .qlist-open {
    margin-left: calc(var(--icon-gap) - var(--masthead-gap));
    pointer-events: auto;
    display: flex; align-items: center;
    background: rgba(11,16,24,0.72);
    backdrop-filter: blur(14px);
    border: 1px solid var(--hair);
    border-radius: 9px;
    padding: 7px 9px;
    color: var(--ink-dim);
    cursor: pointer;
    transition: color 0.18s, border-color 0.18s;
  }
  .qlist-open:hover { color: var(--ink); border-color: var(--ink-dim); }
  .qlist-open svg { width: 15px; height: 15px; }
  /* De derde knop in dezelfde hoek (sessie 25): zelfde vorm, zelfde negatieve marge,
     zodat de drie als één groepje lezen tegen de gap van 18 px in de masthead. */
  .settings-open {
    margin-left: calc(var(--icon-gap) - var(--masthead-gap));
    pointer-events: auto;
    display: flex; align-items: center;
    background: rgba(11,16,24,0.72);
    backdrop-filter: blur(14px);
    border: 1px solid var(--hair);
    border-radius: 9px;
    padding: 7px 9px;
    color: var(--ink-dim);
    cursor: pointer;
    transition: color 0.18s, border-color 0.18s;
  }
  .settings-open:hover { color: var(--ink); border-color: var(--ink-dim); }
  .settings-open svg { width: 15px; height: 15px; }
  /* Actieve stand voor de drie schakelaars in de masthead. Ze zitten in dezelfde
     groep, dus er brandt er hoogstens één.
     `.active` en niet `.on` sinds sessie 25: `panels.sync()` is nu de enige
     schrijver van de knopstand, en die gebruikt dezelfde klasse als de dock-rij.
     Twee klassen voor hetzelfde is precies hoe een knop uit de pas gaat lopen
     zodra een ánder paneel uit de groep hem sluit. */
  .qlist-open.active, .notif-btn.active, .settings-open.active { color: var(--accent); border-color: var(--accent); }
  .notif-close {
    margin-left: 2px;
    background: none; border: 0; padding: 0 2px;
    color: var(--ink-dim); font-family: inherit; font-size: 15px; line-height: 1;
    text-transform: none; letter-spacing: 0; cursor: pointer;
  }
  .notif-close:hover { color: var(--ink); }

  /* `.sunstat-row`, `.sunstat h3` en `.sa-stale` stonden hier tot sessie 26. Ze
     hoorden bij de acht vaste regels zonneactiviteit in de Sun & Moon-tab, en die
     staan nu in het detailvenster — waar `.row` / `.k` / `.v` ze tekent en het
     voorbehoud een gewone rij is. `.sunstat-note` blijft: die wordt nog gebruikt
     bij de schaaluitleg in de Space-sectie. */
  .sunstat-note {
    margin-top: 10px;
    padding-top: 10px;
    border-top: 1px solid var(--hair);
    font-size: 11px;
    line-height: 1.5;
    color: var(--ink-faint);
  }

  /* ---- Bronkeuze (aardbevingen) ---- */
  .srcs { display: flex; flex-direction: column; gap: 6px; margin-bottom: 8px; }
  .src {
    display: grid;
    grid-template-columns: auto auto 1fr;
    align-items: center;
    gap: 8px;
    padding: 7px 10px;
    border: 1px solid var(--hair);
    border-radius: 8px;
    cursor: pointer;
    transition: border-color 0.15s;
  }
  .src:hover { border-color: var(--ink-dim); }
  .src input { accent-color: var(--accent); width: 14px; height: 14px; cursor: pointer; }
  .src span { font-size: 12px; font-weight: 600; color: var(--ink); }
  .src em { font-style: normal; font-size: 11px; color: var(--ink-faint); text-align: right; }
  /* Het tijdvak dat deze catalogus werkelijk dekt — GEMETEN, niet wat de
     documentatie belooft. Staat onder de omschrijving omdat het pas telt zodra je
     met de tijdkiezer terug gaat; bij `nu` is het achtergrondinformatie. */
  .src em b {
    display: block;
    font-weight: 500;
    color: var(--ink-dim);
    font-variant-numeric: tabular-nums;
  }
  /* De regel die verschijnt als de app zelf van bron wisselde. Zelfde amber taal als
     .cosmos-warn bij polar motion — en met opzet dezelfde waarden, niet een eigen
     tint: dit is geen fout, maar wel iets waarvan je moet weten dat het gebeurd is. */
  /* Het voorbehoud bij een historisch tijdvak: hoeveel deze catalogus in dat jaar wist
     te zien, tegen vandaag. Geen waarschuwing maar context — vandaar de gedempte tint
     en niet het amber van .src-switch. Gedeeld door de bevingen (#quake-era) en de
     bosbranden (#fire-era); heette tot sessie 21 .quake-era, wat een klassenaam was
     die aan één laag vastzat terwijl het patroon dat niet is. */
  .era-note {
    margin: 10px 0 0;
    padding: 7px 9px;
    border-radius: 6px;
    background: rgba(125,138,160,0.07);
    border: 1px solid var(--hair);
    font-size: 10px;
    line-height: 1.55;
    color: var(--ink-dim);
  }
  .era-note b { color: var(--ink); font-weight: 600; font-variant-numeric: tabular-nums; }
  .src-switch {
    margin: -6px 0 12px;
    padding: 7px 9px;
    border-radius: 6px;
    background: rgba(255,209,102,0.08);
    border: 1px solid rgba(255,209,102,0.22);
    font-size: 10px;
    line-height: 1.5;
    color: #e4cd9a;
  }
  .apply {
    width: 100%;
    background: transparent;
    border: 1px solid var(--hair);
    border-radius: 8px;
    padding: 7px 0;
    margin-bottom: 14px;
    color: var(--ink-dim);
    font-family: inherit;
    font-size: 12px;
    font-weight: 500;
    cursor: pointer;
    transition: all 0.15s;
  }
  .apply:not(:disabled):hover { border-color: var(--accent); color: var(--accent); }
  .apply:disabled { opacity: 0.35; cursor: default; }
  /* ---- Beeldkwaliteit (sectie Imagery & data) ----
     DRIE KNOPPEN IN PLAATS VAN TWEE sinds de satelliettrap, en ze komen op TWEE
     REGELS te staan: twee naast elkaar, de derde eronder over de volle breedte.

     Dat is gemeten en niet toevallig. Het instellingenpaneel geeft deze rij 266
     px; drie kolommen zouden er 85 per stuk worden, en daar past `10 m · 2-18
     MB/visit` niet op zonder te breken. Bovendien onderscheidt de volle breedte
     de satelliettrap van de twee erboven, en dat is precies wat hij is: geen
     derde stap op dezelfde schaal maar een andere soort keuze.

     De ondergrens van 92 px is wat twee naast elkaar houdt; wrap doet de rest. */
  .texq { display: flex; flex-wrap: wrap; gap: 6px; }
  .texq-opt {
    flex: 1 1 92px;
    display: flex; flex-direction: column; gap: 2px;
    background: transparent;
    border: 1px solid var(--hair);
    border-radius: 8px;
    padding: 8px 9px;
    color: var(--ink-dim);
    font-family: inherit;
    text-align: left;
    cursor: pointer;
    transition: border-color 0.15s, color 0.15s, background 0.15s, opacity 0.15s;
  }
  .texq-opt:hover:not(:disabled) { border-color: var(--ink-dim); color: var(--ink); }
  .texq-opt .texq-name { font-size: 12px; font-weight: 500; }
  .texq-opt .texq-meta { font-size: 10px; font-variant-numeric: tabular-nums; opacity: 0.75; }
  .texq-opt.active {
    background: rgba(255,255,255,0.05);
    border-color: var(--accent);
    color: var(--ink);
  }
  .texq-opt:disabled { opacity: 0.5; cursor: progress; }
  .texq-note {
    margin-top: 9px;
    font-size: 10px;
    line-height: 1.45;
    color: var(--ink-dim);
  }
  .texq-note.busy { color: var(--accent); }

  .h2-note {
    font-weight: 500;
    letter-spacing: 0.04em;
    text-transform: none;
    color: var(--ink-dim);
    opacity: 0.7;
  }
  .depth-head { margin-top: 14px; }
  .depth-scale {
    height: 6px;
    border-radius: 3px;
    margin: 2px 0 5px;
    /* GEEN KLEUREN MEER HIER (sessie 41). Deze reeks stond de DEPTH_STOPS uit
       index.html na te schrijven, en dat is een tweede bron van waarheid: bij de
       nieuwe diepteschaal zei de legenda nog de oude kleuren terwijl de ringen
       de nieuwe droegen. Het script vult hem nu uit DEPTH_STOPS zelf.
       De waarde hieronder is alleen wat er staat vóór dat script draait. */
    background: linear-gradient(90deg, #fff5be, #3e1d5e);
  }
  .depth-label {
    display: flex;
    justify-content: space-between;
    font-size: 10px;
    color: var(--ink-dim);
    letter-spacing: 0.02em;
  }
  .slider-wrap { margin-bottom: 2px; }
  /* Deze styling hing eerst alleen aan `#slider`, het ID van de tijdvenster-slider
     bij de aardbevingen. Elke nieuwe slider viel daardoor terug op de native
     browserstijl — smaller dan het paneel en met een ander uiterlijk. Nu een klasse
     die alle sliders delen; `#slider` blijft erbij zodat de bestaande markup niets
     hoeft te veranderen. */
  #slider,
  .range {
    -webkit-appearance: none;
    appearance: none;
    display: block;
    width: 100%;
    box-sizing: border-box;
    height: 3px;
    border-radius: 2px;
    background: var(--hair);
    outline: none;
    /* De slider mag zelf alle vingerbeweging afhandelen. Met de `pan-y` van
       .panel-body zou een schuine sleep het paneel laten meescrollen. */
    touch-action: none;
    margin: 4px 0 8px;
  }
  #slider::-webkit-slider-thumb,
  .range::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 14px; height: 14px;
    border-radius: 50%;
    background: var(--accent);
    cursor: pointer;
    box-shadow: 0 0 8px rgba(255,107,61,0.6);
  }
  #slider::-moz-range-thumb,
  .range::-moz-range-thumb {
    width: 14px; height: 14px;
    border: none;
    border-radius: 50%;
    background: var(--accent);
    cursor: pointer;
    box-shadow: 0 0 8px rgba(255,107,61,0.6);
  }
  .slider-label {
    display: flex;
    justify-content: space-between;
    font-size: 10px;
    color: var(--ink-dim);
    letter-spacing: 0.02em;
  }
  .slider-label span:last-child {
    color: var(--ink);
    font-variant-numeric: tabular-nums;
  }

  /* ---- Earth-lagen indicatie ---- */
  .earth-layers { display: none; }
  .earth-layers.show { display: block; }
  .elayer {
    display: flex;
    align-items: center;
    gap: 9px;
    padding: 5px 0;
    font-size: 12px;
    color: var(--ink-dim);
    cursor: pointer;
    user-select: none;
    transition: opacity 0.15s;
  }
  .elayer:hover { color: var(--ink); }
  .elayer.disabled {
    cursor: default;
    opacity: 0.4;
  }
  .elayer.disabled:hover { color: var(--ink-dim); }
  .elayer .edot {
    width: 7px; height: 7px;
    border-radius: 50%;
    background: var(--hair);
    flex-shrink: 0;
    transition: background 0.2s, box-shadow 0.2s;
  }
  .elayer.on { color: var(--ink); }
  .elayer.on .edot {
    background: #4ade80;
    box-shadow: 0 0 7px rgba(74,222,128,0.7);
  }
  /* Overlay-rijen dragen hun eigen lijnkleur (via --dot, gezet vanuit JS), zodat het
     paneel en de bol dezelfde taal spreken. Uit = gedempt, aan = vol met gloed. */
  .elayer[data-overlay] .edot {
    background: var(--dot, var(--hair));
    box-shadow: none;
    opacity: 0.4;
    transition: opacity 0.2s, box-shadow 0.2s;
  }
  .elayer[data-overlay].on .edot {
    opacity: 1;
    box-shadow: 0 0 7px var(--dot-glow, rgba(255,255,255,0.55));
  }
  .elayer .estat {
    margin-left: auto;
    font-size: 10px;
    font-variant-numeric: tabular-nums;
  }

  /* Kop-met-waarde boven een slider. `.opt-slider` deelt de opmaak met
     `.night-slider`: die styling hing eerst alleen aan de nachtzijde, en een tweede
     exemplaar zou hem hebben moeten kopiëren — dezelfde tijdbom als de sliderstyling
     die aan `#slider` hing (sessie 14). Nieuwe sliders krijgen `.opt-slider`. */
  .night-slider, .opt-slider { margin-top: 12px; }
  .opt-slider { margin-bottom: 4px; }
  .night-slider .ns-label,
  .opt-slider .ns-label {
    display: flex;
    justify-content: space-between;
    font-size: 11px;
    color: var(--ink-dim);
    margin-bottom: 6px;
  }
  .night-slider .ns-label span:last-child,
  .opt-slider .ns-label span:last-child {
    color: var(--ink);
    font-variant-numeric: tabular-nums;
  }
  #night-slider {
    -webkit-appearance: none;
    appearance: none;
    width: 100%;
    height: 3px;
    border-radius: 2px;
    background: var(--hair);
    outline: none;
    /* De slider mag zelf alle vingerbeweging afhandelen. Met de `pan-y` van
       .panel-body zou een schuine sleep het paneel laten meescrollen. */
    touch-action: none;
  }
  #night-slider::-webkit-slider-thumb {
    -webkit-appearance: none;
    appearance: none;
    width: 13px; height: 13px;
    border-radius: 50%;
    background: #b3e5fc;
    cursor: pointer;
    box-shadow: 0 0 7px rgba(179,229,252,0.6);
  }
  #night-slider::-moz-range-thumb {
    width: 13px; height: 13px;
    border: none; border-radius: 50%;
    background: #b3e5fc;
    cursor: pointer;
  }

  .panel .footnote {
    margin-top: 14px;
    padding-top: 13px;
    border-top: 1px solid var(--hair);
    font-size: 10px;
    line-height: 1.5;
    color: var(--ink-dim);
  }
  .panel .footnote a { color: var(--ink); text-decoration: none; border-bottom: 1px solid var(--hair); }
  .panel .footnote a:hover { color: #5fcdfb; border-bottom-color: #5fcdfb; }
  .panel .footnote .live {
    display: inline-block;
    width: 6px; height: 6px;
    border-radius: 50%;
    background: #4ade80;
    margin-right: 5px;
    animation: pulse 2s infinite;
  }
  @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.3} }

  /* ---- Detail readout ---- */
  .readout {
    position: fixed;
    right: 28px;
    /* `--panel-lift` en niet de ingetikte 28 px die hier tot sessie 25 stond: onder
       1100 px loopt het tijdeiland door tot `right: 44px` en overlapt het dit
       venster horizontaal. Op 81 px (breed) staat de onderrand boven de tijdrij,
       die op 28 begint en 40 hoog is. */
    bottom: var(--panel-lift);
    z-index: var(--z-detail);
    width: 280px;
    background: rgba(11,16,24,0.72);
    backdrop-filter: blur(14px);
    border: 1px solid var(--hair);
    border-radius: 14px;
    padding: 18px;
    opacity: 0;
    pointer-events: none;
  }
  /* Tot 2026-07-31 was een andere indicator aanklikken de enige manier om dit
     venster kwijt te raken. Nu een sluitknop, op élk formaat: op mobiel omdat het
     venster daar de halve bol bedekt, op desktop omdat het er simpelweg hoorde. */
  .readout-close {
    position: absolute;
    top: 8px; right: 8px;
    width: 28px; height: 28px;
    display: flex; align-items: center; justify-content: center;
    background: transparent;
    border: 0;
    border-radius: 8px;
    color: var(--ink-dim);
    font-size: 18px;
    line-height: 1;
    cursor: pointer;
  }
  .readout-close:hover { background: rgba(255,255,255,0.08); color: var(--ink); }
  /* Sluiten of terug — dezelfde plek, dezelfde knop. Wisselen op `data-mode` en niet
     op `hidden`: dit is `display: flex`, en dan doet `hidden` niets. Vijfde keer in
     dit project; zie ook `.ti-panel`, `.ti-reset`, `.dock-btn`, `.detail-plot`. */
  .readout-close .rc-back { display: none; width: 15px; height: 15px; }
  .readout-close[data-mode="back"] .rc-close { display: none; }
  .readout-close[data-mode="back"] .rc-back { display: block; }
  /* De kicker mag niet onder de sluitknop doorlopen. */
  .readout .kicker { padding-right: 26px; }
  .readout .kicker {
    font-size: 10px;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    margin-bottom: 8px;
  }
  .readout .headline {
    font-size: 17px;
    font-weight: 600;
    line-height: 1.25;
    margin-bottom: 12px;
  }
  .readout .row {
    display: flex;
    justify-content: space-between;
    /* EEN TUSSENRUIMTE, WANT EEN WAARDE MAG EEN ZIN ZIJN (sessie 32). Tot nu toe
       stond er rechts altijd een getal van hooguit tien tekens. De magnetosfeer
       zet er zinnen neer — "not published yet · band 2 (house value)" — en
       `space-between` zonder gap laat die tegen zijn sleutel aan lopen zodra de
       regel omslaat. Gemeten in het venster van 280 px. */
    gap: 10px;
    font-size: 12px;
    padding: 4px 0;
    border-top: 1px solid var(--hair);
  }
  /* De sleutel krimpt niet mee: hij is de kortste van de twee en hoort in één
     stuk leesbaar te blijven. De waarde vult de rest en lijnt rechts uit, zodat
     een kolom getallen een kolom blijft. */
  .readout .row .k { flex: 0 0 auto; }
  .readout .row .v { flex: 1 1 auto; text-align: right; }
  /* De sectiekop tussen de rijen (sessie 32). Dezelfde vorm als in de POC: een
     bovenrand die zwaarder is dan die van een rij, en kleine kapitalen. De
     eerste kop krijgt geen extra ruimte erboven — daar staat de headline al. */
  .readout .sec {
    margin: 10px 0 2px;
    padding-top: 8px;
    border-top: 1px solid var(--hair);
    font-size: 10px;
    letter-spacing: 0.11em;
    text-transform: uppercase;
    color: var(--ink-faint);
  }
  .readout .sec:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
  /* EEN ALINEA OVER DE VOLLE BREEDTE (sessie 42). De rijen hierboven zetten hun
     waarde rechts tegen de rand; dat is goed voor een getal en fout voor de
     lopende tekst van een JTWC-waarschuwing. Iets kleiner en met meer regelwit
     dan een rij, zodat het als tekst leest en niet als nog een meting. */
  .readout .prose {
    margin: 6px 0 2px;
    font-size: 11.5px;
    line-height: 1.5;
    color: var(--ink-dim);
    text-align: left;
  }
  /* Een rij direct ná een kop heeft zijn eigen bovenrand niet nodig: die van de
     kop staat er al, en twee lijnen op vier pixels leest als een fout. */
  .readout .sec + .row { border-top: 0; }
  .readout .row .k { color: var(--ink-dim); }
  /* Een waarde die er niet is. Geen getal, dus ook niet de nadruk van een getal —
     en cursief, zodat "no density or speed" niet als een meting leest. */
  .readout .row .v.absent { color: var(--ink-faint); font-style: italic; }
  .readout .row .v { font-variant-numeric: tabular-nums; }
  .readout .row .v a { color: #5fcdfb; text-decoration: none; }
  .readout .row .v a:hover { text-decoration: underline; }
  .readout .more-link {
    display: block; margin-top: 12px; padding: 8px 12px; text-align: center;
    font-size: 11.5px; font-weight: 600; letter-spacing: 0.02em;
    color: #5fcdfb; text-decoration: none;
    background: rgba(95,205,251,0.10); border: 1px solid rgba(95,205,251,0.30);
    border-radius: 8px; transition: background 0.15s, border-color 0.15s;
  }
  .readout .more-link:hover { background: rgba(95,205,251,0.18); border-color: rgba(95,205,251,0.5); }
  .readout .more-link[hidden] { display: none; }

  /* ---- De plot-sleuf en de handelingsknoppen (sessie 25) ------------------
     De sleuf is leeg tenzij de soort er iets in tekent — het magneetveld zet er
     zijn poolplot in, want een canvas past niet in een sleutel/waarde-rij.
     Allebei krijgen ze een eigen `[hidden]`-regel mee: `display` uit een klasse
     verslaat het attribuut, en die val heeft in dit project al vier keer
     toegeslagen (.ti-panel, .ti-reset, .dock-btn, .time-reset). */
  .readout .detail-plot { margin-top: 12px; display: flex; justify-content: center; }
  .readout .detail-plot[hidden] { display: none; }
  .readout .detail-plot canvas { max-width: 100%; height: auto; }

  .readout .detail-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 12px; }
  .readout .detail-actions[hidden] { display: none; }
  /* ÉÉN ACTIE PER REGEL (sessie 28). Hier stond `flex: 1` met de knoppen naast
     elkaar, en dat kapte ze allemaal af. GEMETEN in het venster van 280 px: de rij
     is 231 px breed, dus twee knoppen krijgen er 112 elk, en daarvan blijft na de
     rand (2), de padding (12), het icoon (13) en de tussenruimte (6) precies 79 px
     tekst over. "Go to this moment" meet 102 en "Show on globe" 81 — allebei te
     lang, en dat was al zo vóór de antipode-knop erbij kwam; die maakte het alleen
     zichtbaar door er een derde bij te zetten (75 px per stuk).

     Twee wegen eruit: de labels inkorten of ze onder elkaar zetten. Inkorten laat
     de gebruiker raden wat een knop doet, en het is bovendien niet aan de layout om
     de tekst te herschrijven. Onder elkaar kost hoogte — GEMETEN 109 px voor drie
     knoppen tegen 31 voor één rij — maar dit venster is een LEESvenster met hooguit
     drie acties, en hoogte is er goedkoper dan onduidelijkheid. Bijvangst: een
     treffervlak van 231×31 in plaats van 112×31. Alleen de BREEDTE wint daarmee; de
     31 px hoogte blijft onder de 44 van Apple en is een eigen punt. */
  .detail-act {
    flex: 1 1 100%; min-width: 0;
    display: flex; align-items: center; justify-content: center; gap: 6px;
    padding: 8px 6px;
    background: transparent;
    border: 1px solid var(--hair);
    border-radius: 8px;
    color: var(--ink-dim);
    font-family: inherit; font-size: 11px; font-weight: 600; letter-spacing: 0.02em;
    cursor: pointer;
    transition: color 0.15s, border-color 0.15s, background 0.15s;
  }
  .detail-act span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  .detail-act svg { width: 13px; height: 13px; flex-shrink: 0; }
  .detail-act:hover { color: var(--ink); border-color: var(--ink-faint); background: rgba(255,255,255,0.05); }

  /* ---- Loading ---- */
  .boot {
    position: fixed;
    inset: 0;
    z-index: 50;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 16px;
    background: var(--void);
    transition: opacity 0.6s;
  }
  .boot.done { opacity: 0; pointer-events: none; }
  /* HET MERK, in dezelfde vorm als de kop van de app. */
  .boot-mark {
    font-size: 15px; font-weight: 600; letter-spacing: 0.34em;
    text-transform: uppercase; color: var(--ink);
  }
  .boot-mark span { color: var(--accent); }

  /* DE RING VULT ZICH VAN 0 NAAR 100 % (sessie 42, Terry).
     Hij draaide alleen maar rond, en dat zegt niets over hoe ver het is. De
     omtrek van r=15 is 2*pi*15 = 94,25; `stroke-dasharray` zet één streep van
     die lengte neer en `stroke-dashoffset` schuift hem weg. Offset 94,25 is leeg,
     0 is vol. De -90 graden draai zet het beginpunt bovenaan in plaats van
     rechts. */
  .boot-ring { width: 34px; height: 34px; transform: rotate(-90deg); }
  .boot-ring circle {
    fill: none; stroke-width: 2; cx: 17; cy: 17; r: 15;
  }
  .boot-ring-baan { stroke: var(--hair); }
  .boot-ring-vul {
    stroke: var(--accent);
    stroke-linecap: round;
    stroke-dasharray: 94.25;
    stroke-dashoffset: 94.25;
    /* Vloeiend meelopen met de sprongen die de fasen maken; zonder deze
       overgang klikt de ring van stand naar stand. */
    transition: stroke-dashoffset 0.35s linear;
  }
  .boot .msg {
    font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase;
    color: var(--ink-dim);
    /* De tekst wisselt; een korte vervaging maakt van de wissel een overgang
       in plaats van een sprong. */
    transition: opacity 0.2s;
  }
  .boot .msg.wisselt { opacity: 0; }

  /* ---- Het boot-scherm als foutmelding (2026-08-01) --------------------------
     Tot sessie 11 kende dit scherm maar één toestand: draaien. Startte de module
     niet, dan bleef de ring eeuwig rondgaan en zag een kapotte app eruit alsof hij
     nog bezig was. Dat is precies wat de boot-hang op Vercel opleverde.
     `.failed` zet de ring uit en de uitleg aan; de opbouw zit in het head-script
     van index.html, zodat hij ook werkt als de module nooit draait. */
  .boot.failed .boot-ring { display: none; }
  .boot.failed .msg { display: none; }
  .boot-error {
    display: none;
    max-width: 380px;
    padding: 0 28px;
    text-align: center;
  }
  .boot.failed .boot-error { display: block; }
  .boot-error h2 {
    margin: 0 0 10px;
    font-size: 13px;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--ink);
  }
  .boot-error p {
    margin: 0 0 18px;
    font-size: 13px;
    line-height: 1.6;
    color: var(--ink-dim);
  }
  .boot-error button {
    font: inherit;
    font-size: 12px;
    letter-spacing: 0.08em;
    color: var(--ink);
    background: rgba(255,107,61,0.14);
    border: 1px solid var(--accent);
    border-radius: 8px;
    padding: 9px 20px;
    cursor: pointer;
    transition: background 0.15s ease;
  }
  .boot-error button:hover { background: rgba(255,107,61,0.24); }
  .boot-error .hint {
    margin: 16px 0 0;
    font-size: 11px;
    color: var(--ink-faint);
  }

  /* ---- Leader-line labels voor de zwaarste bevingen ----
     ONDERAAN IN DE LADDER (sessie 25). Dit stond op een ingetikte 9 — tussen het
     tijdeiland (8) en de panelen (10) in — en dat las als een keuze, maar het
     gevolg was dat een label over het datumveld van het tijdpaneel schoof en de
     tekst eronder onleesbaar maakte.
     Deze labels horen bij de BOL en niet bij de app: ze komen en gaan met de data,
     en er komt vanzelf een volgende in beeld. Wat je bedient hoort daar altijd
     bovenop te liggen. Let op: `.rlabel` (zonnegebieden) en `.alabel` (de aspolen)
     wonen in dezelfde container en erven deze laag. */
  #quake-labels {
    position: fixed;
    inset: 0;
    z-index: var(--z-labels);
    pointer-events: none;
    overflow: hidden;
  }
  /* Labels bij de actieve gebieden op de zon. Een kader met blur in de overlay-laag
     #quake-labels, die gedeeld wordt met de beving-labels (.ql-*, opgemaakt vanuit
     js/ui/quake-labels.js) en de aslabels. Warmer van kleur en zonder de
     zoom-afhankelijke schaling, want in het zonaanzicht staat de camera altijd op
     dezelfde afstand. */
  .rlabel {
    position: absolute;
    top: 0; left: 0;
    display: none;
    white-space: nowrap;
    background: rgba(5,7,13,0.82);
    backdrop-filter: blur(6px);
    border: 1px solid var(--hair);
    border-radius: 7px;
    padding: 4px 9px;
    font-size: 11px;
    line-height: 1.3;
    color: var(--ink);
    box-shadow: 0 4px 16px rgba(0,0,0,0.5);
    pointer-events: auto;
    cursor: pointer;
    transition: border-color 0.15s, background 0.15s;
  }
  .rlabel b { color: #ffb347; font-weight: 700; }
  .rlabel span { color: var(--ink-dim); margin-left: 6px; }
  .rlabel:hover { border-color: #ffb347; background: rgba(11,16,24,0.92); }
  .rline { stroke: #ffb347; stroke-width: 1; stroke-dasharray: 2 3; opacity: 0.55; }

  /* A flare on the sun, under the spot its region sits on — the region label
     hangs above it, so a region with a flare reads as one block. The colour is
     the class colour from the lane, set per label, so the same flare looks the
     same in both places. */
  .flabel {
    position: absolute;
    top: 0; left: 0;
    display: none;
    white-space: nowrap;
    background: rgba(5,7,13,0.82);
    backdrop-filter: blur(6px);
    border: 1px solid var(--flare-ink, var(--hair));
    border-radius: 7px;
    padding: 3px 8px;
    font-size: 10px;
    line-height: 1.3;
    color: var(--ink);
    box-shadow: 0 4px 16px rgba(0,0,0,0.5);
    pointer-events: auto;
    cursor: pointer;
    transition: background 0.15s;
  }
  .flabel b { color: var(--flare-ink, var(--ink)); font-weight: 700; letter-spacing: 0.04em; }
  .flabel span { color: var(--ink-dim); margin-left: 6px; }
  .flabel:hover { background: rgba(11,16,24,0.94); }
  .fline { stroke: var(--flare-ink, var(--hair)); stroke-width: 1; stroke-dasharray: 2 3; opacity: 0.5; }

  /* N en S bij de uiteinden van de assen. Delen dezelfde overlay-laag als .rlabel
     en .rlabel, maar zonder kader, zonder leader-line en zonder muisinteractie:
     het is een aanduiding bij een lijn, geen kaartje met inhoud. De kleur komt
     inline mee en onderscheidt de rotatie-as van de magnetische. */
  .alabel {
    position: absolute;
    top: 0; left: 0;
    display: none;
    font-size: 11px; font-weight: 700; letter-spacing: 0.06em;
    line-height: 1;
    padding: 3px 5px;
    border-radius: 5px;
    background: rgba(5,7,13,0.6);
    pointer-events: none;
    text-shadow: 0 0 5px rgba(0,0,0,0.95);
  }

  #label-lines {
    position: absolute;
    inset: 0;
    width: 100%; height: 100%;
    overflow: visible;
    pointer-events: none;
  }

  /* ---- Het tijdeiland (sessie 20, 2026-08-08) ----------------------------------
     Tegenhanger van .mode-switch bovenaan: zelfde taal (dun kader, blur, ingedrukte
     staat lichter), andere kant van het scherm.

     GEEN `transform: translateX(-50%)`, en dat is bewust. Die truc is bij .mode-switch
     en #toasts twee keer misgegaan zodra een media query `left`/`right` zette — het
     element werd schermbreed en schoof daarna nog een halve schermbreedte weg. Hier
     doen `left`/`right` + `margin: 0 auto` het centreren, en dan is er geen transform
     die opgeheven moet worden.

     De marges van 316 px zijn niet willekeurig: het paneel staat op `left: 28px` en is
     280 px breed, de readout net zo aan de rechterkant. Het eiland centreert zich dus
     in de RUIMTE ERTUSSEN in plaats van op het scherm, en kan per constructie niet
     botsen — ook niet op een smal venster, waar het gewoon meekrimpt. */
  /* De transportbalk van de magnetosfeer deelt deze plek — nooit tegelijk, zie
     de schakelregels verderop. Eén regel voor allebei, zodat een correctie aan
     de onderrand er niet één van de twee vergeet; dat is precies wat er in
     sessie 24 gebeurde toen deze maten uit elkaar liepen. */
  .time-island, .msphere-time, .solar-time {
    position: fixed;
    /* De variabele en niet de ingetikte 28 px: `@media (max-height: 480px)` zet
       `--dock-bottom` op 12 en schoof de knoppenrij mee, maar het eiland bleef op
       28 staan. Precies de val waar de noot in `:root` voor waarschuwt. */
    bottom: var(--dock-bottom);
    left: 316px;
    right: 316px;
    margin: 0 auto;
    max-width: 560px;
    z-index: var(--z-island);
    display: flex;
    flex-direction: column;
    gap: 8px;
    pointer-events: auto;
  }
  /* EEN GRID EN GEEN FLEXBOX, en dat is de kern van de sleep-fix. Met flexbox bepaalde
     de BREEDTE VAN HET MOMENTLABEL hoeveel ruimte de slider overhield: dat label groeit
     van 22 px ("now") tot 145 px, dus de baan sprong tijdens het slepen van 375 naar
     214 px en de knop schoof onder de cursor weg. Een `min-width` op het label is geen
     oplossing — "minstens" betekent dat een breder label alsnog duwt (gemeten: 240 vs
     214 px). Met een VASTE kolom voor het moment kan de inhoud de slider per constructie
     niet meer raken; `minmax(0, 1fr)` houdt de sliderkolom krimpbaar. */
  .ti-row {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) 190px auto;
    grid-template-areas: "play slider now more";
    align-items: center;
    gap: 10px;
    padding: 6px 8px 6px 6px;
    background: rgba(11,16,24,0.72);
    backdrop-filter: blur(14px);
    border: 1px solid var(--hair);
    border-radius: 12px;
  }
  .ti-play { grid-area: play; display: flex; gap: 2px; }
  .ti-play .play-btn { padding: 5px 9px; font-size: 11px; }

  /* De slider krijgt de rek. `min-width: 0` is nodig omdat een flex-item anders niet
     onder zijn inhoudsbreedte krimpt en de rij op smalle vensters uitbreekt. */
  /* Eén rij, geen kolom. Het bereiklabel stond hier onder de slider en maakte deze
     kolom hoger dan de knoppen ernaast; `align-items: center` op de rij centreert dan
     de KOLOM, waardoor de sliderbaan zelf boven het midden uitkwam. Het label is naar
     het uitklappaneel verhuisd, bij het trackvenster waar het thuishoort. */
  .ti-slider { grid-area: slider; display: flex; align-items: center; }
  .ti-slider .range { width: 100%; margin: 0; }
  .ti-span { font-style: normal; letter-spacing: 0; text-transform: none; color: var(--ink-faint); font-variant-numeric: tabular-nums; }

  /* `justify-content: flex-end` houdt het moment tegen de rechterkant van zijn vaste
     kolom, zodat de tekst naar links uitgroeit en de ✕ altijd op dezelfde plek staat. */
  .ti-now { grid-area: now; display: flex; align-items: center; justify-content: flex-end; gap: 6px; }
  /* Geen eigen breedte: de kolom in .ti-row is al vast, dus dit label mag groeien en
     krimpen zonder dat de slider het merkt. Loopt het ooit over zijn kolom (een andere
     taal, een ander lettertype), dan kapt het netjes af in plaats van de rij te breken —
     layout-stabiliteit gaat hier voor de laatste letters van een uiterste geval. */
  /* HET IS EEN <button> SINDS SESSIE 25, en de knopstijlen worden hier gewist zodat
     hij precies zoveel ruimte inneemt als het <span> ervoor deed. Dat is geen
     schoonheidsfoutje maar de kern: de kolombreedte in .ti-row is een GEMETEN getal
     dat een sleepfout afdekt (zie de noot daar), en een knop met eigen padding zou
     dat getal stilzwijgend ongeldig maken. */
  .ti-val {
    overflow: hidden;
    text-overflow: ellipsis;
    font-size: 11px;
    font-weight: 600;
    color: var(--ink-dim);
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    background: none;
    border: 0;
    padding: 0;
    margin: 0;
    font-family: inherit;
    line-height: 1.2;
    cursor: pointer;
    transition: color 0.15s;
  }
  .ti-val:hover { color: var(--ink); }
  /* Verschoven moment: de niet te missen markering dat de scene een ander moment
     toont dan het heden — de live datalagen doen dat namelijk niet. Amber en niet
     `--accent`: dat is wat er tot sessie 25 daadwerkelijk stond, via een
     ID-selector die deze regel versloeg. Zie het blok bij .time-input. */
  .ti-val.shifted { color: #ffd166; }
  .ti-val.shifted:hover { color: #ffe1a0; }
  .ti-reset {
    display: flex; align-items: center; justify-content: center;
    width: 20px; height: 20px; padding: 0;
    background: transparent;
    border: 1px solid var(--hair);
    border-radius: 6px;
    color: var(--ink-dim);
    font-family: inherit; font-size: 10px; line-height: 1;
    cursor: pointer;
    transition: all 0.15s;
  }
  .ti-reset:hover { border-color: var(--accent); color: var(--accent); }
  /* Zie .ti-panel[hidden]: een `display` uit een klasse wint van het attribuut, en dan
     staat "terug naar nu" er ook als de tijd al op nu staat. */
  .ti-reset[hidden] { display: none; }

  .ti-more {
    grid-area: more;
    display: flex; align-items: center; justify-content: center;
    width: 26px; height: 26px; padding: 0;
    background: transparent;
    border: none;
    border-radius: 7px;
    color: var(--ink-faint);
    cursor: pointer;
    transition: color 0.15s, transform 0.2s;
  }
  .ti-more:hover { color: var(--ink); }
  .ti-more svg { width: 15px; height: 15px; }
  .ti-more[aria-expanded="true"] { color: var(--ink); }
  /* TWEE ICONEN, ÉÉN ZICHTBAAR. Dicht een klok — die zegt waar de knop over gaat,
     wat een kale chevron niet deed; open de chevron omlaag, want dat betekent
     "klap weg". Hier stond `transform: rotate(180deg)` op de knop, en dat kan nu
     niet meer: een klok een halve slag draaien betekent niets.
     Wisselen met `display` en niet met het `hidden`-attribuut, zodat de val van
     .ti-panel[hidden] hier niet eens kan optreden. */
  .ti-more .ti-ico-chev { display: none; }
  .ti-more[aria-expanded="true"] .ti-ico-clock { display: none; }
  .ti-more[aria-expanded="true"] .ti-ico-chev { display: block; }

  .ti-panel {
    position: relative;   /* anker voor .ti-close */
    padding: 13px 14px;
    background: rgba(11,16,24,0.82);
    backdrop-filter: blur(14px);
    border: 1px solid var(--hair);
    border-radius: 12px;
    display: flex;
    flex-direction: column;
    gap: 11px;
  }
  /* `display` uit een klasse wint van het `hidden`-attribuut, dat alleen een
     user-agent-stijl is. Zonder deze regel staat het paneel altijd open. Dezelfde val
     als bij .ti-reset[hidden] hierboven. */
  .ti-panel[hidden] { display: none; }
  /* Het kruisje van het tijdpaneel (sessie 25). Zelfde vorm en gedrag als
     .readout-close; absoluut geplaatst omdat een kop-rij hier een regel kost die
     een telefoon in landscape niet heeft. */
  .ti-close {
    position: absolute;
    top: 6px; right: 8px;
    width: 24px; height: 24px;
    display: flex; align-items: center; justify-content: center;
    background: transparent;
    border: 0;
    border-radius: 7px;
    color: var(--ink-dim);
    font-family: inherit; font-size: 16px; line-height: 1;
    cursor: pointer;
    transition: color 0.15s, background 0.15s;
  }
  .ti-close:hover { color: var(--ink); background: rgba(255,255,255,0.08); }
  /* De uitleg mag niet onder het kruisje doorlopen — zelfde correctie als
     `.readout .kicker { padding-right: 26px }`. */
  .ti-note { font-size: 10px; line-height: 1.55; color: var(--ink-faint); padding-right: 24px; }
  .ti-field { display: flex; flex-direction: column; gap: 6px; }
  .ti-field > span {
    display: flex; align-items: baseline; justify-content: space-between;
    font-size: 10px; font-weight: 600; letter-spacing: 0.06em;
    text-transform: uppercase; color: var(--ink-dim);
  }
  .ti-field > span i { font-style: normal; letter-spacing: 0; text-transform: none; color: var(--ink-faint); }
  .ti-field .time-input { margin: 0; }
  .ti-field .presets { margin: 0; }

  /* Wat in een weergave niet van toepassing is, wordt niet getoond — niet uitgegrijsd.
     De klasse staat met opzet op <body> en niet op de sectie, precies zoals
     body.space-on, zodat een volgende state hier niets nieuws voor hoeft te bedenken.
     In het zonaanzicht zijn de zonnevlekken een momentopname van NOAA; een tijdslider
     erbij zou iets beloven wat er niet is. */

  /* En om een verwante maar niet dezelfde reden ook in de magnetosfeer. Daar is
     er WEL een tijd om te kiezen, maar alleen binnen de zeven dagen waarin
     gemeten is — de magnetosfeer van vorige maand bestaat niet als meting. Een
     slider die tot 1985 reikt zou een vorm beloven die nooit is waargenomen.
     De state neemt de klok over en brengt in B3c zijn eigen bediening mee;
     zie de kop van js/states/magnetosphere.js. */
  body.msphere-on .time-island { display: none; }

  /* En de transportbalk komt ervoor in de plaats. Hij staat standaard uit en
     niet op `hidden`: de regel hierboven zet `display`, en display in een
     klasse verslaat het hidden-attribuut. De body-klasse weet toch al in welke
     state we zijn, dus een tweede schakelaar in JS zou een tweede waarheid zijn. */
  .msphere-time { display: none; }
  body.msphere-on .msphere-time { display: flex; }

  /* The sun's strip, the same way and for the same reason. */
  .solar-time { display: none; }
  body.solar-on .solar-time { display: flex; }

  /* WAT IN DEZE STATE NIET VAN TOEPASSING IS, WORDT NIET GETOOND — niet
     uitgegrijsd. Dezelfde vorm als .space-only hierboven, en om dezelfde reden
     op <body> en niet op de sectie zelf.

     Het gaat om échte betekenisloosheid en niet om opruimen: `enter()` zet met
     eventsOff() alle gebeurtenislagen uit en met environmentOff() zon en maan,
     dus die schakelaars staan er als knoppen die niets doen. De bevingsfilters
     (Magnitude, Upper limit, Time window, Depth) zitten binnen dezelfde sectie
     als de lagenlijst en gaan mee.

     `Axes & poles` BLIJFT, en dat is inhoudelijk: de dipoolas is hier het
     onderwerp — GSM heeft hem nodig om zijn Y-as te definiëren. Zie de sectie
     in index.html.

     De tabrij gaat uit via zijn id en niet via `.panel .tabs`: een selector die
     de plek noemt breekt stil bij een verhuizing, en dat is in sessie 25 al een
     keer gebeurd. */
  /* HET HELE FILTERS-TABPANEL, en niet zijn secties een voor een (sessie 38).
     Sinds de splitsing draagt dat panel vier accordeons in plaats van een; ze een
     voor een noemen zou betekenen dat een vijfde er stil doorheen glipt. De JS
     dwingt hier bovendien de tab `Layers` af, maar CSS is de tweede verdediging:
     dezelfde tweeledige vorm die de tekentab in sessie 36 nodig had. */
  body.msphere-on .panel .tabpanel[data-panel="filters"],
  body.msphere-on [data-acc="earth-overlays"],
  body.msphere-on [data-acc="sky-projections"],
  body.msphere-on [data-acc="cosmos-layers"],
  body.msphere-on #panel-tabs,
  /* OP ZIJN ID EN NIET OP `.mode-switch`, want die klasse draagt sinds sessie 31
     twee dingen: de bolweergave én de camerastand, die dezelfde gleuf en
     dezelfde vormtaal delen. Op de klasse toetsen verborg ze allebei — en dat
     zag je niet in de code, alleen aan een gleuf die leeg bleef. Zelfde reden
     die #panel-tabs hierboven al op zijn id zet. */
  body.msphere-on #mode-switch { display: none; }

  /* En de eigen sectie is er alleen dan. LET OP dezelfde val als bij
     .space-only: een expliciete `display` in een klasse verslaat het
     `hidden`-attribuut. */
  .msphere-only { display: none; }
  body.msphere-on .msphere-only { display: block; }

  /* ---- HET PANEEL TOONT HIER ALLEEN DE ZON (Terry, sessie 50) ----

     Er hoort precies één sectie bij deze state, en zonder deze regels staat het
     paneel er compleet: alle tabpanelen en de hele tabrij. Dezelfde vorm als
     `body.msphere-on` hierboven, en om dezelfde reden: wat hier niet van
     toepassing is wordt niet getoond in plaats van uitgegrijsd. `enter()` zet
     met eventsOff() en environmentOff() alle aardgebonden lagen uit, dus die
     schakelaars staan er als knoppen die niets doen.

     (Geen laagnaam in deze noot: dit blok overleeft de standalone-knip, en de
     vangrail in tools/build-standalone.mjs slaat aan op de naam van de
     tekenlaag. Zelfde reden als bij de paneelkop in index.html.)

     `Axes & poles` gaat hier WÉL mee, anders dan in de magnetosfeer: daar is de
     dipoolas het onderwerp, hier staat de aarde niet eens in beeld.

     HET HELE TABPANEL en niet zijn secties een voor een, zodat een vijfde sectie
     er niet stil doorheen glipt. De tabrij op zijn id: een selector die de plek
     noemt breekt stil bij een verhuizing, en dat is in sessie 25 al gebeurd.

     DE CSS VERBERGT, DE JS KLIKT — beide helften zijn nodig. Een verborgen
     tabknop verzet zijn eigen `.show` niet, want bedraadTabs() is de enige
     schrijver daarvan en hangt aan een klik die niet meer kan komen. De klik
     staat bij de msphere-tak in index.html. */
  body.solar-on .panel .tabpanel[data-panel="filters"],
  body.solar-on [data-acc="earth-overlays"],
  body.solar-on [data-acc="sky-projections"],
  body.solar-on [data-acc="axis-layers"],
  body.solar-on [data-acc="cosmos-layers"],
  body.solar-on #panel-tabs,
  /* Op zijn id en niet op `.mode-switch`: die klasse draagt sinds sessie 31 twee
     dingen — de bolweergave én de camerastand — en op de klasse toetsen verbergt
     ze allebei. */
  body.solar-on #mode-switch { display: none; }

  /* De tabrij is weg, dus de kop zou een lege balk met een kruisje zijn. Zelfde
     regel als bij de magnetosfeer; de sectie eronder heet `The Sun` en zegt
     daarmee iets anders dan de titel van het paneel zelf. */
  body.solar-on .panel .panel-title { display: block; }

  /* ---- DE DETAILS-TAB HOUDT HIER ÉÉN ONDERWERP OVER (Terry, sessie 50) ----

     `Earth` en `Planets` verklaren in deze state niets: de aarde staat er als
     schaalobject en niet als onderwerp, en er is geen planeet te lezen. Zelfde
     vorm en dezelfde reden als de twee regels bij de magnetosfeer hierboven.

     DIT IS DE HELFT DIE CSS DOET; de andere helft klikt de tab `Sun & Moon` aan
     bij binnenkomst. Zonder die tweede helft houdt een verborgen tab zijn eigen
     `.show` en kijk je naar een paneel zonder knop erboven. */
  body.solar-on .detail-panel .tab[data-tab="d-earth"],
  body.solar-on .detail-panel .tab[data-tab="d-planets"] { display: none; }

  /* En binnen `Sun & Moon` blijft alleen de activiteit over. Zonstanden,
     schemertijden en de eclipsbaan gaan over de zon ZOALS DIE VANAF DE AARDE
     STAAT; hier kijk je door een instrument dat op de zon zelf gericht is, en
     dan is een opkomsttijd een antwoord op een andere vraag. */
  body.solar-on [data-acc="cosmos-readings"],
  body.solar-on [data-acc="cosmos-eclipse"],
  /* De vlekkenschakelaar hoort bij Terra's eigen zonnebol en doet hier niets:
     de gebieden in deze state hangen aan `Active regions` in het lagenpaneel.
     Twee schakelaars die hetzelfde beloven en waarvan er één niet werkt, is
     erger dan één schakelaar op de goede plek. */
  body.solar-on #sunspots-toggle,
  body.solar-on #sunspots-note { display: none; }


  /* ---- WHAT YOU ARE LOOKING AT, ON SCREEN ----

     ABOVE THE PICTURE, NOT IN THE LEFT COLUMN, where the layers panel covers
     it. The slot above the picture is free in this state (`#mode-switch` goes
     under body.solar-on), and that is where a caption for the picture belongs.

     TWO INSETS AND AUTO MARGINS CENTRE IT, NOT left: 50% WITH translateX(-50%).
     A fixed block placed that way gets half the screen at most for its width,
     whatever its max-width says: 188 px on a phone 375 px wide, where the rows
     break in two. The max-width keeps it clear of the brand on the left, which
     ends near 210 px, and of the icon buttons on the right.

     EVERY ROW ON ONE LINE. A row that may wrap jumps between one line and two
     while a film counts its frames. Tabular figures keep a changing number
     from shifting its row; the ellipsis is for a screen too narrow to hold it.

     `pointer-events: none` on the whole: nothing here can be operated, and
     without it an invisible rectangle over the scene would block dragging. */
  .solar-orient {
    position: fixed;
    top: 22px; left: 12px; right: 12px;
    margin-inline: auto;
    max-width: min(640px, 100vw - 460px);
    z-index: var(--z-panel);
    pointer-events: none;
    display: flex; flex-direction: column; align-items: center; gap: 3px;
    text-align: center;
    font-size: 11px; line-height: 1.45;
    font-variant-numeric: tabular-nums;
    color: var(--ink-3, #8b95a4);
  }
  .solar-orient[hidden] { display: none; }
  .solar-orient > div {
    white-space: nowrap;
    max-width: 100%; overflow: hidden; text-overflow: ellipsis;
  }
  /* De kwalificerende regel bovenaan, in de vorm die Terra elders voor een
     kicker gebruikt: klein, gespatieerd, en niet de kleur van de rest. */
  .solar-orient .so-kicker {
    font-size: 9.5px; font-weight: 600;
    letter-spacing: 0.09em; text-transform: uppercase;
    color: var(--ink-dim, #6b7480);
    margin-bottom: 2px;
  }
  .solar-orient .so-row { color: var(--ink-3, #8b95a4); }
  .solar-orient .so-sharp { display: flex; align-items: center; gap: 6px; }
  .solar-orient .so-dot {
    width: 6px; height: 6px; border-radius: 50%;
    background: currentColor; flex: 0 0 auto;
  }
  /* Drie standen, en ze horen NIET op elkaar te lijken. Groen en amber zijn
     mededelingen — scherp genoeg, of haal opnieuw op. Rood is het enige dat een
     waarschuwing is: voorbij het plafond van de bron zijn de extra pixels
     verzonnen, en dat is een andere soort zin. */
  .solar-orient .so-green { color: #7ec98f; }
  .solar-orient .so-amber { color: #ffb347; }
  .solar-orient .so-warn  { color: #ffb347; }
  .solar-orient .so-red   { color: #ff8a7a; font-weight: 600; }
  /* Not a verdict: what is on screen is a frame of a film. */
  .solar-orient .so-film  { color: var(--solar-film); }

  /* ON A PHONE IT STAYS, UNDER THE MASTHEAD. This line says the disc is a
     photograph, how old it is, and whether the zoom has passed what the source
     holds, and a phone is where nobody opens a panel to ask. The masthead row
     ends around 55 px, so it starts below that, and it takes the full width
     between the insets: there is no brand beside it to keep clear of. */
  @media (max-width: 640px) {
    .solar-orient {
      top: 58px;
      max-width: none;
      font-size: 10.5px;
    }
    .solar-orient .so-kicker { font-size: 9px; }
  }

  /* ---- HET OVERZICHTSKAARTJE (Terry, sessie 50) ----

     Rechtsboven, onder de knoppenrij. Dat is de enige hoek die in deze state in
     beide versies vrij is: links staat het lagenpaneel, rechtsonder de uitlezing
     en onderin het tijd-eiland.

     `hidden` doet het verbergen en niet een klasse: de tekenaar zet die vlag
     zelf op grond van wat hij zou tekenen, en één schrijver is er precies één
     genoeg. Vandaar ook geen `display` in een klasse hier — die zou het
     hidden-attribuut verslaan, dezelfde val als bij de zon-secties verderop.

     (Geen klassenaam uit dat blok in deze noot: dit staat buiten de
     standalone-knip en de vangrail slaat aan op die namen.) */
  .solar-map {
    position: fixed;
    top: 96px; right: 28px;
    width: 104px; height: 104px;
    z-index: var(--z-panel);
    pointer-events: none;
    border-radius: 10px;
    background: rgba(11, 16, 24, 0.55);
    border: 1px solid var(--hair);
    backdrop-filter: blur(6px);
  }

  /* On a phone it stays too, but not in that corner: the line above is centred
     and grows to four or five rows with an instrument loaded, and at 375 px the
     two would sit on top of each other. So it moves to the other free corner,
     just above the strip on the left — the floating moment uses the right. The
     offset is derived from the strip's own height, so it follows if that
     changes. */
  @media (max-width: 640px) {
    .solar-map {
      top: auto;
      bottom: calc(var(--panel-lift) + var(--sols-gap) + var(--sols-h) + var(--sols-gap) + var(--sols-week-h) + 8px);
      left: 10px; right: auto;
      width: 76px; height: 76px;
    }
  }

  /* ---- DE KALE ZON (Terry, sessie 50) ----

     Zonder de instrumenthelft is deze state een TEKENING met gemeten gebieden
     erop: geen bronnen, geen sleuven, geen ophaalknop. `js/states/sun.js` zet
     deze klasse op grond van wat er werkelijk geladen is, niet op grond van het
     protocol — zie de noot daar.

     HET PANEEL GAAT DAN HELEMAAL DICHT. De regels hierboven snoeien het tot één
     sectie, en die sectie is er in deze stand niet: wat overblijft is een lege
     bak met een titel. Zelfde vorm als bij Space, en om dezelfde reden. */
  body.solar-on.solar-bare .panel,
  body.solar-on.solar-bare #panel-open { display: none; }

  /* THE TIME ISLAND STAYS HIDDEN IN THIS STATE, in both builds. The strip
     under the sun is the time axis here: it stands on measured flux, so it
     only offers the moments that were measured, and it needs no server — the
     standalone has it as well. Only its Fetch button belongs to the imagery
     half, and without that half there is nothing to fetch. */
  body.solar-on .time-island { display: none; }
  body.solar-bare #sol-fetch,
  body.solar-bare #sol-film,
  body.solar-bare #sol-film-go,
  body.solar-bare .sol-transport,
  body.solar-bare #sol-film-x,
  body.solar-bare #sol-rate { display: none; }

  /* The lane. Same box as the magnetosphere strip, and for the same reasons:
     the size lives here and the buffer in JS, the ring is an inset shadow so it
     takes no room from the drawing, and the ground is the row's glass. */
  .solar-lane {
    display: block;
    width: 100%;
    height: var(--sols-h);
    background: rgba(11,16,24,0.72);
    backdrop-filter: blur(14px);
    box-shadow: inset 0 0 0 1px var(--hair);
    border-radius: 12px;
    pointer-events: auto;
    touch-action: none;
    cursor: crosshair;
  }
  /* A week on 560 px is 18 minutes a pixel. Between the panels it has room. */
  body.solar-on .solar-time { max-width: none; }
  /* THE WEEK UNDER THE LANE. The slider walks the measured week and the lane
     shows a window on it; the lighter stretch of the track is that window,
     painted from js/ui/solar-time.js. The input takes the row's full height so
     a finger finds it, and its track is a 3 px line through the middle. */
  .sol-week {
    height: var(--sols-week-h);
    box-sizing: border-box;
    display: flex; align-items: center;
    padding: 0 12px;
    background: rgba(11,16,24,0.72);
    backdrop-filter: blur(14px);
    border: 1px solid var(--hair);
    border-radius: 12px;
  }
  .sol-week .range {
    height: 100%;
    margin: 0;
    background: linear-gradient(var(--hair), var(--hair)) center / 100% 3px no-repeat;
  }
  .sol-actions { justify-content: flex-start; }
  .sol-fetch, .sol-film-go { font-variant-numeric: tabular-nums; }
  /* THE SUN'S ROW HAS NO SLIDER TO PROTECT. The island fixes its moment column
     at 190 px so that a growing label cannot move the slider under a dragging
     pointer. Here the middle holds buttons, and with the film's transport and
     speed beside it a fixed column left ✕ film twenty pixels, broken over two
     lines (measured on a strip of 430 px between the panels). So the moment
     gives way instead, and cuts off with an ellipsis. Phones keep their own
     row, with the moment above it. */
  @media (min-width: 641px) {
    .solar-time .ti-row { grid-template-columns: auto auto minmax(0, 1fr) auto; }
    .solar-time .ti-val { min-width: 0; }
  }
  .solar-time .ti-row .play-btn { white-space: nowrap; }
  /* WHICH BUTTONS OF THE ROW STAND is decided in js/ui/solar-time.js and shown
     by one class. Without a film: Film and Fetch. While a film is looked up or
     fetched: ✕ and the film's own button in the middle. Once a frame is in: the
     transport in the first column, ✕ film in the middle, and the speed beside
     the window. A class, because .play-btn sets display and the hidden
     attribute loses to that. */
  #solar-time .play-btn.sol-off { display: none; }
  /* Why a moment was not kept. It shows for a few seconds, above the lane. */
  .sol-note {
    align-self: flex-start;
    max-width: 100%;
    padding: 4px 9px;
    font-size: 11px;
    line-height: 1.35;
    color: var(--ink-dim);
    background: rgba(11,16,24,0.82);
    backdrop-filter: blur(14px);
    border: 1px solid var(--hair);
    border-radius: 9px;
  }

/* SOLAR:START */
  /* ---- De zon in instrumentbeelden (sessie 48) ----
     Zelfde patroon, en dezelfde val: een expliciete `display` in een klasse
     verslaat het `hidden`-attribuut, dus dit staat op de klasse en niet op
     `hidden`. */
  .solar-only { display: none; }
  body.solar-on .solar-only { display: block; }

  /* De vier voorinstellingen. Twee kolommen: vier knoppen naast elkaar passen
     niet in 280px, en vier onder elkaar maken van de snelste ingang een lijst
     om doorheen te lezen. */
  .solar-presets {
    display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin: 8px 0 10px;
  }
  .solar-presets button {
    font: inherit; font-size: 11px; letter-spacing: .02em; text-align: left;
    padding: 7px 8px; border-radius: 7px; cursor: pointer;
    background: rgba(255,255,255,.04); color: var(--ink-2, #b9c2cf);
    border: 1px solid rgba(255,255,255,.09);
  }
  .solar-presets button:hover { background: rgba(255,255,255,.08); }
  .solar-presets button.on {
    background: rgba(255,179,71,.16); border-color: rgba(255,179,71,.5);
    color: #ffd9a8;
  }

  /* Een sleuf: bron links, dekking rechts. De dekking is een schuif en geen
     getal — de waarde zelf zegt niets, het beeld wel. */
  .solar-slot { display: grid; grid-template-columns: 1fr auto; gap: 6px;
                align-items: center; margin-bottom: 6px; }
  .solar-slot select {
    font: inherit; font-size: 11px; padding: 5px 6px; border-radius: 6px;
    background: rgba(255,255,255,.05); color: var(--ink-1, #e7ecf3);
    border: 1px solid rgba(255,255,255,.1); max-width: 100%;
  }
  .solar-slot input[type=range] { width: 68px; }
  .solar-slot.blocked select { opacity: .45; }

  .solar-fetch {
    width: 100%; font: inherit; font-size: 12px; font-weight: 600;
    padding: 8px; margin-top: 4px; border-radius: 7px; cursor: pointer;
    background: rgba(255,179,71,.18); color: #ffd9a8;
    border: 1px solid rgba(255,179,71,.45);
  }
  .solar-fetch:hover { background: rgba(255,179,71,.26); }
  .solar-fetch[disabled] { opacity: .5; cursor: default; }
  /* The film's door in the panel, under the stills, in the film's own colour: two
     buttons in one colour would read as one action said twice. */
  .solar-film-row { display: flex; gap: 6px; }
  .solar-film-row .solar-fetch { width: auto; }
  /* The label wraps rather than cutting off: the layer it names is the point. */
  .solar-film {
    flex: 1; min-width: 0;
    background: rgba(127,209,199,.12); color: #c9efe9; border-color: rgba(127,209,199,.4);
    font-variant-numeric: tabular-nums;
    line-height: 1.3;
  }
  .solar-film:hover { background: rgba(127,209,199,.2); }
  .solar-film-side {
    flex: 0 0 auto; padding: 8px 11px;
    background: transparent; color: var(--ink-dim); border-color: var(--hair);
    font-variant-numeric: tabular-nums;
  }
  .solar-film-side:hover { color: var(--ink); background: rgba(255,255,255,.06); }
  .solar-film-row [hidden] { display: none; }

  .solar-note, .solar-status {
    font-size: 10.5px; line-height: 1.45; color: var(--ink-3, #8b95a4);
    margin: 8px 0;
  }
  .solar-status:empty { margin: 0; }
  .solar-status.warn { color: #ffb347; }

  /* De schakelaar is geen gevolg van de knop erboven: die haalt beelden op, deze
     zet een meetlaag aan. De statusregel ertussen is leeg zolang er niets is
     opgehaald, dus zonder deze scheiding lezen ze als één blok. */
  #solar-spots-toggle {
    margin-top: 12px; padding-top: 10px;
    border-top: 1px solid var(--hair);
  }
/* SOLAR:END */

  /* ---- De Details-tabs in deze state (sessie 34, Terry) ----

     Alleen Earth geldt hier: `environmentOff()` zet zon en maan uit en de
     planeten staan er niet, dus twee van de drie tabs openen op iets dat er
     niet is. Ze verdwijnen, en de legenda neemt hun plek in.

     OP HUN `data-tab` EN NIET OP EEN GEDEELDE KLASSE. Een `.msphere-hide` zou
     de volgende bewoner van die klasse stil meenemen — dezelfde val die dit
     bestand al een paar keer heeft opgeschreven. Dit noemt precies de twee. */
  body.msphere-on .detail-panel .tab[data-tab="d-sunmoon"],
  body.msphere-on .detail-panel .tab[data-tab="d-planets"] { display: none; }

  /* EEN TAB HEEFT TWEE EIGEN REGELS NODIG, EN DAT IS DE SPECIFICITEITSVAL.

     `.msphere-only { display: none }` weegt (0,1,0) en `.detail-panel .tab`
     weegt (0,2,0), dus die laatste WINT en zette de Legend-tab overal op
     `display: flex` — ook buiten deze state, waar hij niets verklaart. De
     verbergregel greep dus nooit, en dat viel niet op omdat ik alleen bínnen
     de magnetosfeer had gekeken.

     Beide standen staan daarom hier, allebei zwaar genoeg om te winnen:
     (0,3,0) om hem te verbergen, (0,3,1) om hem terug te zetten. En `flex` en
     geen `block`, want de algemene regel zou het label onder het icoon leggen
     in plaats van ernaast.

     Dit is dezelfde les die dit bestand al zeven keer heeft opgeschreven, maar
     één slag dieper: het gaat niet alleen om display in een KLASSE tegen
     `hidden`, maar om display in een klasse tegen een SPECIFIEKERE selector. */
  .detail-panel .tab.msphere-only { display: none; }
  body.msphere-on .detail-panel .tab.msphere-only { display: flex; }

  /* GEEN STREEP BOVEN DE EERSTE ZICHTBARE SECTIE (sessie 34, Terry).
     `.acc:first-child` haalt de streep weg bij de eerste sectie in de DOM —
     maar in deze state is dat er een die verborgen is, dus bleef er een lijn
     boven Magnetosphere staan met niets erboven. Op zijn `data-acc` en niet op
     `:first-of-type`: die telt de verborgen buren mee en gaat bij de volgende
     toevoeging stil verkeerd. */
  body.msphere-on .panel .acc[data-acc="msphere-layers"] { border-top: none; }

  /* De twee knoppen delen de kolom die in het eiland de klok-uitklapper draagt.
     Zelfde breedte-gedrag (auto), zodat de sliderbaan even lang blijft. */
  .mspt-knoppen {
    grid-area: more;
    display: flex;
    gap: 4px;
  }
  .mspt-rate, .mspt-window {
    font-variant-numeric: tabular-nums;
  }
  .mspt-rate { min-width: 46px; }
  .mspt-window { min-width: 30px; }

  /* ---- Het TERRA MODEL-venster (B4) ----
     De DOOS staat hier en het bufferattribuut in JS — zie de noot bij het
     canvas in index.html. `display: block` want een canvas is inline en zou
     anders een regelafstand onder zich krijgen die de flexbox meerekent. */
  .msphere-strip {
    display: block;
    width: 100%;
    height: var(--msps-h);
    /* DEZELFDE ONDERGROND ALS .ti-row, want het is dezelfde verdieping. Zonder
       achtergrond zweeft een grafiek van dunne lijnen rechtstreeks op de scene,
       en juist in deze state staat daar de magnetosfeer — lichte lijnen op
       lichte lijnen.

       EEN INGEZETTE RING EN GEEN `border`. Een rand telt mee in de doos, en de
       tekenmaat komt uit `getBoundingClientRect()`: twee pixels verschil tussen
       de doos en het tekenvlak zetten de hele registratie scheef. Een
       `inset box-shadow` neemt geen ruimte in en ziet er hetzelfde uit. */
    background: rgba(11,16,24,0.72);
    backdrop-filter: blur(14px);
    box-shadow: inset 0 0 0 1px var(--hair);
    border-radius: 12px;
    /* Aanwijzen mag, en dat is het halve nut: de captie leest per lane uit wat
       er onder de muis staat. */
    pointer-events: auto;
    touch-action: none;
  }

  /* DE BALK WORDT BREDER IN DEZE STATE. `max-width: 560px` is de maat van het
     tijd-eiland, waar één schuiver en een klok in staan. Zes lanes houden daar
     na de asgoot van chart.js (52 px) ~508 px plotbreedte over; zonder de cap
     loopt hij tot de panelen aan weerszijden en wordt dat ruim 800 op een
     scherm van 1440. Op mobiel staat `max-width: none` al. */
  body.msphere-on .msphere-time { max-width: none; }

  /* DE TIJDLIJN TELT NIET MEE VOOR WAAR EEN PANEEL STOPT (sessie 31, Terry).

     Alleen de BEDIENING wordt gereserveerd — de afspeelknoppen en de schuiver.
     Die moeten bereikbaar blijven terwijl een paneel openstaat, want dat is
     waarmee je door de reeks loopt. De tijdlijn erboven en het zwevende moment
     mogen bedekt worden: een uitlezing achter een paneel lees je toch niet, en
     op een telefoon is de ruimte er niet om hem vrij te houden.

     Dat staat in de mobiele media-query, bij `--panel-lift`, en het geldt voor
     élke state — de bediening is overal even hoog. Op desktop stopt een paneel
     toch al boven de knoppenrij en staat de balk er ruim naast. */

  /* ---- Toast-meldingen (nieuwe real-time entries) ---- */
  #toasts {
    position: fixed;
    top: 70px; left: 50%;
    transform: translateX(-50%);
    z-index: 40;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 8px;
    pointer-events: none;
  }
  .toast {
    display: flex;
    align-items: center;
    gap: 11px;
    background: rgba(11,16,24,0.92);
    backdrop-filter: blur(14px);
    border: 1px solid var(--hair);
    border-radius: 10px;
    padding: 10px 14px;
    min-width: 220px;
    box-shadow: 0 8px 28px rgba(0,0,0,0.5);
    transform: translateY(-16px);
    opacity: 0;
    transition: transform 0.4s cubic-bezier(.2,.9,.3,1), opacity 0.4s, border-color 0.18s;
    pointer-events: auto;
    cursor: pointer;
  }
  .toast:hover { border-color: var(--ink-dim); }
  .toast.show { transform: translateY(0); opacity: 1; }
  .toast .toast-dot {
    width: 9px; height: 9px;
    border-radius: 50%;
    flex-shrink: 0;
    box-shadow: 0 0 8px rgba(255,255,255,0.4);
  }
  .toast div { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
  .toast b { font-size: 12px; font-weight: 600; color: var(--ink); }
  .toast div span {
    font-size: 11px;
    color: var(--ink-dim);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    max-width: 240px;
  }

  /* Het eiland houdt zich aan de vrije ruimte tussen paneel en readout. Zodra die
     ruimte op raakt, gaan eerst de losse onderdelen eruit en pas daarna de breedte:
     het bereik onder de slider is de eerste die valt, want de slider zelf zegt al
     hoe ver hij reikt. */
  @media (max-width: 1100px) {
    /* HIER STOND `right: 44px`, met als reden "readout is hier meestal dicht". Dat
       gold tot sessie 26: sindsdien staat er rechtsonder een knoppenkolom die NOOIT
       dicht is, op dezelfde `--dock-bottom` als dit eiland. Op 1024 px liep het
       eiland er dwars doorheen. `right: 316px` is het desktopgetal en laat de
       kolom met ruimte over vrij; wat we hier winnen is alleen de `.ti-span`. */
    .ti-span { display: none; }
  }

  @media (max-width: 640px) {
    /* De knoppenrij schuift naar de rand, en alles wat erboven stopt schuift mee.
       Eén plek om te verzetten in plaats van vier — zie de noot bij :root.

       HIER WORDT DE ONDERSTE STAPEL GEZET (sessie 25). Het eiland is op deze
       breedte schermbreed en ligt dus wél in de weg, dus stopt een paneel niet
       boven de knoppenrij maar boven de TIJDRIJ. Van onder naar boven:
       knoppenrij (12..51) · tijdrij (61..133) · paneel (vanaf 143).

       EEN PANEEL STOPT HIER OP DE BEDIENING (sessie 31, Terry). Tot dan hield hij
       de hele tijdverdieping vrij: 40 (rij) + 6 + 28 voor het zwevende moment —
       dat is 20 px bij "now" en 28 zodra het verschoven is, want dan verschijnt
       er een resetknop (gemeten in sessie 26). In de magnetosfeer kwam daar de
       tijdlijn van 132 px bij, en dus 214.

       Dat kostte de halve scene op 375x812. Nu bedekt een paneel het moment en
       de tijdlijn en stopt hij op de rij eronder. De datum is dan niet zichtbaar
       zolang er een paneel openstaat; dat is de ruil, en de bediening waarmee je
       hem verzet ligt er nog gewoon onder. */
    :root {
      --dock-bottom: 12px;
      --dock-gap: 10px;
      /* ZES EN NIET TIEN (sessie 31, Terry: "iets beter aangesloten"). Dit is de
         ruimte tussen de tijdverdieping en de onderrand van een paneel, en zes
         is dezelfde maat die het zwevende moment van zijn eigen rij houdt. Eén
         ritme op die verdieping in plaats van twee. Geldt voor álle panelen,
         want ze hangen allemaal aan `--panel-lift`. */
      --time-gap: 6px;
      /* ALLE ONDERSTE VENSTERS LIJNEN UIT OP DE BEDIENING (sessie 31, Terry).

         Ze stopten boven de hele tijdverdieping, en dat kostte op 375 px de
         halve scene. Nu stoppen ze op de rand van de knoppenrij en bedekken ze
         wat daarboven staat: in de magnetosfeer de tijdlijn, overal het
         zwevende moment. "We nemen voor lief dat de datum dan niet zichtbaar
         is" — want een paneel is iets waar je even in leest, en de bediening
         waarmee je het venster verzet blijft eronder bereikbaar.

         Eén regel voor élke state: er is geen `body.msphere-on`-uitzondering
         meer nodig, want de bediening is overal even hoog. */
      --panel-lift: calc(var(--dock-lift) + var(--time-ctl-h) + var(--time-gap));
      /* Zes lanes op 132 px in plaats van 178: op 812 px hoog is de volle maat
         bijna een kwart van het scherm, en de scene is hier het onderwerp.
         `laneLayout` verdeelt naar gewicht, dus de verhoudingen blijven. */
      --msps-h: 132px;
      /* The sun's single lane, lowered in the same proportion. */
      --sols-h: 92px;
    }
    /* DE TIJDLIJN TELT WÉL MEE, en het moment staat er BOVEN (sessie 31, Terry).

       Twee dingen die elkaar oplossen. Het zwevende moment lag over de onderste
       lane en de tijdas — je las Kp niet meer en wist niet welk uur je zag. En
       een paneel dat de tijdlijn half bedekte, eindigde midden in een grafiek.

       Nu staat het moment BOVEN de hele balk en stopt een paneel daar weer
       boven. De tijdlijn blijft daarmee volledig leesbaar terwijl een paneel
       openstaat, en de onderrand van dat paneel valt samen met een echte rand
       in plaats van ergens in een lane.

       De prijs is de 140 px paneelhoogte die de vorige stap opleverde. Dat is
       een bewuste ruil: een tijdlijn die je half ziet is geen tijdlijn.

       LET OP als je hier ooit een state-eigen `--panel-lift` bij zet: een custom
       property wordt opgelost op het element waar hij GEDECLAREERD staat. Een
       override op <body> bereikt een afgeleide in `:root` dus niet, en dat is
       niet in de code te zien — alleen op het scherm. Gemeten toen die val
       toesloeg: het paneel lag er 93 px overheen. */
    /* HET MOMENT KLIMT OVER DE TIJDLIJN HEEN. Hij hangt absoluut aan `.ti-row`
       (zie de noot daar: uit de flow, zodat zijn breedte de sliderbaan niet
       inkort), dus optellen bij `bottom` is genoeg — de 6 px die hij van de rij
       hield, plus de hele balk erboven. */
    body.msphere-on .ti-now {
      bottom: calc(100% + 6px + var(--msps-gap) + var(--msps-h));
    }
    body.solar-on .ti-now {
      bottom: calc(100% + 6px + var(--sols-gap) + var(--sols-h) + var(--sols-gap) + var(--sols-week-h));
    }
    /* Volle breedte, en GEEN transform die opgeheven moet worden — zie de noot bij
       .time-island. `--dock-lift` en niet `--panel-lift`: het eiland stopt boven de
       KNOPPENRIJ, de panelen stoppen boven het eiland. Tot sessie 25 waren die twee
       hetzelfde getal, en dat kon alleen omdat het eiland verdween zodra er een
       paneel openging. */
    .time-island, .msphere-time, .solar-time {
      left: 10px; right: 10px; max-width: none;
      bottom: calc(var(--dock-lift) + env(safe-area-inset-bottom, 0px));
    }
    /* HET MOMENT ZWEEFT BOVEN DE BAAN (sessie 26, Terry). Het stond hier in een
       vaste kolom van 126 px, en dat was uitgerekend: het langste moment
       ("31 Dec 2026, 23:59") meet 100 px op 10 px met tabular-nums, plus 20 px
       resetknop en 6 px tussenruimte. Wat de slider overhield was 78 px. Dat is
       geen slider meer — het is een streepje met een knop erop.

       Het label uit het grid halen wint 126 + 7 = 133 px, en de baan gaat daarmee
       naar ruim 200 px. `position: absolute` doet dat beter dan een tweede rij:
       het label neemt dan helemaal geen hoogte in de rij in, en de drie
       klikdoelen (afspelen, baan, klok) houden hun volle 40 px.

       EN DE SLEEP-BUG VAN SESSIE 20 IS HIERMEE DOOD, niet omzeild. Die kwam
       doordat de breedte van het label bepaalde hoeveel baan er overbleef: het
       groeit van 22 px ("now") tot 145 px, dus de slider sprong tijdens het
       slepen en de knop schoof onder de vinger weg. Daar hielp een vaste kolom
       tegen; uit de flow halen is sterker, want er ís geen inhoud meer die de
       sliderkolom kan duwen. Op desktop blijft de kolom van 190 px staan, en
       daarmee ook de bescherming daar.

       EIGEN GLASPLAATJE, want het label ligt nu over de bol in plaats van op de
       rij. Zonder achtergrond is het onleesbaar boven een lichte oceaan. */
    .ti-row {
      position: relative;
      grid-template-columns: auto minmax(0, 1fr) auto;
      grid-template-areas: "play slider more";
      gap: 7px;
    }
    /* `grid-area: auto` is niet overbodig: `now` bestaat niet meer in
       `grid-template-areas` hierboven, en een absolute doos met een gebiedsnaam
       die nergens op slaat laat de browser kiezen waar hij hem ophangt. */
    .ti-now {
      grid-area: auto;
      position: absolute;
      bottom: calc(100% + 6px);
      right: 0;
      /* Buiten de flow begrenst niets de breedte meer, en zonder grens doet de
         `text-overflow: ellipsis` op .ti-val niets. */
      max-width: calc(100% - 12px);
      padding: 3px 8px;
      background: rgba(11,16,24,0.72);
      backdrop-filter: blur(14px);
      border: 1px solid var(--hair);
      border-radius: 10px;
    }
    .ti-val { font-size: 10px; }

    .panel, .detail-panel {
      left: 10px; right: 10px; width: auto;
      /* Boven de knoppenrij. De home-indicator van een iPhone is 20 px hoog en
         zat over de onderrand van het paneel heen; `env()` valt terug op 0 waar
         het niet bestaat. */
      bottom: calc(var(--panel-lift) + env(safe-area-inset-bottom, 0px));
      max-height: 62vh;
      max-height: 62dvh;
    }
    .panel-head { padding: 12px 12px 10px; }
    .panel-body { padding: 14px 16px 16px; }
    /* Was tot 2026-07-31 `display: none`. De klik op een label wérkte, maar er
       verscheen niets, en dat las als "aanklikken doet niks". Nu een bottom sheet:
       het venster komt van onderen op over de volle breedte.

       HET RAAKT DE ONDERRAND NIET MEER (sessie 25). Het stond op `bottom: 0` met
       een afgeknipte onderrand, en dat is wat `setDockVisible(false)` nodig maakte:
       het venster lag over de knoppenrij heen, dus moest die weg. Nu stopt het op
       `--panel-lift`, boven de tijdrij, en is de rij gewoon bereikbaar terwijl je
       leest. Daarmee komen ook de volledige afronding en de onderrand terug — die
       hoorden bij een sheet die tegen de schermrand plakt, en dat doet hij niet
       meer. Het paneel hoeft er niet meer voor te wijken; dit venster ligt in de
       z-ladder erbovenop. */
    .readout {
      /* 10 px inspringing, gelijk aan .panel, .qlist en .notif-panel: nu de hoeken
         weer helemaal rond zijn, zouden ze op 0 tegen de schermrand aan liggen. */
      left: 10px; right: 10px; width: auto;
      bottom: calc(var(--panel-lift) + env(safe-area-inset-bottom, 0px));
      padding: 18px;
      max-height: 60vh;
      max-height: 60dvh;
      overflow-y: auto;
      overscroll-behavior: contain;
      touch-action: pan-y;   /* scrollen mag, knijpen niet */
    }
    /* Grotere raakvlakken: 28 px is met een duim aan de rand net te weinig. */
    .readout-close { top: 10px; right: 10px; width: 34px; height: 34px; }
    /* Zelfde reden voor het tijdpaneel; de uitleg eronder houdt navenant meer
       ruimte vrij. */
    .ti-close { top: 4px; right: 5px; width: 32px; height: 32px; }
    .ti-note { padding-right: 30px; }
    .masthead { padding: 12px 14px; --masthead-gap: 10px; }
    .tagline { display: none; }
    /* Op smalle schermen alleen de iconen. Beide opties blijven zichtbaar; dat is juist
       de reden dat het een segmented toggle is. */
    .mode-opt .mode-text { display: none; }
    .view-switch .mode-text { display: inline; }
    /* Alleen hier: op een breed scherm staat de uitlezing naast de bol en
       hoeft er niets weg te klappen. */
    .antipode-exit { display: flex; }
    .antipode-exit[hidden] { display: none; }

    /* DE CAMERASTAND HOUDT ZIJN TEKST, en dat is geen uitzondering maar het
       verschil tussen de twee bewoners van deze gleuf. De bolweergave heeft
       ICONEN (◐ en ▦) en kan zijn tekst dus missen; een camerastand heeft er
       geen, want Meridian en Top zijn allebei "recht op een vlak" en alleen het
       woord zegt op wélk vlak. Zonder deze regel staan er drie lege pillen —
       gemeten: 20 px per knop, en niets erin.

       Wat wél korter mag is het label dat te lang is: "3D orbit" wordt "3D".
       Gemeten op 375 px, met deze maten: de magnetosfeer komt op 122 px en
       Space op 137, tegen 145 px vrije ruimte tussen de wordmark en de knoppen
       rechts. */
    /* DE CAMERASTAND ZAKT EEN RIJ, en houdt daarmee zijn tekst.

       De bolweergave kan in de mastheadrij blijven omdat hij ICONEN heeft (◐ en
       ▦) en zijn tekst daar dus weglaat. Een camerastand heeft er geen: Meridian
       en Top zijn allebei "recht op een vlak" en alleen het woord zegt op wélk
       vlak. Zonder tekst staan er drie lege pillen — gemeten: 20 px per knop en
       niets erin.

       En met tekst past hij daar niet. GEMETEN op 375 px: tussen de wordmark
       (eindigt op 115) en de knoppen rechts (beginnen op 260) is 145 px vrij, en
       de rij meet 147 in de magnetosfeer en 171 in Space — zelfs met "3D orbit"
       ingekort tot "3D". Een rij lager is er 355.

       56 px is dezelfde lijn waarop de meldingen, de bevingenlijst en de
       instellingen opengaan; die bedekken hem dan, en dat mag — het zijn
       tijdelijke vensters. */
    /* TWEE KLASSEN, want `.mode-switch { top: 12px }` staat verderop in deze
       media-query en heeft dezelfde soortelijkheid — dan wint de laatste. Dit
       element draagt beide klassen, dus `.mode-switch.view-switch` wint hoe de
       volgorde ook loopt. */
    .mode-switch.view-switch { top: 56px; }
    .mode-opt { padding: 6px 10px; }
    /* Op één lijn met de wordmark en de bel. Stond tot 2026-07-31 op 48 px, onder de
       masthead, omdat het eiland toen nog tekstlabels droeg en op 375 px tegen de
       wordmark aan liep. Met alleen de iconen is het nog 76 px breed en is er tussen
       wordmark en bel 211 px vrij: ruim genoeg, en het scheelt een verdieping. */
    .mode-switch { top: 12px; }
    .dock { left: 12px; }
    .dock-right { left: auto; right: 12px; }
    /* BACK TO EARTH STAAT IN HET MIDDEN (Terry, sessie 52).

       Hij is het eerste kind van de rechterhoek, en op een breed scherm valt dat
       niemand op. Op 375 px raken de twee hoeken elkaar bijna en leest de rij als
       één balk — dan staat de enige uitweg zichtbaar scheef: gemeten 195,5 tegen
       een schermmidden van 187,5, doordat de gaten links en rechts van de knop
       24 en 8 px zijn.

       `left: 0; right: 0; margin: 0 auto` en GEEN transform: de noot bij .dock
       waarschuwt daarvoor, en terecht — een transform die een media query
       overleeft schuift het element een halve schermbreedte weg. Met `width:
       max-content` blijft de doos zo breed als zijn label. */
    .dock-back {
      position: fixed;
      left: 0; right: 0;
      bottom: var(--dock-bottom);
      margin: 0 auto;
      width: max-content;
    }
    /* GEMETEN op 375x812, slechtste stand (Space aan + Layers open): de rij werd
       363 px breed in een beeld van 375, en de knoppen werden daarbij al
       samengeknepen — 140 px viel terug naar 131. Op een 360 px scherm loopt hij
       eroverheen. Het label op de OPEN knop vervalt daarom hier; welk paneel open
       staat blijft zichtbaar aan de accentrand, én aan het paneel zelf.
       `Back to Earth` houdt zijn label wel: die is geen schakelaar maar een
       handeling, en zonder tekst is een pijltje niet te raden.

       Sinds de splitsing van sessie 26 is die 363 px verdeeld over twee hoeken —
       de klem hieronder blijft staan, want de rechterkolom draagt in zijn zwaarste
       stand nog altijd een label plus een knop. */
    .dock-btn.active .dock-label { display: none; }
    .nav-panel {
      left: 12px; right: 12px; width: auto;
      bottom: calc(var(--panel-lift) + env(safe-area-inset-bottom, 0px));
      max-height: 56dvh;
    }
    .notif-panel { left: 10px; right: 10px; width: auto; top: 56px; max-height: 60vh; }
    /* Zelfde behandeling; de instellingen zijn langer, dus iets meer hoogte. */
    .settings-panel { left: 10px; right: 10px; width: auto; top: 56px; max-height: 70vh; max-height: 70dvh; }
    /* `transform: none` is hier GEEN opsmuk. Op desktop staat #toasts op
       `left: 50%` met `translateX(-50%)` als centrering. Zet je dan links én
       rechts vast zonder die transform op te heffen, dan wordt het element eerst
       schermbreed en schuift het dáárna nog eens de halve schermbreedte naar
       links: de meldingen vielen half buiten beeld (gezien op iPhone, portret,
       2026-08-01).
       Zelfde val als bij .mode-switch, waar translateX(-50%) óók de centrering
       zelf is. Regel: verandert een media query `left`/`right`, controleer dan of
       er nog een transform onder ligt. */
    #toasts { left: 10px; right: 10px; top: 56px; transform: none; }
    .toast { min-width: 0; width: 100%; }
    /* Volledige breedte, als bottom sheet — hier past niet twee dingen naast
       elkaar. Openen sluit daarom het linkerpaneel, net als de readout doet. */
    .qlist {
      left: 10px; right: 10px; width: auto;
      top: 56px;
      max-height: 58vh;
      max-height: 58dvh;
    }
  }

  /* ---- Lage schermen: telefoon in landscape ----------------------------------
     Tot 2026-07-31 had dit stylesheet maar één media query, en die keek naar
     bréédte. Een telefoon in landscape is ongeveer 750 px breed en viel er dus
     buiten: die kreeg de volledige desktoplayout op 292 px hoogte.

     Alle bediening van de app zit in de bovenste 76 px (weergavewissel op y 16,
     notificatiebel op 18, tabs op 47, inklappijl op 46). Het paneel groeide daar
     overheen. Deze query zet het paneel onder die strook, zodat het niet meer met
     de bediening kan overlappen hoe laag het scherm ook wordt. */
  @media (max-height: 480px) {
    /* Ook hier moet alles boven de knoppenrij stoppen. De rij zelf mag dichter
       bij de rand: op 292 px hoogte telt elke pixel, en 28 px onderaan is dan
       een derde van wat het paneel overhoudt. */
    :root { --dock-bottom: 12px; --dock-gap: 8px; }
    .panel, .detail-panel {
      top: 92px;
      /* GEEN 12 px meer (sessie 24): dat is de plek van de knoppenrij, en het
         paneel liep er tot dan toe achter door. `--panel-lift` rekent de rij
         plus zijn tussenruimte erbij — zie :root. */
      bottom: var(--panel-lift);
      /* top én bottom samen bepalen nu de hoogte; een max-height zou daar
         overheen gaan en het paneel opnieuw laten groeien */
      max-height: none;
    }
    .nav-panel { bottom: var(--panel-lift); max-height: calc(100dvh - 150px); }
    /* De ondertitel kost een regel die er in landscape niet is. */
    .tagline { display: none; }
    .masthead { padding: 12px 22px; }
    /* Het detailvenster mag op een laag scherm niet buiten beeld groeien. */
    .readout {
      max-height: calc(100dvh - 120px);
      overflow-y: auto;
      overscroll-behavior: contain;
    }
    .notif-panel { max-height: calc(100dvh - 120px); }
    .settings-panel { top: 92px; max-height: calc(100dvh - 104px); }
    /* Landscape: 58dvh is hier 170px en dus onbruikbaar. Net als bij het paneel
       bepalen top en bottom samen de hoogte, onder de bedieningsstrook door. */
    .qlist {
      top: 92px;
      /* GEEN 12 px meer (sessie 25): dat is precies de plek van de knoppenrij, en
         de lijst liep er tot dan toe achter door. Dat werd gemaskeerd doordat
         `setQuakeListOpen()` de rij wegzette; die functie is er niet meer. */
      bottom: var(--panel-lift);
      max-height: none;
    }
  }

  /* ---- Eigen texturen (Settings > Earth textures) ---- */
  .ctex { display: flex; flex-direction: column; gap: 6px; margin-bottom: 10px; }
  .ctex-slot {
    display: grid;
    grid-template-columns: auto 1fr auto auto;
    align-items: center; gap: 8px;
    padding: 7px 10px;
    border: 1px solid var(--hair); border-radius: 8px;
  }
  .ctex-label { font-size: 12px; font-weight: 600; color: var(--ink); white-space: nowrap; }
  /* De bestandsnaam mag krimpen, de rest niet — anders duwt een lange naam de
     knoppen uit de rij. */
  .ctex-value {
    min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
    text-align: right; font-size: 11px; color: var(--ink-faint);
  }
  .ctex-value.custom { color: var(--accent); }
  .ctex-pick, .ctex-clear {
    background: transparent; border: 1px solid var(--hair); border-radius: 6px;
    color: var(--ink-dim); font-family: inherit; cursor: pointer;
    transition: color 0.15s, border-color 0.15s;
  }
  .ctex-pick { padding: 4px 9px; font-size: 11px; white-space: nowrap; }
  .ctex-pick:hover:not(:disabled) { border-color: var(--ink-dim); color: var(--ink); }
  .ctex-pick:disabled { opacity: 0.4; cursor: default; }
  .ctex-clear { width: 22px; height: 22px; padding: 0; font-size: 14px; line-height: 1; }
  .ctex-clear:hover { border-color: var(--quake); color: var(--quake); }
  /* Een expliciete regel naast de eigen display-declaraties: `hidden` is niets
     anders dan een user-agent-regel en verliest daarvan (sessie 14). */
  .ctex-clear[hidden], .ctex-reset[hidden] { display: none; }
  .ctex-reset { margin-bottom: 10px; }
  .ctex-note { font-size: 11px; line-height: 1.5; color: var(--ink-faint); }
  @media (prefers-reduced-motion: reduce) { .ctex-pick, .ctex-clear { transition: none; } }

  /* SKETCH:START — tekenlaag. Dit hele blok wordt uit de standalone gestript;
     zie tools/build-standalone.mjs. */

  /* ---- Sketch-tab (in het paneel) ---- */
  .sk-lead {
    font-size: 11.5px; line-height: 1.55; color: var(--ink-dim); margin-bottom: 12px;
  }
  .sk-note {
    font-size: 11px; line-height: 1.5; color: var(--ink-faint); margin-top: 10px;
  }
  .sk-stat {
    display: flex; justify-content: space-between; gap: 14px;
    font-size: 11.5px; color: var(--ink-dim); padding: 4px 0;
  }
  .sk-stat b { color: var(--ink); font-weight: 500; font-variant-numeric: tabular-nums; }
  /* Terugkoppeling op de knop waarop je drukte. `[hidden]` heeft hier een eigen
     regel nodig omdat de display-declaratie eronder anders wint (sessie 14). */
  .sk-msg[hidden] { display: none; }
  .sk-msg {
    display: block; margin-top: 10px; padding: 8px 10px;
    background: rgba(255,107,61,0.08); border: 1px solid var(--hair); border-radius: 6px;
    font-size: 11px; line-height: 1.5; color: var(--ink-dim);
  }
  .sk-buttons { display: flex; gap: 8px; }
  .sk-buttons .sk-btn { flex: 1 1 0; }

  /* Knop in Terra's stijl. Deelt de vorm met `.apply` maar niet de volle breedte:
     in de editor staan er drie naast elkaar in een balk. */
  .sk-btn {
    background: transparent;
    border: 1px solid var(--hair);
    border-radius: 8px;
    padding: 7px 12px;
    color: var(--ink-dim);
    font-family: inherit; font-size: 12px; font-weight: 500;
    cursor: pointer; white-space: nowrap;
    transition: color 0.15s, border-color 0.15s, background 0.15s;
  }
  .sk-btn:hover { border-color: var(--ink-dim); color: var(--ink); }
  .sk-btn.wide { width: 100%; margin-top: 10px; }
  .sk-btn.primary { border-color: var(--accent); color: var(--accent); }
  .sk-btn.primary:hover { background: rgba(255,107,61,0.12); }
  .sk-btn.danger:hover { border-color: var(--quake); color: var(--quake); background: rgba(255,107,61,0.08); }
  @media (prefers-reduced-motion: reduce) { .sk-btn { transition: none; } }

  /* ---- Bewerkscherm ---- */
  /* Het `hidden`-attribuut is niets anders dan een user-agent-regel
     `display: none`, en de eigen `display: flex` hieronder wint daarvan. Zonder
     deze regel staat de editor dus meteen open — exact de val uit sessie 14. */
  .sketch-editor[hidden] { display: none; }
  .sketch-editor {
    position: fixed; inset: 0; z-index: 60;
    display: flex; background: var(--void);
  }
  .sk-tools {
    flex: 0 0 52px;
    display: flex; flex-direction: column; align-items: center; gap: 4px;
    padding: 10px 0;
    background: var(--void-2); border-right: 1px solid var(--hair);
  }
  .sk-tools .tool {
    width: 36px; height: 36px;
    display: grid; place-items: center;
    background: transparent; border: 1px solid transparent; border-radius: 6px;
    color: var(--ink-dim); cursor: pointer;
  }
  .sk-tools .tool:hover { background: rgba(255,255,255,0.06); color: var(--ink); }
  .sk-tools .tool[aria-pressed="true"] {
    color: var(--accent); border-color: var(--accent); background: rgba(255,107,61,0.10);
  }
  .sk-tools .tool svg {
    width: 19px; height: 19px;
    fill: none; stroke: currentColor; stroke-width: 1.6;
    stroke-linecap: round; stroke-linejoin: round;
  }
  .sk-tools .tool-sep { width: 22px; height: 1px; background: var(--hair); margin: 6px 0; }

  .sk-stage { flex: 1; position: relative; min-width: 0; }
  /* `touch-action: none` mag hier omdat er op het canvas niets te scrollen valt.
     Op een voorouder zou het het scrollen in het paneel breken (sessie 9). */
  #sketch-map {
    width: 100%; height: 100%; display: block;
    touch-action: none; cursor: crosshair;
  }
  #sketch-map.tool-select { cursor: default; }
  #sketch-map.panning { cursor: grabbing; }

  .sk-bar {
    position: absolute; left: 0; right: 0; top: 0;
    display: flex; align-items: center; gap: 10px;
    height: 52px; padding: 0 14px;
    background: linear-gradient(var(--void), rgba(5,7,13,0));
    pointer-events: none;
  }
  .sk-bar > * { pointer-events: auto; }
  .sk-bar-note { margin-right: auto; font-size: 11px; color: var(--ink-faint); }

  .sk-toggle {
    display: inline-flex; align-items: center; gap: 7px;
    font-size: 11.5px; color: var(--ink-dim); cursor: pointer;
    -webkit-user-select: none; user-select: none;
    /* Zonder dit breekt "Dim map" over twee regels zodra de balk krap wordt, en
       dan groeit de balk in hoogte in plaats van dat er iets wijkt. */
    white-space: nowrap;
  }
  .sk-toggle input {
    -webkit-appearance: none; appearance: none; margin: 0;
    width: 30px; height: 16px; border-radius: 9px;
    background: var(--void-2); border: 1px solid var(--hair);
    position: relative; cursor: pointer;
    transition: background 0.15s, border-color 0.15s;
  }
  .sk-toggle input::after {
    content: ""; position: absolute; top: 2px; left: 2px;
    width: 10px; height: 10px; border-radius: 50%;
    background: var(--ink-dim); transition: transform 0.15s, background 0.15s;
  }
  .sk-toggle input:checked { border-color: var(--accent); background: rgba(255,107,61,0.15); }
  .sk-toggle input:checked::after { transform: translateX(14px); background: var(--accent); }
  @media (prefers-reduced-motion: reduce) {
    .sk-toggle input, .sk-toggle input::after { transition: none; }
  }

  .sk-coords {
    position: absolute; left: 14px; bottom: 14px;
    padding: 10px 14px;
    background: rgba(11,16,24,0.82); border: 1px solid var(--hair); border-radius: 8px;
    backdrop-filter: blur(6px);
    font-variant-numeric: tabular-nums;
  }
  .sk-coords .lbl {
    display: block; margin-bottom: 4px;
    font-size: 9.5px; letter-spacing: 0.16em; text-transform: uppercase; color: var(--accent);
  }
  .sk-coords .val { font-size: 15px; color: var(--ink); }
  .sk-coords .val i { color: var(--ink-dim); font-style: normal; }

  .sk-inspector {
    flex: 0 0 232px;
    padding: 14px; overflow-y: auto;
    background: var(--void-2); border-left: 1px solid var(--hair);
  }
  .sk-inspector h2 {
    margin-bottom: 12px;
    font-size: 10px; font-weight: 500; letter-spacing: 0.16em;
    text-transform: uppercase; color: var(--accent);
  }
  .sk-field { margin-bottom: 13px; }
  .sk-field > label {
    display: block; margin-bottom: 6px;
    font-size: 9.5px; letter-spacing: 0.14em; text-transform: uppercase; color: var(--ink-dim);
  }
  .sk-row { display: flex; align-items: center; gap: 9px; }
  .sk-row .range { flex: 1; }
  .sk-row output {
    min-width: 40px; text-align: right;
    font-size: 11px; color: var(--ink); font-variant-numeric: tabular-nums;
  }
  .sk-inspector input[type="color"] {
    width: 100%; height: 30px; padding: 2px;
    background: var(--void); border: 1px solid var(--hair); border-radius: 6px; cursor: pointer;
  }
  .sk-swatches { display: flex; gap: 5px; margin-top: 6px; }
  .sk-swatch {
    width: 22px; height: 22px; padding: 0;
    border: 1px solid rgba(255,255,255,0.18); border-radius: 4px; cursor: pointer;
  }
  .sk-swatch[aria-pressed="true"] {
    box-shadow: 0 0 0 2px var(--void-2), 0 0 0 3px var(--accent);
  }
  .sk-empty { font-size: 11.5px; line-height: 1.55; color: var(--ink-dim); }
  .sk-empty code { font-size: 11px; color: var(--accent); }
  .sk-stack {
    display: flex; flex-direction: column; gap: 6px;
    margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--hair);
  }
  .sk-keys {
    margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--hair);
    font-size: 10.5px; line-height: 1.9; color: var(--ink-faint);
  }
  .sk-keys b {
    display: inline-block; min-width: 58px;
    color: var(--ink-dim); font-weight: 500;
  }

  /* Op een smal scherm gaat de inspector zweven: de kaart heeft de breedte harder
     nodig dan de bediening, en anders houdt het canvas een strook van 100px over. */
  @media (max-width: 820px) {
    .sk-inspector {
      position: absolute; right: 0; top: 0; bottom: 0; z-index: 5;
      box-shadow: -14px 0 30px rgba(0,0,0,0.5);
    }
    .sk-coords { left: auto; right: 246px; }
    .sk-bar { flex-wrap: wrap; height: auto; padding: 10px 14px; }
    .sk-bar-note { display: none; }
  }
  /* Tekenen alleen op een groot liggend scherm; smaller dekt de inspector de
     sluitknoppen van de editor af. Omgekeerde vorm van SKETCH_VIEWPORT in
     index.html — wijzig ze samen. */
  @media (max-width: 1023px), (orientation: portrait) {
    .panel .tab[data-tab="sketch"],
    .panel .tabpanel[data-panel="sketch"],
    .sketch-editor { display: none !important; }
    /* DE TABRIJ BLIJFT HIER SINDS SESSIE 38. Tot dan bleef er met de tekentab
       verborgen precies ÉÉN tab over en was de rij ruis; sinds de splitsing zijn
       het er twee — Filters en Layers — en dat is geen ruis maar navigatie. De
       paneeltitel gaat juist wél weg, want de actieve tab zegt nu waar je bent. */
  }
  /* TEKENEN HOORT BIJ DE AARDE ALS KAART (sessie 36, Terry).

     Buiten de aarde is er geen kaart om op te tekenen: de magnetosfeer toont een
     doorsnede door een veld, Space een stelsel op ware afstand, en de zon een
     projectie van instrumentbeelden. De tab verklaart daar niets en is er dus
     niet — dezelfde redenering als de
     Legend-tab die alleen BINNEN de magnetosfeer staat.

     DIT IS DE HELFT DIE CSS DOET. De andere helft staat in index.html en klikt de
     actieve tab weg. Zonder die tweede helft houdt het tabpanel zijn `.show`
     terwijl zijn knop verdwenen is, en dat is precies de bug die deze sessie
     opende: in de magnetosfeer gaat de hele tabrij uit (zie #panel-tabs hierboven),
     dus wie hier stond zag één paneel zonder enige weg terug.

     GEEN `!important`, en dat is gemeten en geen gok. `.panel .tab` weegt (0,2,0)
     en `.tabpanel.show` ook; deze regels wegen (0,4,1) en winnen dus op eigen
     kracht. De buurregel hierboven heeft de vlag wél nodig omdat een mediaquery
     niets aan gewicht toevoegt. Dit is dezelfde val die commit 3232ed1 moest
     repareren bij de Legend-tab — daar won `.detail-panel .tab` van een kale
     klasse en stond de tab overal. */
  body.msphere-on .panel .tab[data-tab="sketch"],
  body.space-on .panel .tab[data-tab="sketch"],
  body.solar-on .panel .tab[data-tab="sketch"],
  body.msphere-on .panel .tabpanel[data-panel="sketch"],
  body.space-on .panel .tabpanel[data-panel="sketch"],
  body.solar-on .panel .tabpanel[data-panel="sketch"],
  /* Het bewerkscherm gaat mee, net als in de mediaquery hierboven. De JS sluit hem
     netjes met behoud van werk; dit is de vangrail eronder voor het geval die weg
     een keer niet loopt. */
  body.msphere-on .sketch-editor,
  body.space-on .sketch-editor,
  body.solar-on .sketch-editor { display: none; }
  /* SKETCH:END */

  /* ---- Het welkomstscherm (sessie 44) --------------------------------------
     Het eerste wat een nieuwe bezoeker ziet, en het enige vlak in Terra dat de
     scene volledig afdekt. Dat mag hier: er valt nog niets te zien waar de
     keuze niet overheen gaat, want de zware textuur wordt pas gehaald nadat
     dit scherm dicht is.

     GEEN `backdrop-filter` OP DE LAAG ZELF. Die staat elders in dit bestand op
     panelen van 300 px; hier zou hij over het hele scherm gaan, en dat is een
     blur over de volle bol op het moment dat de GPU net de aarde staat op te
     bouwen. Een ondoorzichtige laag kost niets en leest rustiger. */
  .ob {
    position: fixed;
    inset: 0;
    z-index: var(--z-onboard);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 20px;
    background: rgba(5, 7, 13, 0.92);
    transition: opacity 0.35s;
  }
  .ob.done { opacity: 0; pointer-events: none; }

  .ob-card {
    position: relative;
    width: 100%;
    max-width: 520px;
    max-height: 100%;
    overflow-y: auto;
    padding: 30px 30px 24px;
    background: var(--void-2);
    border: 1px solid var(--hair);
    border-radius: 14px;
  }

  .ob-close {
    position: absolute;
    top: 10px; right: 12px;
    background: none; border: none;
    font: inherit; font-size: 19px; line-height: 1;
    color: var(--ink-faint);
    cursor: pointer;
    padding: 4px 6px;
  }
  .ob-close:hover { color: var(--ink); }

  /* Dezelfde vormtaal als de kop van de app: gespatieerd, klein, hoofdletters. */
  .ob-title {
    margin: 0 0 10px;
    font-size: 13px;
    font-weight: 600;
    letter-spacing: 0.2em;
    text-transform: uppercase;
    color: var(--ink);
  }
  .ob-lead {
    margin: 0 0 20px;
    font-size: 13px;
    line-height: 1.6;
    color: var(--ink-dim);
  }

  /* DE DRIE TRAPPEN, in kolom en niet in een rij van drie. In Settings staan
     ze naast elkaar omdat daar alleen de naam en de maat op de knop passen;
     hier draagt elke knop ook de zin die zegt wat je krijgt, en drie van die
     zinnen naast elkaar worden drie smalle kolommen tekst. */
  .ob-tiers { display: flex; flex-direction: column; gap: 8px; }
  .ob-tier {
    display: flex;
    flex-direction: column;
    gap: 3px;
    width: 100%;
    text-align: left;
    background: transparent;
    border: 1px solid var(--hair);
    border-radius: 10px;
    padding: 11px 13px;
    color: var(--ink-dim);
    font-family: inherit;
    cursor: pointer;
    transition: border-color 0.15s, background 0.15s, color 0.15s;
  }
  .ob-tier:hover:not(:disabled) { border-color: var(--ink-faint); color: var(--ink); }
  .ob-tier.on {
    background: rgba(255, 255, 255, 0.05);
    border-color: var(--accent);
    color: var(--ink);
  }
  .ob-tier.off { opacity: 0.45; cursor: not-allowed; }
  .ob-tier-name { font-size: 13px; font-weight: 500; }
  .ob-tier-meta {
    font-size: 10px;
    letter-spacing: 0.06em;
    font-variant-numeric: tabular-nums;
    color: var(--accent);
  }
  .ob-tier.off .ob-tier-meta { color: var(--ink-faint); }
  .ob-tier-note { font-size: 11px; line-height: 1.5; color: var(--ink-faint); }

  /* De reden onder de rij, niet erboven: eerst wat je kunt kiezen, dan waarom
     er al iets aanstaat. Andersom leest het als een mededeling waar je nog
     doorheen moet voordat de keuze in beeld komt. */
  .ob-reason {
    margin: 11px 0 0;
    font-size: 11px;
    line-height: 1.5;
    color: var(--ink-faint);
  }

  .ob-row {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    margin-top: 20px;
    padding-top: 16px;
    border-top: 1px solid var(--hair);
  }
  .ob-row-label { font-size: 12px; color: var(--ink-dim); }
  .ob-seg {
    display: flex;
    border: 1px solid var(--hair);
    border-radius: 8px;
    overflow: hidden;
  }
  .ob-seg-opt {
    background: transparent;
    border: none;
    padding: 6px 14px;
    font: inherit;
    font-size: 11px;
    color: var(--ink-faint);
    cursor: pointer;
    transition: background 0.15s, color 0.15s;
  }
  .ob-seg-opt:hover { color: var(--ink); }
  .ob-seg-opt.on { background: rgba(255, 255, 255, 0.07); color: var(--ink); }

  .ob-acts {
    display: flex;
    flex-direction: column;
    gap: 8px;
    margin-top: 22px;
  }
  .ob-btn {
    padding: 11px 16px;
    background: transparent;
    border: 1px solid var(--hair);
    border-radius: 9px;
    font: inherit;
    font-size: 12px;
    letter-spacing: 0.04em;
    color: var(--ink-dim);
    cursor: pointer;
    transition: border-color 0.15s, color 0.15s, background 0.15s;
  }
  .ob-btn:hover { border-color: var(--ink-faint); color: var(--ink); }
  /* DE AANBEVOLEN KNOP DRAAGT DE ACCENTKLEUR EN STAAT BOVENAAN. Hij is de
     belangrijkste van het scherm: hij laat iemand die niet over megabytes wil
     nadenken weglopen zonder over megabytes na te denken. */
  .ob-btn-primary {
    background: var(--accent);
    border-color: var(--accent);
    color: var(--void);
    font-weight: 600;
  }
  .ob-btn-primary:hover { filter: brightness(1.1); color: var(--void); }

  .ob-foot {
    margin: 14px 0 0;
    font-size: 10px;
    text-align: center;
    color: var(--ink-faint);
  }

  /* Op een telefoon vult de kaart de breedte en verliest hij zijn ronde hoeken
     niet, maar wel zijn ruime binnenmarge: 30 px aan weerszijden van 375 laat
     315 over voor tekst die op drie regels moet passen. */
  @media (max-width: 480px) {
    .ob { padding: 12px; }
    .ob-card { padding: 24px 18px 18px; }
    .ob-tier-note { font-size: 10px; }
  }

  /* Minder beweging gevraagd: geen uitvloeier, het scherm is er of het is weg. */
  @media (prefers-reduced-motion: reduce) {
    .ob { transition: none; }
    .ob-tier, .ob-btn, .ob-seg-opt { transition: none; }
  }

  /* ---- De rondleiding (sessie 44) ------------------------------------------
     Zes stops op de eerste bezoekbeurt, en daarna nooit meer. Hij wijst alleen:
     er gaat geen paneel open en er verandert geen laag.

     DE UITSNEDE IS ÉÉN `box-shadow`, GEEN MASKER. Een div op de plek van de
     bediening met een schaduw die de rest van het scherm dekt — het gat is het
     element zelf. Een SVG-masker of een clip-path zou hetzelfde beeld geven en
     bij elke stap opnieuw geparseerd moeten worden; dit verplaatst vier
     getallen.

     De schaduw vangt zelf geen kliks (dat doet een box-shadow nooit), dus de
     laag eronder vangt ze: anders klikt de bezoeker tijdens de rondleiding
     dwars door de uitleg heen op de app die hij nog niet kent. */
  .ht {
    position: fixed;
    inset: 0;
    z-index: var(--z-onboard);
    transition: opacity 0.25s;
  }
  .ht.done { opacity: 0; pointer-events: none; }

  .ht-hole {
    position: absolute;
    border-radius: 10px;
    box-shadow: 0 0 0 9999px rgba(5, 7, 13, 0.84);
    /* Het gat mag geen kliks opvangen: wie de bediening eronder al herkent,
       moet erop kunnen klikken zonder eerst de rondleiding weg te doen. */
    pointer-events: none;
    outline: 1px solid var(--accent);
    outline-offset: 0;
    transition: left 0.28s ease, top 0.28s ease, width 0.28s ease, height 0.28s ease;
  }

  .ht-card {
    position: absolute;
    width: min(320px, calc(100vw - 24px));
    padding: 15px 16px 12px;
    background: var(--void-2);
    border: 1px solid var(--hair);
    border-radius: 12px;
    transition: left 0.28s ease, top 0.28s ease;
  }

  .ht-close {
    position: absolute;
    top: 7px; right: 9px;
    background: none; border: none;
    font: inherit; font-size: 17px; line-height: 1;
    color: var(--ink-faint);
    cursor: pointer;
    padding: 3px 5px;
  }
  .ht-close:hover { color: var(--ink); }

  .ht-title {
    margin: 0 0 6px;
    padding-right: 22px;
    font-size: 11px;
    font-weight: 600;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: var(--ink);
  }
  .ht-text {
    margin: 0;
    font-size: 12px;
    line-height: 1.55;
    color: var(--ink-dim);
  }

  .ht-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    margin-top: 13px;
    padding-top: 11px;
    border-top: 1px solid var(--hair);
  }

  /* DE BOLLETJES ZEGGEN HOEVEEL STOPS ER ZIJN, en dat is de reden dat ze er
     staan: zonder teller weet je niet of je aan het begin van zes staat of van
     twintig, en dan is wegklikken de veilige keuze. */
  .ht-dots { display: flex; gap: 5px; }
  .ht-dot {
    width: 6px; height: 6px;
    padding: 0;
    background: var(--ink-faint);
    border: none;
    border-radius: 50%;
    cursor: pointer;
    opacity: 0.5;
    transition: opacity 0.15s, background 0.15s, transform 0.15s;
  }
  .ht-dot:hover { opacity: 1; transform: scale(1.25); }
  .ht-dot.on { background: var(--accent); opacity: 1; }

  .ht-nav { display: flex; align-items: center; gap: 6px; }
  .ht-step {
    background: none;
    border: none;
    padding: 4px 6px;
    font: inherit;
    font-size: 11px;
    color: var(--ink-dim);
    cursor: pointer;
    white-space: nowrap;
    transition: color 0.15s;
  }
  .ht-step:hover { color: var(--ink); }
  .ht-step-next { color: var(--accent); }
  .ht-step-next:hover { filter: brightness(1.2); }
  .ht-step-done {
    border: 1px solid var(--accent);
    border-radius: 7px;
    padding: 4px 12px;
  }

  /* Op een smal scherm staat de kaart bijna altijd tegen een rand aan; de
     binnenmarge mag dan kleiner, de raakvlakken van de knoppen niet. */
  @media (max-width: 480px) {
    .ht-card { padding: 13px 13px 10px; }
    .ht-text { font-size: 11.5px; }
  }

  /* Minder beweging gevraagd: het gat en de kaart verspringen, ze glijden niet. */
  @media (prefers-reduced-motion: reduce) {
    .ht, .ht-hole, .ht-card, .ht-dot, .ht-step { transition: none; }
  }

  /* ---- Presentation mode (session 47) -------------------------------------
     ONE LIST HIDES THE INTERFACE, and `tools/check-present-chrome.mjs` holds
     it against the markup: add a panel without adding it here and the guard
     rail falls over instead of the panel turning up on someone's wallpaper.

     `body.presenting` alone hides everything; `chrome-on` puts the panels
     back for a look around. What it does not put back are the DOM labels —
     those cannot land in the file, and a frame that shows what the file will
     not hold is the one thing this mode may not do. */
  body.presenting:not(.chrome-on) #mode-switch,
  body.presenting:not(.chrome-on) #view-switch,
  body.presenting:not(.chrome-on) #time-island,
  body.presenting:not(.chrome-on) #msphere-time,
  body.presenting:not(.chrome-on) #solar-time,
  body.presenting:not(.chrome-on) #msphere-scale,
  body.presenting:not(.chrome-on) #msphere-craft,
  body.presenting:not(.chrome-on) #antipode-exit,
  body.presenting:not(.chrome-on) .masthead,
  body.presenting:not(.chrome-on) #notif-panel,
  body.presenting:not(.chrome-on) #settings-panel,
  body.presenting:not(.chrome-on) #dock,
  body.presenting:not(.chrome-on) #dock-right,
  body.presenting:not(.chrome-on) #nav-panel,
  body.presenting:not(.chrome-on) #details-panel,
  body.presenting:not(.chrome-on) .panel,
  body.presenting:not(.chrome-on) #qlist,
  body.presenting:not(.chrome-on) #solar-map,
  body.presenting:not(.chrome-on) #solar-orient,
  body.presenting:not(.chrome-on) #readout {
    display: none !important;
  }
  /* SKETCH:START — this one selector names a layer the standalone build cuts
     out, so it lives in its own rule rather than in the list above: a name
     removed from the middle of a selector list would leave a dangling comma
     and take the whole rule down with it. */
  body.presenting:not(.chrome-on) #sketch-editor { display: none !important; }
  /* SKETCH:END */

  /* The label layers go in both states: see the note above. */
  body.presenting #quake-labels,
  body.presenting #label-lines {
    display: none !important;
  }

  /* ---- The frame ---------------------------------------------------------
     The dim is one box-shadow rather than four panels around the frame:
     four would need four sets of coordinates that have to agree, and any
     disagreement shows up as a bright seam exactly where the eye is. */
  .pm-crop { position: fixed; inset: 0; z-index: 60; pointer-events: none; display: none; }
  .pm-crop.on { display: block; }
  .pm-box {
    position: absolute;
    border: 1px solid rgba(255,255,255,0.34);
    box-shadow: 0 0 0 100vmax rgba(5,7,13,0.58);
  }
  .pm-cut {
    position: absolute; top: 0; bottom: 0; width: 1px;
    background: rgba(255,255,255,0.24);
  }

  /* ---- The bar ----------------------------------------------------------- */
  .pm-bar {
    position: fixed; left: 50%; bottom: 22px; transform: translateX(-50%);
    z-index: 62;
    display: none;
    flex-direction: column; align-items: center; gap: 8px;
  }
  body.presenting .pm-bar { display: flex; }
  .pm-row {
    display: flex; align-items: center; gap: 10px;
    background: rgba(11,16,24,0.82);
    backdrop-filter: blur(14px);
    border: 1px solid var(--hair);
    border-radius: 12px;
    padding: 9px 12px;
  }
  .pm-field { display: flex; align-items: center; gap: 6px; }
  .pm-field > span {
    font-size: 10px; font-weight: 600; letter-spacing: 0.16em;
    text-transform: uppercase; color: var(--ink-faint);
  }
  .pm-bar select {
    background: rgba(5,7,13,0.6);
    border: 1px solid var(--hair);
    border-radius: 8px;
    color: var(--ink);
    font-family: inherit; font-size: 12px;
    padding: 5px 7px;
    cursor: pointer;
  }
  .pm-bar select:hover { border-color: var(--ink-dim); }
  .pm-px { font-size: 11.5px; color: var(--ink-dim); font-variant-numeric: tabular-nums; }
  .pm-px.warn { color: var(--volcano); }
  .pm-sep { width: 1px; align-self: stretch; background: var(--hair); }
  .pm-btn {
    background: none;
    border: 1px solid var(--hair);
    border-radius: 9px;
    color: var(--ink);
    font-family: inherit; font-size: 12px; font-weight: 500;
    padding: 7px 12px;
    cursor: pointer;
    transition: border-color 0.18s, color 0.18s, background 0.18s;
  }
  .pm-btn:hover { border-color: var(--ink-dim); }
  .pm-btn:disabled { color: var(--ink-faint); cursor: default; border-color: var(--hair); }
  .pm-go { border-color: var(--accent); color: var(--accent); }
  .pm-go:hover:not(:disabled) { background: rgba(255,107,61,0.12); border-color: var(--accent); }
  .pm-exit { color: var(--ink-dim); }
  .pm-check { display: flex; align-items: center; gap: 6px; font-size: 11.5px; color: var(--ink-dim); cursor: pointer; }
  .pm-check input { accent-color: var(--accent); cursor: pointer; }
  .pm-check:hover { color: var(--ink); }

  /* The message sits ABOVE the bar and not inside it: a line appearing inside
     would widen the row and shift every control under the pointer. */
  .pm-msg {
    max-width: min(560px, 88vw);
    padding: 0 12px;
    font-size: 11.5px;
    text-align: center;
    color: var(--ink-dim);
    opacity: 0; transition: opacity 0.2s;
  }
  .pm-msg.on { opacity: 1; }
  .pm-msg.bad { color: var(--wildfire); }

  @media (max-width: 640px) {
    .pm-row { flex-wrap: wrap; justify-content: center; max-width: 94vw; }
    .pm-field > span { display: none; }
  }

  @media (prefers-reduced-motion: reduce) {
    .pm-msg, .pm-btn { transition: none; }
  }

  /* The frame-count control. A segmented row rather than a third dropdown:
     there are at most three values, they are ordered, and seeing all of them
     at once is what makes the choice legible. */
  .pm-seg { display: flex; border: 1px solid var(--hair); border-radius: 8px; overflow: hidden; }
  .pm-seg button {
    background: rgba(5,7,13,0.6);
    border: none;
    border-left: 1px solid var(--hair);
    color: var(--ink-dim);
    font-family: inherit; font-size: 12px;
    padding: 5px 11px;
    cursor: pointer;
    transition: color 0.16s, background 0.16s;
  }
  .pm-seg button:first-child { border-left: none; }
  .pm-seg button:hover { color: var(--ink); }
  .pm-seg button.on { background: rgba(255,107,61,0.16); color: var(--accent); }
  /* `hidden` needs its own rule here: .pm-field sets an explicit display and
     that beats the attribute. Same trap as .dock-btn and .time-reset. */
  .pm-field[hidden] { display: none; }
  .pm-bar select:disabled { color: var(--ink-faint); cursor: default; opacity: 0.7; }
