/* Borderless — de lessenwereld
 *
 * Het volledige scherm, de bediening eromheen, en alles wat er in de wereld staat en
 * beweegt. Er staat hier geen enkele kleurcode: de vormen verwijzen naar het palet van de
 * wereld (--wp-*, gezet door world.js) en de bediening naar de tokens van het ontwerp.
 *
 * Elke animatie gebruikt uitsluitend transform en opacity. Iets anders animeren dwingt de
 * browser om bij elk beeldje de hele wereld opnieuw door te rekenen, en met honderden
 * vormen in beeld is dat meteen merkbaar.
 */

/* Het palet van de wereld waar je in bent. world.js overschrijft deze waarden zodra hij
   weet welke wereld het is; wat hier staat is het vangnet, zodat de wereld nooit
   ongekleurd in beeld komt als daar iets misgaat. Dit is de aardbol uit het logo. */
.worldstage {
  --wp-sky-top: #060c1c;
  --wp-sky-mid: #12325a;
  --wp-sky-low: #2f6f9e;
  --wp-sun: #cfe8ff;
  --wp-far: #0d1c33;
  --wp-mid: #122a44;
  --wp-ground: #173a3a;
  --wp-ground-far: #123030;
  --wp-wall: #dfe8f2;
  --wp-wall-shade: #b4c0cf;
  --wp-roof: #2f5f7f;
  --wp-window: #ffd9a0;
  --wp-road: #1e4444;
  --wp-accent: #4faa72;
  --wp-ink: #eaf4ff;
  --wp-glow: #7fd8a8;
}

/* ── Het scherm ────────────────────────────────────────────────────────────── */

.worldstage {
  position: fixed;
  inset: 0;
  z-index: 700;
  /* Vangnet, geen decor. Mocht er ooit tóch een beeldje langskomen waarop de wereldlaag nog
     niet staat, dan zie je hier de lucht en de grond van deze wereld in plaats van een vlakke
     blauwe plaat. Het verschil tussen "de camera beweegt" en "de pagina laadt iets". */
  background:
    linear-gradient(to bottom,
      var(--wp-sky-top, var(--bg-deep)) 0%,
      var(--wp-sky-mid, var(--bg-deep)) 12%,
      var(--wp-sky-low, var(--bg-deep)) 19%,
      var(--wp-ground-far, var(--bg-deep)) 24%,
      var(--wp-ground-far, var(--bg-deep)) 100%);
  opacity: 0;
  transition: opacity .5s var(--ease-out);
  /* Knipt de wereld af op de schermrand en houdt de opmaak binnen dit scherm. Bewust niet
     `strict`: dat legt ook de afmeting vast en dat botst met een scherm dat meegroeit. */
  contain: layout paint;
}
.worldstage.is-open { opacity: 1; }
body.worldstage-open { overflow: hidden; }

.worldstage-viewport {
  position: absolute;
  inset: 0;
  overflow: hidden;
  cursor: grab;
  touch-action: none;
  outline: none;
}
.worldstage-viewport.is-dragging { cursor: grabbing; }
.worldstage-viewport:focus-visible { box-shadow: inset 0 0 0 2px var(--accent); }

.worldstage-camera {
  position: absolute;
  top: 0;
  left: 0;
  transform-origin: 0 0;
  /* De grootste enkele winst in de hele wereld, en hij verandert niets aan wat je ziet.
   *
   * Gemeten in echte Chrome met echte muisgebeurtenissen: bij zoomen zakte de p95 van 500
   * naar 217 milliseconde, en dat is meer dan de héle stad weghalen oplevert (+66%).
   *
   * Waarom: zonder dit is de wereld gewoon inhoud van de pagina, en bij elke zoomstap
   * berekent de browser de complete tekening opnieuw — dertienhonderd vormen met duizenden
   * losse stukjes, over een vlak van 3550 bij 1650 punten. Met dit ene woord krijgt de laag
   * een eigen geheugen op de grafische kaart, en hoeft er bij verplaatsen niets herrekend
   * te worden; de kaart schuift de al getekende laag.
   *
   * Er zit een prijs aan: die laag kost geheugen zolang hij bestaat. Daarom staat het hier
   * en niet op elke laag apart — dat is ook gemeten en het werd juist slechter (+42% tegen
   * +48%), want dan krijg je tien lagen die elk apart samengesteld moeten worden. */
  will-change: transform;
}
.worldstage-svg { display: block; }

/* Bij het openen komt de wereld naar je toe in plaats van dat hij er ineens staat. */
.worldstage.is-opening .worldstage-camera { animation: ws-arrive .8s var(--ease-out) both; }
@keyframes ws-arrive { from { opacity: 0; } to { opacity: 1; } }

/* Randen die iets donkerder zijn: dat maakt het beeld filmisch en houdt je blik in het
   midden. Puur decoratief, dus het vangt geen klikken. */
.worldstage-vignette {
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    radial-gradient(130% 90% at 50% 45%, transparent 42%, rgba(0, 0, 0, .5) 100%),
    linear-gradient(to bottom, rgba(0, 0, 0, .45) 0%, transparent 22%, transparent 74%, rgba(0, 0, 0, .5) 100%);
}

/* ── De bediening eromheen ─────────────────────────────────────────────────── */

.worldstage-hud {
  position: absolute;
  top: 0; left: 0; right: 0;
  display: flex;
  align-items: flex-start;
  gap: var(--space-5);
  padding: var(--space-4) var(--space-5);
  pointer-events: none;
}
.worldstage-hud > * { pointer-events: auto; }

.ws-back {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 36px;
  padding: 0 var(--space-3);
  border: 1px solid rgba(255, 255, 255, .16);
  border-radius: var(--radius-pill);
  /* Was rgba met backdrop-filter: blur(). Die blur moet opnieuw bij elke beweging van de
     wereld erachter — precies tijdens slepen en zoomen. Een dekkende achtergrond kost
     niets en leest op een donkere wereld nagenoeg hetzelfde. */
  background: #14161f;
  color: var(--wp-ink, #fff);
  font-size: 12px;
  font-weight: 600;
  transition: background var(--dur-1) var(--ease-out), transform var(--dur-1) var(--ease-out);
}
.ws-back:hover { background: #1d212c; transform: translateX(-2px); }

.ws-title { flex: 1; min-width: 0; text-align: center; }
.ws-world-name {
  margin: 0 0 3px;
  color: var(--wp-accent, var(--accent));
  font-size: 10.5px;
  font-weight: 650;
  letter-spacing: .14em;
  text-transform: uppercase;
}
.ws-title h1 {
  margin: 0;
  color: var(--wp-ink, #fff);
  font-size: clamp(15px, 2vw, 20px);
  font-weight: 600;
  letter-spacing: -.01em;
  text-shadow: 0 2px 18px rgba(0, 0, 0, .6);
  /* Nooit meer dan één regel, wat er ook in staat.
   *
   * De naam die je je cursus geeft is kort, dus normaal is dit niet nodig. Maar accounts van
   * vóór dat veld hebben hier hun volledige doelomschrijving staan, en die kan tweehonderd
   * tekens lang zijn — drie regels dwars over de kaart. Dit is het vangnet: wat er ook komt,
   * het blijft één regel en eindigt netjes met een beletselteken.
   *
   * Op een smal scherm is de balk krap, dus daar mag hij twee regels innemen. */
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
@media (max-width: 720px) {
  .ws-title h1 {
    white-space: normal;
    display: -webkit-box;
    -webkit-box-orient: vertical;
    -webkit-line-clamp: 2;
    line-clamp: 2;
  }
}

.ws-progress { flex: 0 0 auto; width: 170px; text-align: right; }
.ws-progress span { display: block; margin-bottom: 6px; color: var(--wp-ink, #fff); opacity: .72; font-size: 10.5px; }
.ws-progress-track { height: 3px; border-radius: var(--radius-pill); background: rgba(255, 255, 255, .18); overflow: hidden; }
.ws-progress-track i { display: block; height: 100%; border-radius: var(--radius-pill); background: var(--wp-accent, var(--accent)); transition: width var(--dur-4) var(--ease-out); }

.worldstage-tools {
  position: absolute;
  right: var(--space-4);
  top: 50%;
  transform: translateY(-50%);
  display: grid;
  gap: 6px;
}
.ws-tool {
  width: 38px; height: 38px;
  display: grid;
  place-items: center;
  border: 1px solid rgba(255, 255, 255, .14);
  border-radius: var(--radius-s);
  background: #14161f;
  color: var(--wp-ink, #fff);
  font-size: 15px;
  transition: background var(--dur-1) var(--ease-out), transform var(--dur-1) var(--ease-out), color var(--dur-1) var(--ease-out);
}
.ws-tool:hover { background: #1d212c; transform: scale(1.06); }
.ws-tool.is-on { color: var(--wp-accent, var(--accent)); border-color: var(--wp-accent, var(--accent)); }

/* ── De kaart van een plek ─────────────────────────────────────────────────── */

.worldstage-card {
  position: absolute;
  left: 50%;
  bottom: var(--space-5);
  width: min(460px, calc(100vw - 40px));
  transform: translate(-50%, 24px);
  opacity: 0;
  padding: var(--space-5);
  border: 1px solid rgba(255, 255, 255, .12);
  border-radius: var(--radius-l);
  background: #0b0e16;
  box-shadow: 0 30px 80px rgba(0, 0, 0, .55);
  color: #fff;
  transition: opacity var(--dur-3) var(--ease-out), transform var(--dur-3) var(--ease-spring);
}
.worldstage-card.is-open { opacity: 1; transform: translate(-50%, 0); }
.ws-card-close {
  position: absolute;
  top: 10px; right: 12px;
  width: 28px; height: 28px;
  border: 0; border-radius: 50%;
  background: transparent;
  color: rgba(255, 255, 255, .5);
  font-size: 18px;
}
.ws-card-close:hover { color: #fff; background: rgba(255, 255, 255, .08); }
.ws-card-kind {
  margin: 0 0 var(--space-2);
  color: var(--wp-accent, var(--accent));
  font-size: 10.5px;
  font-weight: 650;
  letter-spacing: .12em;
  text-transform: uppercase;
}
.worldstage-card h2 { margin: 0 0 var(--space-2); font-size: 23px; font-weight: 620; letter-spacing: -.02em; }
.ws-card-sub { margin: 0 0 var(--space-3); color: rgba(255, 255, 255, .62); font-size: 12.5px; line-height: 1.55; }
.ws-card-meta { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: var(--space-4); }
.ws-card-meta span {
  padding: 4px 9px;
  border: 1px solid rgba(255, 255, 255, .15);
  border-radius: var(--radius-pill);
  color: rgba(255, 255, 255, .72);
  font-size: 10.5px;
}
.ws-card-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); }
.ws-card-primary, .ws-card-secondary {
  min-height: 42px;
  padding: 0 var(--space-4);
  border-radius: var(--radius-control);
  font-size: 12.5px;
  font-weight: 620;
  transition: transform var(--dur-1) var(--ease-out), background var(--dur-1) var(--ease-out);
}
.ws-card-primary { flex: 1; border: 0; background: var(--wp-accent, var(--accent)); color: #10131a; }
.ws-card-primary:hover { transform: translateY(-1px); filter: brightness(1.08); }
.ws-card-secondary { border: 1px solid rgba(255, 255, 255, .18); background: transparent; color: #fff; }
.ws-card-secondary:hover { background: rgba(255, 255, 255, .08); }
.ws-card-locked { margin: 0; color: rgba(255, 255, 255, .55); font-size: 12px; line-height: 1.55; }

/* ── Het register ──────────────────────────────────────────────────────────── */

.worldstage-index {
  position: absolute;
  top: 0; right: 0; bottom: 0;
  width: min(320px, 88vw);
  padding: var(--space-5) var(--space-4);
  overflow-y: auto;
  border-left: 1px solid rgba(255, 255, 255, .1);
  background: #090b12;
  transform: translateX(100%);
  transition: transform var(--dur-3) var(--ease-out);
  color: #fff;
}
.worldstage-index.is-open { transform: none; }
.worldstage-index header { display: flex; align-items: center; justify-content: space-between; margin-bottom: var(--space-4); }
.worldstage-index h2 { margin: 0; font-size: 14px; font-weight: 620; }
.ws-index-close { width: 28px; height: 28px; border: 0; border-radius: 50%; background: transparent; color: rgba(255, 255, 255, .5); font-size: 18px; }
.ws-index-close:hover { color: #fff; background: rgba(255, 255, 255, .08); }
.ws-index-group { margin-bottom: var(--space-4); }
.ws-index-group h3 {
  margin: 0 0 var(--space-2);
  color: var(--wp-accent, var(--accent));
  font-size: 10px;
  font-weight: 650;
  letter-spacing: .12em;
  text-transform: uppercase;
}
.ws-index-row {
  width: 100%;
  display: flex;
  align-items: center;
  gap: var(--space-3);
  padding: 9px 10px;
  border: 0;
  border-radius: var(--radius-xs);
  background: transparent;
  color: #fff;
  text-align: left;
  transition: background var(--dur-1) var(--ease-out);
}
.ws-index-row:hover { background: rgba(255, 255, 255, .07); }
.ws-index-num {
  flex: 0 0 auto;
  width: 24px; height: 24px;
  display: grid; place-items: center;
  border: 1px solid rgba(255, 255, 255, .2);
  border-radius: 50%;
  font-size: 10px;
}
.ws-index-copy { min-width: 0; }
.ws-index-copy strong { display: block; font-size: 12.5px; font-weight: 560; }
.ws-index-copy small { display: block; margin-top: 2px; color: rgba(255, 255, 255, .45); font-size: 10.5px; }
.ws-index-done .ws-index-num { border-color: var(--wp-accent, var(--accent)); color: var(--wp-accent, var(--accent)); }
.ws-index-locked { opacity: .45; }

.ws-empty { padding: 20vh var(--space-5); color: rgba(255, 255, 255, .6); text-align: center; }

/* ═══ De wereld zelf ═══════════════════════════════════════════════════════ */

.ws-sun { fill: var(--wp-sun); opacity: .5; }
.ws-sun-glow { fill: url(#glow-gradient); opacity: .5; }
.ws-star { fill: #fff; opacity: .5; }
.ws-cloud ellipse { fill: var(--wp-sun); opacity: .09; }

.ws-ridge-0 { fill: var(--wp-far); opacity: .85; }
.ws-ridge-1 { fill: var(--wp-mid); opacity: .92; }
.ws-ridge-2 { fill: var(--wp-ground-far); }

.ws-sea { fill: var(--wp-mid); }
.ws-swell { fill: none; stroke: var(--wp-ink); stroke-width: 1.6; opacity: .12; }
.ws-ground-far { fill: var(--wp-ground-far); }
.ws-ground { fill: var(--wp-ground); }

.ws-road-bed { fill: none; stroke: var(--wp-road); stroke-width: 74; stroke-linecap: round; opacity: .85; }
.ws-road { fill: none; stroke: var(--wp-ground); stroke-width: 58; stroke-linecap: round; opacity: .35; }
.ws-road-dash { fill: none; stroke: var(--wp-ink); stroke-width: 3; stroke-dasharray: 26 30; stroke-linecap: round; opacity: .2; }
.ws-road-ties { fill: none; stroke: var(--wp-ink); stroke-width: 62; stroke-dasharray: 5 26; opacity: .1; }

/* ═══ Gebieden — het eerste navigatieniveau ════════════════════════════════ */

.ws-region { cursor: pointer; }
/* Geen eigen focusrand van de browser om een wijk.
 *
 * Dit stond alleen op `:focus-visible`, en dat dekt de klik niet af. Klik je een wijk aan,
 * dan krijgt de groep gewone focus, en dan tekent de browser er zijn standaard focusrand
 * omheen — een rechthoek om het omhullende vlak van de héle groep, dwars over de stad. Dat
 * is de witte lijn die soms om een gebied verscheen.
 *
 * Dat je hem er niet altijd zag komt doordat een focusrand pas verschijnt zodra het element
 * focus krijgt én niet meteen weer verliest; klikte je door naar een les, dan was hij weg
 * voor je het zag.
 *
 * Wat er voor de toetsenbordgebruiker voor in de plaats komt staat verderop: bij
 * `:focus-visible` lichten de gloed en de rand van de wijk zelf op. Dat is duidelijker dan
 * een rechthoek en het volgt de vorm van de wijk. */
.ws-region:focus,
.ws-region:focus-visible { outline: none; }

.ws-region-landmark path, .ws-region-landmark circle {
  fill: none;
  stroke: var(--wp-ink);
  stroke-width: 3;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: .3;
  transition: opacity var(--dur-4) var(--ease-out);
}
.ws-region:hover .ws-region-landmark path,
.ws-region:hover .ws-region-landmark circle { opacity: .55; }

/* De naamborden.
 *
 * Geen kaartje, geen pil, geen kader. De leesbaarheid komt van een donkere rand óm de
 * letters heen — `paint-order` laat de rand eerst tekenen en de vulling erover, zodat de
 * rand een contrastlaag wordt in plaats van een omlijning. Dat is de goedkoopste manier om
 * tekst op een drukke kaart leesbaar te houden: geen filter, geen extra vorm, geen vlak dat
 * de stad eronder afdekt.
 *
 * Een schaduwfilter zou hetzelfde doen en dwingt de browser om elke letter apart te
 * renderen en na te bewerken. Dat staat nergens meer in dit bestand en hier komt het niet
 * terug. */
.ws-region-title {
  fill: var(--wp-ink);
  font-family: var(--font-ui);
  font-size: 58px;
  font-weight: 600;
  letter-spacing: -.015em;
  stroke: var(--wp-sky-top);
  stroke-width: 7;
  paint-order: stroke fill;
  opacity: .9;
  transition: opacity .35s var(--ease-out);
}
.ws-region-note {
  fill: var(--wp-accent);
  font-family: var(--font-ui);
  font-size: 25px;
  font-weight: 620;
  letter-spacing: .06em;
  stroke: var(--wp-sky-top);
  stroke-width: 5;
  paint-order: stroke fill;
  opacity: .62;
  transition: opacity .35s var(--ease-out);
}

/* Aanwijzen maakt de naam helderder. Alleen dekking — dat is de goedkope kant. Zo hoort de
   tekst zichtbaar bij de rand die tegelijk oplicht. */
.ws-region:hover .ws-region-title,
.ws-region:focus-visible .ws-region-title { opacity: 1; }
.ws-region:hover .ws-region-note,
.ws-region:focus-visible .ws-region-note { opacity: .95; }

/* De wijken zonder les: dezelfde vorm, maar duidelijk op de achtergrond. Ze zijn er om de
   kaart te laten kloppen, niet om aandacht te vragen. */
.ws-idle-title {
  fill: var(--wp-ink);
  font-family: var(--font-ui);
  font-size: 40px;
  font-weight: 500;
  letter-spacing: .01em;
  stroke: var(--wp-sky-top);
  stroke-width: 6;
  paint-order: stroke fill;
  opacity: .34;
}
.ws-idle-plate { pointer-events: none; }
/* Ingezoomd verdwijnen ze: dan gaat het om de lessen voor je neus, niet om de wijk waar je
   in staat. Dezelfde regel als voor de naamborden met een hoofdstuk. */

[data-level="world"] .ws-layer-labels { opacity: 1; }
.ws-layer-labels { transition: opacity .45s var(--ease-out); }
.ws-region-lock { fill: none; stroke: var(--wp-ink); stroke-width: 2.4; opacity: .5; transform: scale(2.2); transform-box: fill-box; transform-origin: center; }

.ws-region-locked { cursor: not-allowed; }
.ws-region-locked .ws-region-title,
.ws-region-locked .ws-region-note { opacity: .45; }
.ws-region-locked .ws-region-landmark path,
.ws-region-locked .ws-region-landmark circle { opacity: .18; }

/* De mist over een gesloten gebied. Je ziet dat daar iets is, maar niet wat. */
.ws-area-fog {
  fill: url(#fog-gradient);
  pointer-events: none;
  transition: opacity 1.4s var(--ease-out);
}

/* De wegen tussen gebieden bestaan pas als beide kanten bereikbaar zijn: zo groeit je
   wereld zichtbaar aan elkaar naarmate je vordert. */
.ws-link {
  fill: none;
  stroke: var(--wp-road);
  stroke-width: 26;
  stroke-linecap: round;
  opacity: 0;
  transition: opacity 1.2s var(--ease-out);
}
.ws-link.is-live { opacity: .55; }
.ws-street { fill: none; stroke: var(--wp-road); stroke-width: 30; stroke-linecap: round; opacity: .5; }

/* Decor */
.ws-trunk, .ws-line { fill: none; stroke: var(--wp-ink); stroke-width: 3; stroke-linecap: round; opacity: .45; }
.ws-foliage { fill: var(--wp-far); opacity: .9; }
.ws-pine, .ws-frond { fill: var(--wp-far); stroke: none; opacity: .9; }
.ws-frond { fill: none; stroke: var(--wp-far); stroke-width: 5; stroke-linecap: round; opacity: .95; }
.ws-lamp-head { fill: var(--wp-glow); }
.ws-lamp-pool { fill: var(--wp-glow); opacity: .07; }
.ws-flag, .ws-banner { fill: var(--wp-accent); opacity: .8; }
.ws-lantern { fill: var(--wp-glow); opacity: .85; }
.ws-kiosk, .ws-roof-prop { fill: var(--wp-wall-shade); }
.ws-canvas { fill: var(--wp-accent); opacity: .7; }
.ws-water { fill: var(--wp-mid); opacity: .8; }
.ws-jet { fill: none; stroke: var(--wp-ink); stroke-width: 2.5; stroke-linecap: round; opacity: .35; }
.ws-vine { fill: none; stroke: var(--wp-far); stroke-width: 3; stroke-linecap: round; opacity: .8; }
.ws-line-fill { fill: var(--wp-ink); opacity: .35; }
.ws-car { fill: var(--wp-wall-shade); }
.ws-train { fill: var(--wp-wall-shade); }
.ws-window { fill: var(--wp-window); opacity: .25; }
.ws-window.lit { opacity: .95; }
.ws-hull { fill: var(--wp-wall-shade); }
.ws-sail { fill: var(--wp-wall); opacity: .85; }
.ws-plane { fill: var(--wp-ink); opacity: .5; }
.ws-trail { fill: none; stroke: var(--wp-ink); stroke-width: 2; opacity: .12; }
.ws-bird { fill: none; stroke: var(--wp-ink); stroke-width: 2.4; stroke-linecap: round; opacity: .45; }
.ws-mist { fill: var(--wp-ink); opacity: .05; }
.ws-shorewave { fill: none; stroke: var(--wp-ink); stroke-width: 2.5; opacity: .16; }

/* ── De plekken ────────────────────────────────────────────────────────────── */

.wp-place { cursor: pointer; }
.wp-body { transform-origin: center bottom; }
/* Het onzichtbare vangvlak. Het beweegt nooit — zie de toelichting in world.js. */
.wp-hit { fill: #fff; opacity: 0; }

/* Wijs je een lesplek aan, dan licht de grond eronder op.
 *
 * Hier stond: het gebouw komt zes pixels omhoog. Dat leest goed — er beweegt iets, dus je
 * begrijpt dat je erop kunt drukken — maar het was gemeten de duurste regel van het hele
 * zweven. Een transform op een SVG-groep gaat niet over de grafische kaart: de browser moet
 * die hele groep opnieuw tekenen, dus het gebouw, de daken, de ramen, de rekwisieten en het
 * bordje. En met een overgang erop gebeurt dat bij elk beeldje van een kwart seconde. Veeg
 * je met de muis over een buurt, dan start elke plek die je raakt zo'n hertekening, en elke
 * plek die je verlaat ook. Alleen die ene regel was vierentwintig procent.
 *
 * Dekking op één kleine vorm doet hetzelfde werk voor je oog en kost bijna niets: er wordt
 * één ovaal hertekend in plaats van een heel gebouw. Het verloop komt uit de defs die er
 * toch al waren, dus er is geen filter en geen extra vorm bij nodig.
 */
.wp-glow {
  opacity: 0;
  pointer-events: none;
  transition: opacity .26s var(--ease-out);
}
.wp-place:hover .wp-glow,
.wp-place:focus-visible .wp-glow { opacity: .55; }
.wp-locked .wp-glow { opacity: 0 !important; }
/* Zelfde verhaal voor een lesplek: ook die is aanklikbaar en krijgt dus focus. */
.wp-place:focus,
.wp-place:focus-visible { outline: none; }
.wp-place:focus-visible .wp-plate-title { text-decoration: underline; }

.wp-ground-shadow { fill: #000; opacity: .22; }
.wp-wall { fill: var(--wp-wall); }
.wp-wall-shade { fill: var(--wp-wall-shade); }
.wp-roof, .wp-quay { fill: var(--wp-roof); }
.wp-window { fill: var(--wp-window); opacity: .22; }
.wp-window.lit { opacity: .9; }
.wp-door { fill: var(--wp-roof); opacity: .8; }
.wp-knob { fill: var(--wp-glow); }
.wp-glass { fill: var(--wp-window); opacity: .3; }
.wp-awning, .wp-canopy, .wp-canvas { fill: var(--wp-accent); opacity: .85; }
.wp-awning-fold { fill: none; stroke: var(--wp-wall-shade); stroke-width: 2; opacity: .5; }
.wp-column { fill: var(--wp-wall-shade); }
.wp-entrance { fill: var(--wp-roof); }
.wp-entrance-dark { fill: #000; opacity: .55; }
.wp-platform, .wp-stair { fill: var(--wp-wall-shade); }
.wp-cliff { fill: var(--wp-ground-far); }
.wp-line { fill: none; stroke: var(--wp-ink); stroke-width: 2.6; stroke-linecap: round; opacity: .5; }
.wp-line-fill { fill: var(--wp-ink); opacity: .4; }
.wp-crate { fill: var(--wp-wall-shade); }
.wp-fruit { fill: var(--wp-accent); }
.wp-planter { fill: var(--wp-roof); }
.wp-leaf, .wp-vine { fill: none; stroke: var(--wp-accent); stroke-width: 2.4; stroke-linecap: round; opacity: .75; }
.wp-lamp, .wp-bulb { fill: var(--wp-glow); }
.wp-lantern { fill: var(--wp-glow); opacity: .9; }
.wp-flag, .wp-banner { fill: var(--wp-accent); opacity: .85; }
.wp-sign { fill: var(--wp-accent); opacity: .9; }
.wp-clock { fill: var(--wp-wall); stroke: var(--wp-ink); stroke-width: 2; }
.wp-smoke { fill: var(--wp-ink); opacity: 0; }
.wp-water { fill: var(--wp-mid); opacity: .85; }
.wp-ripple { fill: none; stroke: var(--wp-ink); stroke-width: 2; opacity: .3; }
.wp-jet { fill: none; stroke: var(--wp-ink); stroke-width: 2.4; stroke-linecap: round; opacity: .4; }
.wp-hull, .wp-train { fill: var(--wp-wall-shade); }
.wp-sail { fill: var(--wp-wall); }
.wp-plane { fill: var(--wp-ink); opacity: .55; }
.wp-scope { fill: var(--wp-wall-shade); }
.wp-pad { fill: var(--wp-road); opacity: .5; }
.wp-tree .wp-foliage { fill: var(--wp-far); }
.wp-trunk { fill: none; stroke: var(--wp-ink); stroke-width: 3; opacity: .5; }
.wp-foliage { fill: var(--wp-far); }
.wp-path { fill: none; stroke: var(--wp-road); stroke-width: 12; stroke-linecap: round; opacity: .7; }
.wp-surf { fill: none; stroke: var(--wp-ink); stroke-width: 2.5; opacity: .3; }
.wp-bird { fill: none; stroke: var(--wp-ink); stroke-width: 2.2; stroke-linecap: round; opacity: .5; }

/* Het bordje boven een plek. */
/* De naambordjes van de lessen.
 *
 * Ze waren te moeilijk te lezen, en het antwoord daarop is niet "groter maken" — dan krijg je
 * zwevende opschriften over de stad. Drie dingen samen doen het werk:
 *
 *   een donkere rand óm de letters, via paint-order, zodat de tekst leesbaar blijft boven
 *   een dak, een boom of een plein zonder dat er een vlak overheen komt;
 *   iets zwaarder en iets groter, maar net genoeg;
 *   en ze krimpen niet meer mee met het gebouw — zie de omgekeerde schaal in world.js.
 *
 * Dat laatste deed het meeste: de verste lesplek staat op schaal 0,52 en die tekst was
 * daardoor bijna half zo groot als die van de dichtstbijzijnde. Nu zijn ze allemaal gelijk.
 */
.wp-plate-kind {
  fill: var(--wp-accent);
  font-family: var(--font-ui);
  font-size: 11px;
  font-weight: 680;
  letter-spacing: .12em;
  text-transform: uppercase;
  stroke: var(--wp-sky-top);
  stroke-width: 3;
  paint-order: stroke fill;
  opacity: .92;
}
.wp-plate-title {
  fill: var(--wp-ink);
  font-family: var(--font-ui);
  font-size: 19px;
  font-weight: 650;
  letter-spacing: -.005em;
  stroke: var(--wp-sky-top);
  stroke-width: 4;
  paint-order: stroke fill;
}

/* De speld boven een plek vertelt in één oogopslag waar je staat. */
.wp-marker circle { fill: var(--wp-accent); }
.wp-marker-check { fill: none; stroke: var(--wp-sky-top); stroke-width: 2.6; stroke-linecap: round; stroke-linejoin: round; }
.wp-marker-open circle { fill: none; stroke: var(--wp-accent); stroke-width: 2.4; }
.wp-marker-number { fill: var(--wp-accent); font-family: var(--font-ui); font-size: 12px; font-weight: 650; }
.wp-marker-locked circle { fill: none; stroke: var(--wp-ink); stroke-width: 2; opacity: .35; }
.wp-marker-locked path { fill: none; stroke: var(--wp-ink); stroke-width: 1.6; opacity: .5; }
.wp-marker-arrow { fill: none; stroke: var(--wp-sky-top); stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; }
.wp-marker-pulse { fill: var(--wp-accent); opacity: .5; transform-box: fill-box; transform-origin: center; animation: ws-pulse 2.6s var(--ease-out) infinite; }

/* Afgerond, huidig, open, gesloten — hetzelfde gebouw, ander licht. */
.wp-done .wp-window.lit { opacity: 1; }

.wp-current .wp-plate-title { font-weight: 680; }
.wp-open .wp-window.lit { opacity: .6; }

.wp-locked .wp-window.lit { opacity: .12; }
.wp-locked .wp-plate-title, .wp-locked .wp-plate-kind { opacity: .4; }
.wp-locked { cursor: not-allowed; }
.wp-place.is-selected .wp-plate-title { fill: var(--wp-accent); }

/* ── Beweging ──────────────────────────────────────────────────────────────── */

@keyframes ws-pulse { 0% { opacity: .5; transform: scale(1); } 70%, 100% { opacity: 0; transform: scale(2.6); } }
@keyframes ws-sway { 0%, 100% { transform: rotate(-1.1deg); } 50% { transform: rotate(1.1deg); } }
@keyframes ws-sway-soft { 0%, 100% { transform: rotate(-.55deg); } 50% { transform: rotate(.55deg); } }
@keyframes ws-flag { 0%, 100% { transform: skewY(-3deg) scaleX(.97); } 50% { transform: skewY(3deg) scaleX(1); } }
@keyframes ws-glow { 0%, 100% { opacity: .75; } 50% { opacity: 1; } }
@keyframes ws-twinkle { 0%, 100% { opacity: .18; } 50% { opacity: .7; } }
@keyframes ws-smoke { 0% { opacity: 0; transform: translate(0, 0) scale(.5); } 25% { opacity: .16; } 100% { opacity: 0; transform: translate(-16px, -76px) scale(1.5); } }
@keyframes ws-drift { from { transform: translateX(-14%); } to { transform: translateX(114%); } }
@keyframes ws-fly { from { transform: translate(-8%, var(--y, 0px)); } to { transform: translate(108%, var(--y, 0px)); } }
@keyframes ws-flap { 0%, 100% { transform: scaleY(1); } 50% { transform: scaleY(.45); } }
@keyframes ws-traverse { from { transform: translate(-6%, var(--y, 0px)) scaleX(var(--dir, 1)); } to { transform: translate(106%, var(--y, 0px)) scaleX(var(--dir, 1)); } }
@keyframes ws-bob { 0%, 100% { transform: translateY(0) rotate(-.8deg); } 50% { transform: translateY(-5px) rotate(.8deg); } }
@keyframes ws-fountain { 0%, 100% { opacity: .28; transform: scaleY(.92); } 50% { opacity: .5; transform: scaleY(1.06); } }
@keyframes ws-ripple { 0% { opacity: .35; transform: scale(.6); } 100% { opacity: 0; transform: scale(1.5); } }
@keyframes ws-mist { from { transform: translateX(-6%); } to { transform: translateX(6%); } }
@keyframes ws-swell { 0%, 100% { transform: translateX(0); } 50% { transform: translateX(18px); } }
@keyframes ws-shore { 0%, 100% { transform: translateX(-10px); opacity: .1; } 50% { transform: translateX(10px); opacity: .22; } }
@keyframes ws-peck { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(2px); } }
@keyframes ws-surf { 0%, 100% { transform: translateX(-8px); } 50% { transform: translateX(8px); } }

/* De vaste plek waaromheen iets draait, moet onderaan liggen: een boom die om zijn kruin
   draait ziet er onmiddellijk verkeerd uit. */
.anim-sway, .anim-sway-soft, .anim-flag { transform-box: fill-box; transform-origin: bottom center; }
.anim-sway { animation: ws-sway 6.5s ease-in-out infinite; animation-delay: calc(var(--seed, 0) * -1.3s); }
.anim-sway-soft { animation: ws-sway-soft 9s ease-in-out infinite; animation-delay: calc(var(--seed, 0) * -1.7s); }
.anim-flag { transform-origin: left center; animation: ws-flag 2.4s ease-in-out infinite; }
.anim-glow { animation: ws-glow 4.2s ease-in-out infinite; }
.anim-twinkle { animation: ws-twinkle 5s ease-in-out infinite; }
.anim-smoke circle { animation: ws-smoke 3.6s ease-out infinite; }
.anim-drift { animation: ws-drift var(--dur, 100s) linear infinite; animation-delay: var(--delay, 0s); }
.anim-fly { animation: ws-fly var(--dur, 80s) linear infinite; animation-delay: var(--delay, 0s); }
.anim-flap { transform-box: fill-box; transform-origin: center; animation: ws-flap .5s ease-in-out infinite; }
.anim-traverse { animation: ws-traverse var(--dur, 40s) linear infinite; animation-delay: var(--delay, 0s); }
.anim-bob { transform-box: fill-box; transform-origin: center bottom; animation: ws-bob 4.4s ease-in-out infinite; }
.anim-fountain { transform-box: fill-box; transform-origin: center bottom; animation: ws-fountain 2.8s ease-in-out infinite; }
.anim-ripple { transform-box: fill-box; transform-origin: center; animation: ws-ripple 4s ease-out infinite; }
.anim-mist { animation: ws-mist var(--dur, 80s) ease-in-out infinite alternate; animation-delay: var(--delay, 0s); }
.anim-swell { animation: ws-swell 9s ease-in-out infinite alternate; }
.anim-shore { animation: ws-shore 6s ease-in-out infinite; }
.anim-peck { transform-box: fill-box; transform-origin: center; animation: ws-peck 1.6s ease-in-out infinite; }
.anim-surf { animation: ws-surf 5s ease-in-out infinite; }
.anim-train { transform-box: fill-box; animation: ws-bob 6s ease-in-out infinite; }
.anim-plane { animation: ws-drift 70s linear infinite; }

/* ═══ De twee navigatieniveaus ═════════════════════════════════════════════
 *
 * Er zijn geen twee schermen. De zoom bepaalt wat er in beeld is: uitgezoomd zie je de
 * gebieden met hun naam, ingezoomd de buurt met de plekken erin. De overgang loopt over
 * elkaar heen, zodat je onderweg ziet dat het één en dezelfde wereld is.
 */

.ws-neighbourhood,
.ws-region-plate,
.ws-region-landmark {
  transition: opacity .55s var(--ease-out);
}

/* Alleen pointer-events. De dekking stond hier op nul en werd verderop weer op één gezet —
   dode regel die het tegenovergestelde beweerde van wat er gebeurt. De lesgebouwen zijn
   uitgezoomd zichtbaar; dat is de kaart met spelden. */
[data-level="world"] .ws-neighbourhood { pointer-events: none; }
[data-level="world"] .ws-region-plate { opacity: 1; }
[data-level="world"] .ws-region-landmark { opacity: 1; }

[data-level="region"] .ws-neighbourhood { opacity: 1; }
/* Het naambord van een gebied verdwijnt zodra je erin staat: daar heb je de plekken zelf,
   en twee lagen tekst over elkaar leest niet. */

[data-level="region"] .ws-region-landmark { opacity: .45; }

/* Hier stond `pointer-events: none` op alle gebieden zodra je in een buurt stond. Samen
   met het indalen bij het openen zorgde dat ervoor dat er precies één gebied overbleef dat
   reageerde: het gebied waar je zelf in stond. Alle andere leken aanklikbaar en deden
   niets.
   Nu blijft de grondcirkel van een gebied altijd aanklikbaar, maar vangen de plekken erin
   de klik eerst — die liggen er in de tekenvolgorde bovenop. Zo kun je vanuit een buurt
   meteen naar de buurt ernaast, precies zoals op een kaart. */
.ws-region-landmark, .ws-region-plate { pointer-events: none; }

/* ═══ Een nieuw gebied gaat open ═══════════════════════════════════════════
 *
 * Het moment waarop een hoofdstuk zich terugbetaalt. Vier dingen tegelijk, alle vier
 * traag: de mist trekt weg, de grond licht op, het herkenningspunt komt omhoog, en de
 * lichten in de buurt gaan één voor één aan. Samen duurt het ruim drie seconden — lang
 * genoeg om er even naar te kijken, kort genoeg om niet in de weg te zitten.
 */

@keyframes ws-fog-part { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(1.5); } }
@keyframes ws-ground-wake { 0% { opacity: .05; } 40% { opacity: .4; } 100% { opacity: .05; } }
@keyframes ws-landmark-rise { from { opacity: 0; transform: translateY(60px); } to { opacity: 1; transform: none; } }
@keyframes ws-lights-on { from { opacity: 0; } to { opacity: 1; } }
@keyframes ws-street-draw { from { stroke-dashoffset: 2400; } to { stroke-dashoffset: 0; } }

.ws-region-locked ~ .ws-neighbourhood, .ws-area-locked .ws-neighbourhood { pointer-events: none; }
.ws-area.is-revealing .ws-area-fog {
  transform-box: fill-box;
  transform-origin: center;
  animation: ws-fog-part 2.6s var(--ease-out) both;
}
.ws-area.is-revealing .ws-area.is-revealing .ws-region-landmark {
  transform-box: fill-box;
  transform-origin: center bottom;
  animation: ws-landmark-rise 1.8s var(--ease-out) .5s both;
}
.ws-area.is-revealing .ws-street {
  stroke-dasharray: 2400;
  animation: ws-street-draw 2.2s var(--ease-out) .8s both;
}
/* De plekken lichten één voor één op, van links naar rechts, alsof de straatverlichting
   aangaat. De vertraging loopt op via de volgorde in het document. */
.ws-area.is-revealing .wp-place { animation: ws-lights-on 1s var(--ease-out) both; }
.ws-area.is-revealing .wp-place:nth-child(2) { animation-delay: 1.1s; }
.ws-area.is-revealing .wp-place:nth-child(3) { animation-delay: 1.3s; }
.ws-area.is-revealing .wp-place:nth-child(4) { animation-delay: 1.5s; }
.ws-area.is-revealing .wp-place:nth-child(5) { animation-delay: 1.7s; }
.ws-area.is-revealing .wp-place:nth-child(6) { animation-delay: 1.9s; }
.ws-area.is-revealing .wp-place:nth-child(7) { animation-delay: 2.1s; }
.ws-area.is-revealing .wp-place:nth-child(n+8) { animation-delay: 2.3s; }

.worldstage-reveal {
  position: absolute;
  left: 50%;
  top: 22%;
  transform: translate(-50%, 16px);
  opacity: 0;
  padding: var(--space-4) var(--space-6);
  text-align: center;
  color: #fff;
  pointer-events: none;
  transition: opacity .7s var(--ease-out), transform .9s var(--ease-out);
}
.worldstage-reveal.is-open { opacity: 1; transform: translate(-50%, 0); }
.ws-reveal-kicker {
  margin: 0 0 var(--space-2);
  color: var(--wp-accent, var(--accent));
  font-size: 11px;
  font-weight: 650;
  letter-spacing: .28em;
  text-transform: uppercase;
}
.worldstage-reveal h2 {
  margin: 0 0 var(--space-2);
  font-size: clamp(30px, 5vw, 52px);
  font-weight: 620;
  letter-spacing: -.03em;
  text-shadow: 0 4px 40px rgba(0, 0, 0, .7);
}
.ws-reveal-note { margin: 0; color: rgba(255, 255, 255, .68); font-size: 13px; }

.ws-index-group-locked { opacity: .5; }
.ws-index-group h3 { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3); }
.ws-index-group h3 button {
  border: 0; background: transparent; padding: 0;
  color: inherit; font: inherit; letter-spacing: inherit; text-transform: inherit;
  cursor: pointer;
}
.ws-index-group h3 button:hover { text-decoration: underline; }
.ws-index-group h3 small { color: rgba(255, 255, 255, .4); font-size: 10px; letter-spacing: 0; text-transform: none; }

/* Mist over de hele wereld in werelden waar dat past. */
.worldstage[data-weather="misty"] .ws-layer-far { opacity: .8; }
/* Een filter over de hele luchtlaag is te duur voor wat het oplevert; de kustwerelden
   hebben hun verzadiging al in hun palet. */

@media (max-width: 860px) {
  .worldstage-hud { flex-wrap: wrap; gap: var(--space-3); padding: var(--space-3); }
  .ws-progress { width: 100%; text-align: left; order: 3; }
  .ws-title { text-align: left; }
  .worldstage-tools { top: auto; bottom: calc(var(--space-4) + 6px); right: var(--space-3); transform: none; grid-template-columns: repeat(3, 38px); }
  .worldstage-card { bottom: var(--space-3); }
}

@media (prefers-reduced-motion: reduce) {
  .worldstage-svg [class*="anim-"] { animation: none !important; }
  .worldstage, .worldstage-card, .worldstage-index { transition-duration: .01ms; }
  /* Toestandswissels blijven een korte fade, ook bij verminderde beweging.
   *
   * base.css zet hier álle overgangen op 0,01 ms, en dat is voor knoppen en panelen precies
   * goed. Voor de wereldkaart niet: bij het passeren van de zoomdrempel wisselen naambordjes,
   * lesplekken, wijkstraten en nevel tegelijk van dekking, en zonder fade klapt dat in één
   * beeldje om — dat is de "harde herlaad" die Rafi filmde, niet een render- of FPS-probleem.
   * Een fade van 140 ms is geen beweging (er verschuift niets), dus de voorkeur wordt
   * gerespecteerd: alles wat wuift, drijft of rijdt staat hierboven wél volledig uit. */
  .wp-body, .wp-plate, .wp-marker,
  .ws-region-glow, .ws-region-edge, .ws-area-fog,
  .wc-specials, .wc-street, .wc-rail, .wc-bridge, .wc-avenue,
  .ws-region-landmark, .ws-region-plate, .ws-idle-plate {
    transition-duration: .14s !important;
  }
}

/* ═══ De stad van bovenaf ══════════════════════════════════════════════════
 *
 * Het weefsel dat van een gekleurd vlak een plek maakt: water, parken, straten en daken.
 * Alles staat stil. Een stad hoort niet te bewegen, en het is ook wat het betaalbaar
 * houdt — deze duizend vormen worden één keer getekend en daarna alleen nog met de camera
 * meegeschoven.
 */

.wc-water { fill: var(--wp-mid); opacity: .9; }
.wc-shore { fill: none; stroke: var(--wp-ink); stroke-width: 3; opacity: .12; }
.wc-water-stroke {
  fill: none;
  stroke: var(--wp-mid);
  stroke-width: 120;
  stroke-linecap: round;
  opacity: .85;
}


/* Straten zijn lichter dan de grond, zoals op elke kaart. De hoofdwegen zijn breder en
   iets helderder: zonder dat verschil blijft een raster een raster. */

/* Schuine projectie: elk pand heeft een dak en twee wanden. Eén lichtrichting voor de
   hele wereld — de rechterwand vangt licht, de voorwand ligt in de schaduw. Zonder dat
   verschil wordt het een plattegrond met randjes in plaats van volume. */
.wc-side { fill: var(--wp-wall-shade); opacity: .9; }
.wc-front { fill: #000; opacity: .34; }

.wc-roof { stroke: none; }
.wc-roof-a { fill: var(--wp-wall); opacity: .82; }
.wc-roof-b { fill: var(--wp-wall-shade); opacity: .88; }
.wc-roof-c { fill: var(--wp-roof); opacity: .78; }

/* Nu er een echte stad onder ligt, hoeft het gebied zelf nauwelijks meer te tekenen: een
   zachte gloed en een dunne grens. Een gevulde schijf zou de stad juist wegdrukken. */

/* Op het overzicht blijft de plaatsmarkering wél staan, ook al is het gebouw nog te klein
   om te zien. Zonder die spelden weet je niet waar je lessen liggen — met alleen een stad
   kijk je naar een kaart zonder bestemmingen. */
[data-level="world"] .ws-neighbourhood { opacity: 1; pointer-events: none; }
/* De gebouwen van de lessen blijven uitgezoomd staan — dat is precies wat je van een
   wereldkaart verwacht: je ziet je bestemmingen liggen, klein maar herkenbaar. Alleen de
   bordjes gaan weg, want die zijn daar onleesbaar en maken het rommelig. Het zijn er maar
   een stuk of vijftig, dus de kosten vallen weg tegen de stad eromheen. */
[data-level="world"] .wp-plate,
[data-level="world"] .ws-street { opacity: 0; }
[data-level="world"] .wp-marker { opacity: 1; }
.wp-body, .wp-plate, .wp-marker { transition: opacity .45s var(--ease-out); }

/* ═══ Het landschap ════════════════════════════════════════════════════════
 *
 * Hoogtelijnen zoals op een topografische kaart. Ze stapelen op elkaar: hoe hoger het
 * land, hoe meer lagen erover, hoe lichter het wordt. Dat is de enige manier om hoogte af
 * te lezen als je recht van boven kijkt — schaduw alleen werkt daar niet.
 */

.wt-base { fill: var(--wp-ground); }
.wt-band {
  fill: var(--wp-sun);
  /* Elke laag iets lichter dan de vorige: samen leest dat als oplopend land. */
  /* Hier stond mix-blend-mode: screen. Een mengmodus haalt een element uit het snelle pad
     en maakt er een eigen mengroep van — en dit zijn paden die het hele wereldvlak beslaan.
     Een lichtere vulling met oplopende dekking geeft hetzelfde oplopende land. */
  /* De dekking stond op .2 + .14 per laag, en dat waste de hele wereld uit: de banden zijn
     met de zonkleur gevuld (helderheid .89 tegen een grond van .18) en er liggen er vier tot
     zes over elkaar op al het land boven de laagste hoogte. Zolang blokmassa's bijna elke
     beeldpunt bedekten viel dat niet op; sinds de stad ruimte laat waar het wegennet dat
     bepaalt, ligt er kale grond bloot en was de kaart één grauwe waas — gemeten en per laag
     bewezen op 12-8: alleen de banden weghalen gaf de grond terug. */
  opacity: calc(.07 + var(--level, 0) * .05);
}
.wt-water { fill: var(--wp-mid); }
/* De branding: watercellen die aan land grenzen. Dat is de kustlijn, zonder dat er een
   contourberekening aan te pas komt. */
.wt-shore { fill: var(--wp-sun); opacity: .1; }

/* De rivier volgt de dalen van datzelfde hoogteveld. */
.wc-water-stroke { stroke-linejoin: round; }

/* ═══ Gebieden zonder omlijning ════════════════════════════════════════════
 *
 * Er zit geen enkele vorm meer omheen. Het klikvlak is volledig doorzichtig — het vangt
 * alleen de muis — en wat je ziet is de stad zelf. Zweef je erover, dan licht de grond
 * eronder zachtjes op; dat is genoeg om te begrijpen dat je op een buurt staat.
 */
/* Alle drie volgen de werkelijke omtrek van de wijk. Ze hadden een ellips kunnen zijn en
   dat wáren ze ook — maar een ellips is een vorm die niemand ooit gebouwd heeft, en dan lag
   er een schijf óver de stad in plaats van dat je een buurt aanwees.

   `evenodd` is nodig omdat een wijk een gat kan hebben: als een andere wijk er helemaal
   binnenin ligt komt die binnenrand als een eigen lus mee, en dat hoort een gat te zijn. */
.ws-region-hit, .ws-region-glow, .ws-region-edge, path.ws-area-fog { fill-rule: evenodd; }

.ws-region-hit { fill: #fff; opacity: 0; }

/* De wijk licht van binnenuit op. Warm, laag, en over het hele vlak — niet een rand die
   aangaat maar een buurt waar het licht aanspringt. */
.ws-region-glow {
  fill: var(--wp-glow);
  opacity: 0;
  pointer-events: none;
  transition: opacity .45s var(--ease-out);
}
.ws-region:hover .ws-region-glow,
.ws-region:focus-visible .ws-region-glow { opacity: .1; }

/* En een zachte rand die de vorm van de wijk volgt: langs het water, langs het spoor, om
   het park heen. Niet zichtbaar tot je erover gaat — zonder aanwijzen hoor je de wijk aan
   de stad zelf te herkennen, niet aan een lijn eromheen. */
.ws-region-edge {
  fill: none;
  stroke: var(--wp-glow);
  stroke-width: 7;
  stroke-linejoin: round;
  opacity: 0;
  pointer-events: none;
  /* Alleen dekking. Hier stond ook stroke-width in de overgang, en dat was een echte fout:
   * de breedte van een streek is geen samenstellingseigenschap maar een tekeneigenschap.
   * Elk beeldje van die overgang dwong de browser om een pad van honderdtachtig punten over
   * de hele wijk opnieuw te tekenen — vandaar het beeldje van 1266 milliseconde dat in de
   * meting stond bij het zweven over een wijk. Ruim een seconde bevriezen, van één
   * eigenschap in één overgang. */
  transition: opacity .45s var(--ease-out);
}
.ws-region:hover .ws-region-edge,
.ws-region:focus-visible .ws-region-edge { opacity: .55; }

.ws-region-locked .ws-region-hit { cursor: not-allowed; }
.ws-region-locked:hover .ws-region-glow { opacity: .05; }
.ws-region-locked:hover .ws-region-edge { opacity: .28; }

/* Geen oplichtende wijk zolang de camera beweegt.
 *
 * Dit repareert de rand die bleef branden nadat je had uitgezoomd. De oorzaak is een
 * eigenaardigheid van browsers die makkelijk te missen is: `:hover` wordt alleen opnieuw
 * bepaald als de múis beweegt. Zoom je met het wiel, dan schuift de wereld onder een
 * stilstaande cursor vandaan, maar de browser blijft denken dat je nog boven dezelfde wijk
 * hangt. De rand blijft dus aan tot je toevallig je muis verzet.
 *
 * Het is daarmee geen toestand die ik kan opruimen — hij zit in de browser en niet in onze
 * code. Wat wel kan is het gevolg onderdrukken zolang er beweging is; zodra je stilstaat en
 * de muis verplaatst klopt het vanzelf weer. De klasse is-moving bestond al voor de
 * animaties, dus dit kost geen enkele extra berekening. */
.worldstage.is-moving .ws-region-glow,
.worldstage.is-moving .ws-region-edge { opacity: 0 !important; }

/* ═══ Stadsblokken ═════════════════════════════════════════════════════════
 *
 * De straten worden niet getekend: het zijn de tussenruimtes. Doordat de blokken elk een
 * andere maat en vorm hebben, is het stratenpatroon dat ertussen overblijft dat ook —
 * en dát is waardoor het niet meer als een rooster leest.
 */
/* De blokmassa is bedoeld als samenvatting voor als je ver weg staat. Van dichtbij moet
   hij wegvallen achter de panden; anders zie je grote doorschijnende vierkanten over de
   stad heen liggen en oogt alles blokkerig. */
.wc-block { fill: var(--wp-ground-far); opacity: .35; }
.wc-park { fill: var(--wp-far); opacity: .6; }

/* ═══ Detailniveau ═════════════════════════════════════════════════════════
 *
 * Op afstand kun je geen pand onderscheiden, dus worden ze daar niet getekend — alleen de
 * massa van de blokken. Dat scheelt duizenden vormen op precies het moment dat je ze toch
 * niet ziet, en het is de reden dat dit soepeler loopt dan de vorige versie ondanks meer
 * detail van dichtbij.
 */
/* De overgang tussen ver weg en dichtbij is een vervloeiing, geen omschakeling.
 *
 * Hier stond `content-visibility: hidden` op de panden zolang je uitgezoomd was. Dat is
 * meer dan onzichtbaar maken: de browser rendert de inhoud dan helemaal niet. Passeerde je
 * de zoomdrempel, dan moest hij de complete stad — honderden paden met duizenden losse
 * stukjes — vanaf nul opbouwen, en in die beeldjes stónden de gebouwen er niet. Wat je dan
 * ziet is de laag eronder en de achtergrond van het toneel: precies die blauwe flits, en
 * precies het gevoel dat de wereld opnieuw laadt.
 *
 * Nu blijven de panden altijd gerenderd en gaat alleen hun dekking van nul naar één. Volledig
 * doorzichtige inhoud wordt door de browser toch niet getekend, dus uitgezoomd kost het
 * bijna niets — maar hij hoeft ook niets meer opnieuw op te bouwen. De vervloeiing loopt
 * beide kanten op: terwijl de panden opkomen zakken de blokmassa's eronder weg, zodat er
 * geen enkel moment is waarop er niets staat.
 */
 * Eén laag vervloeit, niet twee. Dat is gemeten: twee vlakken die tegelijk half doorzichtig
 * zijn worden allebei een eigen mengroep, en dat kostte negentien procent. De blokmassa's
 * staan nu op één vaste waarde — onder de panden zie je ze toch nauwelijks, en uitgezoomd
 * zijn zíj de stad. Korter laten duren is ook geprobeerd en werd juist negenendertig procent
 * slechter; helemaal niet vervloeien geeft weer een beeldje van een halve seconde.
 */
 * NAGEKOMEN: hier stond dat de pandenlaag als geheel vervloeit tussen nul en één. Dat is
 * vervangen — zie "De panden: de daken staan er altijd" onderaan dit bestand. De reden staat
 * daar; kort gezegd was een half doorzichtige stad boven de blokvlakken zélf de grove versie
 * van de wereld die weg moest.
 *
 * De regels die de dekking per niveau zetten zijn hier weggehaald en niet alleen overschreven:
 * ze hadden een hoger gewicht dan de nieuwe regel en zouden de panden uitgezoomd alsnog
 * onzichtbaar maken. */
.wc-masses { opacity: .3; }

/* Alles wat beweegt staat stil zolang de camera beweegt. Zie markMoving() in world.js. */
.worldstage.is-moving .worldstage-svg [class*="anim-"],
.worldstage.is-moving .worldstage-svg .ws-along { animation-play-state: paused; }

/* De zonzijde van een zadeldak. Eén extra vlak per pand, en alleen bij panden die groot
   genoeg zijn om het te zien — samen met de wisselende diepte is dat wat een woonbuurt van
   een kantoordistrict onderscheidt als je van bovenaf kijkt. */
.wc-pitch { fill: #fff; opacity: .11; }

/* ═══ Grondgebruik ═════════════════════════════════════════════════════════
 *
 * Bos, akkers en kaal gesteente naast de bebouwing. Dit is wat een kaart interessant
 * maakt: niet meer detail binnen één textuur, maar contrast tússen texturen. De stad
 * leest pas als stad wanneer er iets anders naast ligt.
 */

.wt-forest-mass { fill: var(--wp-far); opacity: .8; }
/* De korrel van het bladerdak. Zonder die kruinen is een bos een groene vlek. */
.wt-canopy { fill: var(--wp-far); opacity: .55; }

/* Akkers als lappendeken — waaraan je landbouwgrond van bovenaf herkent. Drie tinten
   door elkaar, anders wordt het weer één vlak. */
.wt-field { opacity: .38; }
.wt-field-0 { fill: var(--wp-ground-far); }
.wt-field-1 { fill: var(--wp-far); }
.wt-field-2 { fill: var(--wp-wall-shade); opacity: .16; }

/* Boven de boomgrens: kaal gesteente. */
.wt-rock-mass { fill: var(--wp-wall-shade); opacity: .14; }

/* De kruinen van het bos hebben alleen zin als je ze kunt onderscheiden. Van afstand
   volstaat de massa; anders staan er duizend cirkels in beeld op het moment dat je toch
   alleen een groen vlak ziet. Zelfde afweging als bij de panden. */
/* Was `display: none`. Dat is een opmaakwijziging: op het moment dat de drempel gepasseerd
   wordt moet de browser de opmaak van de hele tekening opnieuw doen én duizend kruinen in
   één keer tekenen. Dekking doet visueel hetzelfde en verandert niets aan de opmaak. */
.wt-canopies { transition: opacity .4s var(--ease-out); }

/* ═══ De gebouwen van de lessen ════════════════════════════════════════════
 *
 * Dezelfde schuine projectie als de stad: hetzelfde zichtpunt, dezelfde lichtrichting.
 * Dat is wat ze onderdeel van de wereld maakt in plaats van een plaatje erbovenop. Ze
 * zijn wel groter en lichter dan de gewone bebouwing, zodat je ze eruit pikt.
 */
.wp-plot { fill: #000; opacity: .18; }
.wp-side { fill: var(--wp-wall-shade); }
.wp-front { fill: #000; opacity: .3; }
.wp-roof-a { fill: var(--wp-wall); }
.wp-roof-b { fill: var(--wp-wall-shade); }
.wp-roof-c { fill: var(--wp-roof); }
.wp-dome { fill: var(--wp-accent); opacity: .9; }
.wp-dome-line { fill: none; stroke: var(--wp-ink); stroke-width: 1.5; opacity: .35; }
.wp-mast { fill: none; stroke: var(--wp-ink); stroke-width: 2.4; stroke-linecap: round; opacity: .6; }
.wp-awning, .wp-stall { fill: var(--wp-accent); opacity: .85; }
.wp-stall:nth-child(even) { opacity: .6; }
.wp-canopy { fill: var(--wp-far); opacity: .85; }
.wp-water { fill: var(--wp-mid); }
.wp-paving { fill: var(--wp-road); opacity: .85; }
.wp-rails { fill: none; stroke: var(--wp-ink); stroke-width: 2; opacity: .4; }
.wp-stairs { fill: var(--wp-road); }
.wp-stair-lines { fill: none; stroke: var(--wp-ink); stroke-width: 1.5; opacity: .3; }
.wp-boat { fill: var(--wp-wall); opacity: .9; }
.wp-glass { fill: var(--wp-window); opacity: .4; }
.wp-row { fill: var(--wp-far); opacity: .8; }

/* Afgerond, huidig, gesloten — hetzelfde gebouw, ander licht. */
/* Afgerond en gesloten zonder filters.
 *
 * Hier stond filter: saturate()/brightness(). Dat ziet er goed uit maar dwingt de browser
 * om elke gefilterde groep apart te renderen en daarna na te bewerken — en bij een halve
 * leerlijn zijn dat zesenveertig groepen, allemaal in de laag die meebeweegt als je sleept
 * of zoomt. Dat was het duurste dat in dit bestand stond.
 *
 * Dekking en een donkere doek erover doen visueel hetzelfde en gaan wél over de grafische
 * kaart. */
.wp-locked .wp-body { opacity: .42; }
.wp-locked .wp-roof-a, .wp-locked .wp-roof-b, .wp-locked .wp-roof-c { fill: var(--wp-wall-shade); }
.wp-locked .wp-awning, .wp-locked .wp-stall, .wp-locked .wp-dome { fill: var(--wp-wall-shade); }
.wp-done .wp-roof-a { fill: var(--wp-wall); }
.wp-done .wp-plot { opacity: .26; }

/* Daksoorten: waar je een pand van bovenaf aan herkent. */
.wc-hip { fill: #fff; opacity: .13; }
.wc-saw { fill: #fff; opacity: .12; }
.wc-chimney { fill: var(--wp-roof); }

/* Bijzondere bouwwerken tussen de gewone bebouwing. */
.wp-stadium { fill: var(--wp-wall-shade); opacity: .8; }
.wp-pitch-field { fill: var(--wp-far); }
.wp-blades { fill: none; stroke: var(--wp-ink); stroke-width: 2.2; stroke-linecap: round; opacity: .6; }
.wp-legs { fill: none; stroke: var(--wp-ink); stroke-width: 2.4; opacity: .45; }

/* ═══ Rekwisieten ══════════════════════════════════════════════════════════
 *
 * De kleine dingen die vertellen wat je gaat leren zonder één woord: tafeltjes op een
 * terras, kratten bij een levering, koffers bij een hotel, een rij wachtenden bij een
 * loket. Ze horen bij de plek, niet bij het gebouw — daarom kunnen twee lessen in
 * hetzelfde pand toch verschillende plekken zijn.
 */
.pr-table { fill: var(--wp-wall); opacity: .9; }
.pr-chair { fill: var(--wp-wall-shade); opacity: .8; }
.pr-parasol { fill: var(--wp-accent); opacity: .75; }
.pr-parasol-top { fill: var(--wp-accent); opacity: 1; }
.pr-stripe { fill: var(--wp-wall); opacity: .5; }
.pr-crate { fill: var(--wp-roof); opacity: .9; }
.pr-fruit-0 { fill: var(--wp-accent); }
.pr-fruit-1 { fill: var(--wp-glow); }
.pr-fruit-2 { fill: var(--wp-far); }
.pr-flower-0 { fill: var(--wp-accent); }
.pr-flower-1 { fill: var(--wp-glow); opacity: .9; }
.pr-flower-2 { fill: var(--wp-wall); opacity: .8; }
.pr-van rect { fill: var(--wp-wall); opacity: .9; }
.pr-van-cab { fill: var(--wp-wall-shade) !important; }
.pr-bag { fill: var(--wp-roof); }
.pr-taxi rect { fill: var(--wp-glow); opacity: .9; }
.pr-taxi-sign { fill: var(--wp-ink) !important; opacity: .8; }
.pr-bike circle, .pr-bike path { fill: none; stroke: var(--wp-ink); stroke-width: 1.4; opacity: .5; }
.pr-bike path { fill: none; stroke: var(--wp-ink); stroke-width: 1.4; opacity: .5; }
/* Van bovenaf is een mens een stip. Meer is niet nodig: het gaat om de aanwezigheid. */
.pr-person { fill: var(--wp-ink); opacity: .55; }
.pr-person-1 { fill: var(--wp-accent); opacity: .7; }
.pr-person-2 { fill: var(--wp-wall); opacity: .6; }
.pr-string { fill: none; stroke: var(--wp-ink); stroke-width: 1.2; opacity: .35; }
.pr-bulb { fill: var(--wp-glow); }
.pr-plant { fill: var(--wp-far); opacity: .9; }
.pr-smoke circle { fill: var(--wp-ink); opacity: 0; }
.pr-sign { fill: var(--wp-accent); opacity: .9; }
.pr-barrel { fill: var(--wp-roof); opacity: .85; }
.pr-book-0 { fill: var(--wp-accent); } .pr-book-1 { fill: var(--wp-roof); } .pr-book-2 { fill: var(--wp-wall); }
.pr-cross rect { fill: var(--wp-glow); }
.pr-screen { fill: var(--wp-window); opacity: .75; }
.pr-towel-0 { fill: var(--wp-accent); } .pr-towel-1 { fill: var(--wp-glow); } .pr-towel-2 { fill: var(--wp-wall); }

/* Het enige dat beweegt, en heel traag: rook uit een keuken of bakkerij. */
@keyframes pr-rise { 0% { opacity: 0; transform: translate(0, 0) scale(.4); } 25% { opacity: .2; } 100% { opacity: 0; transform: translate(-10px, -46px) scale(1.3); } }
.anim-rise circle { animation: pr-rise 5.5s ease-out infinite; }

/* De handtekening van een wereld: één detail dat op ongeveer één op de acht panden
   terugkomt. Kleur vertelt je dát twee steden verschillen; een terugkerend detail vertelt
   je wélke stad het is. */
.wc-sig { fill: var(--wp-accent); opacity: .8; }


/* ═══ Detail alleen waar je kijkt ══════════════════════════════════════════
 *
 * Sta je ingezoomd in één buurt, dan hoeven de gebouwen van de andere buurten daar niet
 * in vol detail te staan: die vallen toch buiten beeld. De rekwisieten en de bordjes gaan
 * daar weg; de gebouwen zelf blijven, zodat de stad doorloopt als je naar de rand kijkt.
 *
 * Dit is de goedkoopste vorm van wegsnijden die er is: er wordt niets herbouwd, de browser
 * slaat alleen verborgen inhoud over.
 */
/* Was `display: none`. Dat is een opmaakwijziging, en die valt op: sleep je langs de rand
   van een wijk, dan verschijnen of verdwijnen de bordjes en de rekwisieten er in één keer.
   Precies het "stukken van de stad die inladen" dat hier weg moest.

   Met dekking gebeurt hetzelfde zonder sprong, en de browser slaat volledig doorzichtige
   inhoud toch al over bij het tekenen. */
.worldstage[data-region] .ws-area .wp-plate,
.worldstage[data-region] .ws-area [class^="pr-"] { transition: opacity .5s var(--ease-out); }
.worldstage[data-region] .ws-area:not(.is-focus) .wp-plate,
.worldstage[data-region] .ws-area:not(.is-focus) [class^="pr-"] { opacity: 0; }

/* De vignettering was een vast vlak over het hele scherm dat elke beweging meecomponeert.
   Alleen op de randen leggen scheelt de helft van het oppervlak. */
.worldstage-vignette { background: linear-gradient(to bottom, rgba(0,0,0,.5) 0%, transparent 18%, transparent 78%, rgba(0,0,0,.55) 100%); }

/* ═══ Straten, pleinen, schaduw en ramen ═══════════════════════════════════
 *
 * Vier dingen die de stad van bovenaf leesbaar maken, en die alle vier meeliften op een
 * bundel die er toch al was. Ze kosten samen minder vormen dan wat de doorkijkgrens net
 * heeft weggehaald.
 */

/* De aders dragen de stad. Ze zijn breder en donkerder dan de rest, want een stad van
   bovenaf herken je aan zijn hoofdwegen voordat je één gebouw ziet. */
.wc-avenue {
  fill: none;
  stroke: var(--wp-road);
  /* Past binnen de smalste ader-opening die city.js maakt (52). Stond op 30 terwijl een
     opening 20 kon zijn — dan liep de weg dwars door de gevels. */
  stroke-width: 44;
  stroke-linecap: round;
  opacity: .55;
}
.wc-street {
  fill: none;
  stroke: var(--wp-road);
  /* Past binnen de smalste straat-opening (30). */
  stroke-width: 26;
  stroke-linecap: round;
  /* Op .32 was een straat in de praktijk onzichtbaar: de dekking verdunt het toch al kleine
     verschil tussen wegdek en grond. Sinds dat verschil in daylight.js op een bodem van 0,12
     staat, is .5 het punt waar een straat leesbaar wordt zonder met de aders te concurreren. */
  opacity: .5;
}

/* Een plein is bestrating, geen groen: lichter dan de grond eromheen, met een fontein of
   een monument in het midden. Dat middelpunt is wat het tot een plein maakt. */
.wc-plaza { fill: var(--wp-wall-shade); opacity: .18; }
.wc-plaza-heart { fill: var(--wp-wall); opacity: .3; }

/* Schaduw. Het licht komt van rechts, dus valt de schaduw naar links. Dit is het verschil
   tussen panden die op één vlak liggen en panden die ergens boven staan — en het kost één
   extra vorm per baan, niet één per pand. */
.wc-shadow { fill: #000; opacity: .2; }

/* Ramen. Alleen van dichtbij: uitgezoomd zit de hele pandenlaag op dekking nul en tekent de
   browser volledig doorzichtige inhoud niet. Van dichtbij zie je aan het aantal rijen hoe
   hoog een pand is, en dát is waardoor het als gebouw leest. */
.wc-window { fill: var(--wp-window); opacity: .38; }

/* De slagschaduw onder een lesplek. Iets donkerder dan het grondvlak en naar links
   verschoven — dezelfde lichtrichting als de panden van de stad eromheen. */
.wp-cast { fill: #000; opacity: .18; }
.wp-locked .wp-cast { opacity: .1; }

/* De landsoorten worden nu als omtrek getekend in plaats van als raster van vierkantjes.
   Daardoor kan een vorm gaten hebben — een open plek in het bos, een eiland in het meer —
   en die komen als eigen lussen mee. Zonder evenodd worden dat geen gaten maar vlakken. */
.wt-water, .wt-shore, .wt-band, .wt-forest-mass, .wt-rock-mass, .wt-field { fill-rule: evenodd; }

/* ═══ De natuurlijke grenzen ═══════════════════════════════════════════════
 *
 * Rivier, spoor en boulevard. Ze lopen van rand tot rand door de hele stad en ze zijn de
 * reden dat een wijk ergens ophoudt: het stadsplan laat wijken tegen deze lijnen aan
 * groeien, en wat je hier ziet is dezelfde lijn. Daarom valt de rand van een buurt samen
 * met iets dat er staat, in plaats van met niets.
 */
.wc-water-stroke {
  fill: none;
  stroke: var(--wp-mid);
  stroke-width: 78;
  stroke-linecap: round;
  stroke-linejoin: round;
  opacity: .95;
}
.wc-water-stroke.is-canal { stroke-width: 46; }
/* Een lichtere kern in het water: dat leest als diepte zonder een verloop. */
.wc-water-glint {
  fill: none;
  stroke: var(--wp-sun);
  stroke-width: 22;
  stroke-linecap: round;
  opacity: .12;
}

/* Het spoor: een baan met een lijn erop. Het snijdt door de stad heen, en dat hoort ook zo
   te lezen — spoor is de hardste scheiding die een stad zichzelf aandoet na water. */
.wc-rail-bed { fill: none; stroke: var(--wp-ground-far); stroke-width: 34; stroke-linecap: round; opacity: .8; }
.wc-rail {
  fill: none;
  stroke: var(--wp-wall-shade);
  stroke-width: 3;
  stroke-dasharray: 2 10;
  opacity: .5;
}

/* De boulevard scheidt het zachtst: je steekt hem over, maar aan de overkant is het net een
   andere buurt. Breder dan een gewone straat, met een middenberm. */
.wc-boulevard { fill: none; stroke: var(--wp-road); stroke-width: 40; stroke-linecap: round; opacity: .6; }

/* Bruggen. Zonder deze is het geen stad met een rivier maar twee steden. */
.wc-bridge { fill: none; stroke: var(--wp-wall-shade); stroke-width: 13; stroke-linecap: butt; opacity: .55; }

/* ═══ Waarom hier geen viewport-culling meer staat ═════════════════════════
 *
 * Hier stond `content-visibility: auto` op elke wijk, zodat de browser wijken buiten beeld
 * kon overslaan. Gemeten leverde dat ongeveer tien procent op bij het zoomen.
 *
 * Maar het is dezelfde machinerie als de `content-visibility: hidden` hierboven, en dus
 * hetzelfde probleem: een groep die overgeslagen is moet bij binnenkomst in één keer worden
 * opgebouwd, en in die beeldjes staat hij er niet. Sleep je langs de rand van een wijk, dan
 * verschijnt die dus met een schok.
 *
 * Tien procent p95 is die schok niet waard. Een zichtbare hapering weegt zwaarder dan een
 * meetbare winst — dat is precies wat er hier gevraagd werd. De inhoud blijft nu altijd
 * gerenderd; wat wegvalt gaat weg met dekking, en dat kan de browser overslaan zonder
 * hem later opnieuw te hoeven opbouwen.
 */

/* ═══ Bewegen over een route ═══════════════════════════════════════════════
 *
 * Alles wat zich door de wereld verplaatst deed hetzelfde: een rechte veeg over de hele
 * breedte op een vaste hoogte. Daardoor reed er een auto dwars door een huizenblok en voer
 * er een boot over een akker — er bewóóg iets, maar het hoorde nergens bij.
 *
 * Nu zit elk ding op een echt pad uit het stadsplan: de rivier, het spoor, de boulevards.
 * `offset-distance` bepaalt hoe ver het is, en dat wordt intern een transform — dus dit kost
 * niet meer dan de rechte veeg die het verving. Alleen klopt het nu.
 */
.ws-along {
  offset-distance: 0%;
  animation: ws-along var(--dur, 60s) linear infinite;
  animation-delay: var(--delay, 0s);
}
@keyframes ws-along { from { offset-distance: 0%; } to { offset-distance: 100%; } }

/* De tegenliggers rijden hetzelfde pad achterstevoren af. Zonder deze spiegeling rijden ze
   achteruit — de auto wijst dan de kant op waar hij vandaan komt. */
/* De tegenligger rijdt het pad achterstevoren af en wordt horizontaal gespiegeld.
 *
 * Hier stond eerder `offset-rotate: auto 180deg` op de groep. Honderdtachtig graden draaien
 * in een plat vlak zet een voertuig op zijn kop — dat was de fout waardoor auto's
 * ondersteboven reden. Spiegelen is iets anders dan omkeren.
 *
 * Nu draait alles gewoon mee met de richting van de weg (`offset-rotate: auto`), rijdt de
 * tegenligger de route van achteren naar voren, en wijst zijn neus door de spiegeling de
 * kant op waar hij naartoe gaat. Er is geen stand meer waarin iets op zijn kop kan komen. */
.ws-along.is-tegenligger { animation-direction: reverse; }
.ws-lane-back { transform: scaleX(-1); }
.is-tegenligger > .anim-bob { transform: scaleX(-1); }

/* ═══ Detail dat oploopt in stappen, niet in één sprong ════════════════════
 *
 * Dit blok vervangt de zichtbare kant van alle omschakelingen hierboven. Het staat
 * achteraan omdat gelijke gewichten betekent dat de laatste regel wint.
 *
 * Het probleem dat het oplost: er was één drempel, en op dat ene moment schakelden vijftien
 * dingen tegelijk om — panden, boomkruinen, straten, bordjes, rekwisieten, herkenningspunten.
 * Al die sprongen vielen op dezelfde honderdste seconde. Er werd niets herladen, maar het
 * zag eruit alsof er een andere versie van de stad werd ingezet, en dat is hetzelfde voor
 * wie ernaar kijkt.
 *
 * Nu heeft elk soort detail zijn eigen moment van opkomen, verdeeld over zeven stappen:
 *
 *   stap 0   je ziet het geheel: blokmassa's, gebiedsnamen, herkenningspunten
 *   stap 1   de panden komen op, half
 *   stap 2   de panden staan er vol; boomkruinen komen op
 *   stap 3   de buurt met zijn lesplekken en straten komt op
 *   stap 4   de naambordjes van de lessen komen op
 *   stap 5   de rekwisieten komen op: tafeltjes, mensen, fietsen
 *   stap 6   alles staat er
 *
 * Daardoor is er nooit een moment waarop de wereld verandert. Er komt alleen steeds iets
 * bij, en wat er al stond blijft staan waar het stond.
 *
 * Wat dit níét doet: alles altijd op vol detail tekenen. Uitgezoomd staat de helft op
 * dekking nul, en volledig doorzichtige inhoud slaat de browser over bij het tekenen. De
 * kosten blijven dus waar ze waren; alleen de sprong is eruit.
 */

/* ── De panden: de daken staan er altijd, de wanden komen erbij ──────────────
 *
 * Dit is de kern van het "de wereld herlaadt"-gevoel, en de vorige poging maakte het erger.
 *
 * Wat er stond: de hele pandenlaag ging van dekking nul naar één, met een tussenstand op
 * 0,45. In die tussenstand kijk je dwars door alle gebouwen heen naar de blokvlakken
 * eronder — en die twee lijken nergens op elkaar. Een half doorzichtige stad boven platte
 * vlakken ís een grove versie van de wereld. Precies wat er niet mocht.
 *
 * Het probleem zat dus niet in de overgang maar in wat er overgang tússen was: twee
 * afbeeldingen van dezelfde stad die er totaal anders uitzien. Elke menging daarvan valt op,
 * hoe zacht je hem ook maakt.
 *
 * De oplossing is dat de lage-detailversie dezelfde silhouetten heeft als de hoge. De daken
 * zijn dat al: hetzelfde grondvlak, dezelfde plek, dezelfde kleur. Die staan er nu altijd —
 * uitgezoomd lees je ze als platte, gekleurde huizenblokken. Wat erbij komt zijn de wanden,
 * de schaduw en de ramen: díe geven de hoogte. Er verdwijnt dus nooit een gebouw; er komt
 * alleen diepte bij op iets dat er al stond.
 *
 * Gemeten tegenover de vervloeiende laag: p95 twintig procent lager en het ergste beeldje
 * beter, tegen drie procent hoger gemiddelde. Per saldo geen verlies, en visueel het
 * verschil tussen "de stad wordt vervangen" en "de stad komt dichterbij". */
.wc-solids { opacity: 1; }

.wc-side, .wc-front, .wc-shadow, .wc-window,
.wc-pitch, .wc-hip, .wc-saw, .wc-chimney, .wc-sig {
  transition: opacity .55s var(--ease-out);
}

/* ── De boomkruinen ─────────────────────────────────────────────────────────── */
.wt-canopies { transition: opacity .55s var(--ease-out); }

/* ── De buurt met de lesplekken ─────────────────────────────────────────────
 *
 * Hier stond eerst een reeks stappen die de lesplekken lieten opkomen. Dat is teruggedraaid
 * voordat het erin ging: de lesgebouwen zijn óók uitgezoomd zichtbaar — dat is de kaart met
 * spelden waar je je aan oriënteert — en ze laten opkomen zou het ontwerp veranderen in
 * plaats van alleen de sprong eruit halen.
 *
 * Wat wél gestaffeld is, is het detail eróp: de bordjes en de rekwisieten hieronder. */

/* ── De straat door de buurt ────────────────────────────────────────────────── */
.ws-street { transition: opacity .5s var(--ease-out); }

/* ── De naambordjes van de lessen ───────────────────────────────────────────
 * Later dan de gebouwen zelf: eerst zie je dát er iets staat, daarna wát het is. */
.wp-plate { transition: opacity .5s var(--ease-out); }

/* ── De rekwisieten ─────────────────────────────────────────────────────────
 * Het laatst, want ze zijn het kleinst. Tafeltjes van twee pixels hebben geen zin voordat
 * je er echt bovenop zit. */
.pr-table, .pr-chair, .pr-person, .pr-bike, .pr-fruit, .pr-bulb,
.pr-parasol, .pr-crate, .pr-bag, .pr-plant, .pr-book, .pr-towel {
  transition: opacity .45s var(--ease-out);
}

/* ── Wat er hard omsprong zonder overgang ──────────────────────────────────
 * Deze veranderden alleen van dekking op de drempel, maar zonder overgang — en een sprong
 * van 0,12 naar 0,32 is klein genoeg om niet op te vallen als iets, en groot genoeg om als
 * flikkering te lezen. */
.wc-specials, .wc-street, .wc-rail, .wc-bridge, .wc-avenue,
.ws-region-landmark, .ws-region-plate, .ws-idle-plate, .wp-marker {
  transition: opacity .5s var(--ease-out);
}

/* ═══ De gebakken wereld ═══════════════════════════════════════════════════
 *
 * Het canvas met de uitgetekende stad ligt onder de levende SVG-laag, in dezelfde bewegende
 * laag, zodat ze altijd samen opschuiven. Zou het canvas apart bewegen, dan gleed de stad
 * onder de lesplekken vandaan.
 *
 * Zolang het bakken niet klaar is staan de statische lagen gewoon in de SVG en is het canvas
 * leeg. Daarom heeft het canvas geen eigen achtergrond: je zou anders de wereld eronder
 * afdekken tot de oven klaar is.
 */
.worldstage-baked {
  position: absolute;
  top: 0;
  left: 0;
  pointer-events: none;
}
.worldstage-svg { position: relative; }

/* ═══ Namen gaan niet meer uit ══════════════════════════════════════════════
 *
 * Dit blok staat achteraan omdat het de regels hierboven overschrijft, en het lost het
 * geflikker op dat in de opname te zien was: het bordje "Mise-en-place en bereiding" ging
 * aan, uit, terug, uit — in beeldjes van honderd milliseconde, terwijl de stad eronder
 * stilstond.
 *
 * De oorzaak was dat de wijknamen aan het zoomniveau hingen en op nul gingen zodra je de
 * drempel kruiste. Hysterese in world.js haalt het snelle heen-en-weer eruit, maar de echte
 * fout zit in de gedachte: een wijk heet Malasaña, of je nu ver weg staat of ernaast. De
 * naam hoort bij de plek, niet bij een camerastand.
 *
 * Dus gaan ze niet meer uit. Ingezoomd worden ze zwakker en maken ze plaats voor de
 * lesnamen, maar ze blijven staan. Er is dan geen aan-uitgrens meer om overheen te
 * flikkeren — alleen een verandering in nadruk, en die gebeurt één keer.
 */
[data-level="region"] .ws-region-plate { opacity: .3; pointer-events: none; }
[data-level="region"] .ws-layer-labels { opacity: .22; }

/* De overgang mag langer duren dan de vorige. Kruis je toch nog eens een grens, dan is een
   trage vervloeiing minder opvallend dan een snelle — en met de hysterese erbij gebeurt het
   hooguit één keer per zoombeweging in plaats van vier. */
.ws-layer-labels,
.ws-region-plate { transition: opacity .7s var(--ease-out); }

/* De lesbordjes van wijken waar je niet in staat gaan ook niet meer helemaal uit. Ze zijn
   zwak genoeg om de aandacht niet te trekken en sterk genoeg om niet als verdwijnen te
   lezen. */
.worldstage[data-region] .ws-area:not(.is-focus) .wp-plate { opacity: .18; }

/* ═══ Waarom hier geen detailstappen meer staan ════════════════════════════
 *
 * Er stond een systeem van zeven detailstappen dat bepaalde wat er zichtbaar werd naarmate
 * je inzoomde. Dat is weg, en het weghalen wás de reparatie.
 *
 * Het stamde uit de tijd dat de hele wereld als SVG in de pagina stond en er detail
 * weggelaten moest worden om het soepel te houden. Sinds de stad gebakken wordt stuurden
 * vijftien van de zeventien klassen die eraan hingen niets meer aan: die zitten in het
 * canvas en trekken zich van CSS niets aan. Wat overbleef waren de naambordjes en de straten.
 *
 * En daar ging het mis. Hysterese beschermt tegen trillen op één grens, maar dat is niet wat
 * er gebeurt: een gewone zoombeweging kruist alle zes grenzen in tweehonderd milliseconde,
 * en op de terugweg nog eens. Elke kruising startte een vervloeiing van zeven tienden van een
 * seconde, die door de volgende werd onderbroken. Gemeten over acht keer in- en uitzoomen:
 * honderd toestandswisselingen, waarvan zevenentachtig binnen zeven tienden van de vorige.
 * Dat is het geflikker, en het was niet met een marge te repareren.
 *
 * Nu hangen de bordjes en de straten alleen nog aan het niveau, en dat heeft wél hysterese:
 * één wisseling per richting in plaats van zes.
 */
.wp-plate { transition: opacity .55s var(--ease-out); }
[data-level="world"] .wp-plate { opacity: 0; }
[data-level="region"] .wp-plate { opacity: 1; }

.ws-street { transition: opacity .55s var(--ease-out); }
[data-level="world"] .ws-street { opacity: 0; }
[data-level="region"] .ws-street { opacity: .5; }

/* De bordjes van lessen in wijken waar je niet in staat blijven zwak zichtbaar. Deze regel
   moet ná de regels hierboven staan om te winnen. */
.worldstage[data-region] .ws-area:not(.is-focus) .wp-plate { opacity: .18; }
