/* ==================================================================
   THE LOADING CURTAIN — the ONLY source for this component.

   Loaded by site_shared/master.php (every subpage + belevingen/) and by
   site/index.php (the homepage). Markup: site_shared/preloader_markup.php.
   Behaviour + ember field: js/preloader.js.

   WHY THIS IS ITS OWN FILE. This block lived in master.css and, as a copy, in
   index.css — with the line "Mirrors master.css; keep in sync" above it. That
   went exactly the way such lines always go: on 2026-09-01 the homepage turned
   out to be two fix rounds behind. It had no ember field, no sheen and no halo,
   kept PL_MIN at 900ms against 5000ms, and still ran the OLD ring that retracts
   its arc with `stroke-dasharray` — the animation twice reported as "the circle
   stops before loading is done", and removed from master.css for that reason.
   The homepage simply kept showing it.

   One file, two <link>s. Change the curtain and it changes everywhere.
   Guarded by apps/local/tests/loading_curtain/run.sh.
   ================================================================== */
.preloader {
  position: fixed; inset: 0; z-index: 2147483647; display: grid; place-items: center;
  background: #0f0e0b;
  backdrop-filter: none; -webkit-backdrop-filter: none;
  opacity: 1; transition: opacity 0.7s var(--ease);
  /* Noodrem voor het geval js/preloader.js nooit draait (JS uit, script kapot,
     fonts die blijven hangen): dan tilt de CSS het gordijn alsnog op. De
     vertraging moet ruim NA het LAATSTE moment liggen waarop het script zelf kan
     onthullen — stond hij eerder, dan dooft het gordijn midden in zijn eigen
     animatie uit.

     DAT LAATSTE MOMENT IS PL_MAX, NIET PL_MIN (13-09-2026). Toen PL_MIN van 5000
     naar 2000 ging leek 6,5s hier ineens twee keer zo lang als nodig, en de
     verleiding is om hem dan simpelweg te halveren. Fout: het script onthult op
     max(PL_MIN, wachten-op-fonts), en dat wachten loopt tot PL_MAX = 3400ms. Op een
     trage lijn is de onthulling dus ~3,4s, niet 2s. 4,5s ligt daar ruim achter, met
     de fade van 0,6s er nog binnen. Verander je PL_MAX in preloader.js, verander dan
     dit getal mee. */
  animation: pl-failsafe 0.6s var(--ease) var(--preloader-failsafe-delay, 4.5s) forwards;
}
.preloader.pl-done { opacity: 0; pointer-events: none; }
@keyframes pl-failsafe { to { opacity: 0; visibility: hidden; pointer-events: none; } }
/* Ambient warmth behind the mark. A dead-flat field reads as a blank screen
   that happens to have a spinner on it; this gives the curtain some depth and
   ties it to the brand gold without competing with the ring. Decorative only,
   so it sits behind everything and takes no pointer events. */
.preloader::before {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background:
    radial-gradient(58% 46% at 50% 44%, rgba(255,217,0,0.075), transparent 72%),
    radial-gradient(90% 70% at 50% 120%, rgba(255,217,0,0.04), transparent 70%);
  animation: pl-glow 5.2s ease-in-out infinite;
}
@keyframes pl-glow { 0%,100% { opacity: 0.75; } 50% { opacity: 1; } }
.pl-inner { display: flex; flex-direction: column; align-items: center; gap: 20px; }
.pl-card {
  position: relative; display: grid; place-items: center;
  width: clamp(210px,18vw,252px); aspect-ratio: 1; border-radius: 50%;
  background: rgba(244,239,227,0.07); border: 1px solid rgba(244,239,227,0.14);
  /* NO backdrop-filter here any more (2026-09-03). It was blur(18px) saturate(140%)
     sitting on top of a canvas that repaints every frame — so the blur re-ran on
     every frame of the curtain, on the busiest moment of the page, on the one
     device class that struggles. It is also the #1 ranked suspect in
     site/specs/spec.md for the ring that "stops" on the owner's iPhone: WebKit
     is known to stall paint inside backdrop-filter elements. The disc sits over
     a dark field whose embers are already dimmed around the centre (centreHole
     in js/preloader.js), so the glass bought almost nothing visible. A flat
     translucent disc instead, the fill a touch denser to keep the mark on a calm
     ground. */
  box-shadow: 0 30px 80px rgba(0,0,0,0.45);
}
.pl-card { overflow: hidden; }   /* de glans en de vonken blijven binnen de schijf */

/* ── glans over het glas ───────────────────────────────────────────────
   Een smalle lichtband die er één keer per cyclus schuin overheen trekt. Dit is
   het goedkoopste "premium"-signaal dat er is: het maakt van een plat vlak een
   OPPERVLAK, en het is puur een transform op één element. Traag en met een lange
   pauze ertussen, anders wordt het een knipperlicht. */
.pl-card::after {
  content: ''; position: absolute; inset: -40% -60%; pointer-events: none;
  background: linear-gradient(100deg,
    transparent 44%, rgba(255,255,255,0.10) 50%, rgba(255,217,0,0.07) 53%, transparent 60%);
  transform: translateX(-40%);
  animation: pl-sheen 5.6s var(--ease) infinite;
}
@keyframes pl-sheen {
  0%, 62% { transform: translateX(-40%); }
  100%    { transform: translateX(40%); }
}

/* ── embers ────────────────────────────────────────────────────────────
   A brewery with a BBQ: rising embers are brand language here, not generic
   particles. They use the same vocabulary as the grill animation on the dish
   cards (belevingen.css), so the product has one motion language.

   ACROSS THE WHOLE CURTAIN, AND ON A CANVAS. The field started as loose spans in
   .pl-card — seven at first, then 54 — but that card has overflow:hidden for the
   sheen, so they were clamped inside a circle and you never saw more than two at
   once. And for thousands of embers the DOM dies anyway: every span carried seven
   inline custom properties, so a few thousand is hundreds of KB of HTML on EVERY
   public page — exactly the bytes that have to arrive before this curtain can
   hide anything. One canvas costs zero extra HTML.

   The simulation lives in js/preloader.js; only the placement is here. If that JS
   fails the curtain stays up with the halo, the sheen and the ring — all pure CSS.

   POSITION: ABSOLUTE IS NOT A DETAIL HERE. .preloader is display:grid with
   place-items:center. Lose this declaration and .pl-embers becomes an ordinary
   grid child, takes its own row, and pushes .pl-inner — the mark and "Laden…" —
   down and off screen: the field then sits there beautifully and the brand is
   gone.

   NEVER WRITE A COMMENT TERMINATOR IN RUNNING TEXT. An asterisk followed by a
   forward slash closes the comment where it stands, even mid-sentence. The line
   below it is then swallowed as junk up to the next brace — the CSS keeps
   loading, no error appears, and exactly one rule has quietly vanished. That has
   happened here twice, the second time inside the sentence warning about it. If
   you mean the character, describe it in words. */
.pl-embers {
  position: absolute; inset: 0; overflow: hidden; pointer-events: none;
  z-index: 0;               /* behind .pl-inner, which has its own stack */
}
/* THE FIELD ARRIVES WITH THE DISC, NOT AFTER IT.
   The disc and the word are pure CSS and are therefore on screen from the first
   paint; the ember field has to be painted by JS. The canvas element itself now
   ships in the markup (see site_shared/preloader_markup.php), so this fade starts
   at the same timeline position as .pl-card's — however late the script lands,
   the field fills in DURING this fade rather than starting after it.

   Same entrance as .pl-card but without the scale step: a field that scales up is
   a zoom, not an ignition. */
.pl-embers canvas {
  display: block; width: 100%; height: 100%;
  animation: pl-embers-in 0.62s var(--ease) both;
}
@keyframes pl-embers-in { from { opacity: 0; } to { opacity: 1; } }
.pl-inner { position: relative; z-index: 1; }

.pl-logo-wrap { position: relative; width: 96px; height: 96px; display: grid; place-items: center; }
/* Halo achter het beeldmerk: een zachte gouden gloed die ademt in TEGENFASE met
   het logo zelf. Loopt het mee, dan is het één dikke puls; loopt het tegen, dan
   lijkt het mark te zweven in iets warms. Dat verschil is het hele effect. */
.pl-logo-wrap::before {
  content: ''; position: absolute; left: 50%; top: 50%;
  width: 150px; height: 150px; margin: -75px 0 0 -75px;
  border-radius: 50%; pointer-events: none;
  background: radial-gradient(circle, rgba(255,217,0,0.22) 0%, rgba(255,217,0,0.08) 42%, transparent 70%);
  animation: pl-halo 3.4s var(--ease) infinite;
}
@keyframes pl-halo {
  0%,100% { opacity: 0.55; transform: scale(0.94); }
  50%     { opacity: 1;    transform: scale(1.06); }
}
/* A slow breathe, not a pulse. The old 1.8s scale(0.93)+opacity(0.82) throb
   competed with the ring for attention and read as "system busy"; this is
   barely-there movement that keeps the mark alive without announcing itself. */
.pl-logo { width: 72px; height: 72px; object-fit: contain; animation: pl-breathe 3.4s var(--ease) infinite; }
/* SVG stroked-circle ring: vector-drawn so the browser anti-aliases the arc
   at any size/DPI. Replaced a conic-gradient + hard radial mask, whose hard
   stops (transparent→#000 at the same offset) produced visible stair-stepping. */
/* ALLEEN ROTATIE, GEEN GETEKENDE BOOG MEER.
   De boog "tekende" zichzelf door `stroke-dasharray` te animeren. Mooi, maar
   dasharray is een PAINT-eigenschap: elke frame moet de SVG opnieuw geverfd
   worden. En deze ring zit in .pl-card, die een `backdrop-filter` heeft. In WebKit
   (iPhone) is dat een bekende combinatie waarbij zo'n herverving stilvalt terwijl
   transforms gewoon doorlopen — de ring lijkt dan halverwege te blijven staan.

   Dat is precies wat de eigenaar twee keer meldde, en het is hier NIET na te
   bootsen: op deze machine (Chrome) draaide hij per frame gemeten 5675ms lang
   zonder één stilstand boven 120ms. Een bug die alleen op het toestel van de
   melder bestaat, los je niet op door harder te meten maar door het mechanisme
   weg te nemen dat hem kan veroorzaken.

   Dus: een boog met een VASTE lengte, rondgedraaid met een transform. Transforms
   draaien op de compositor en zijn precies het soort animatie dat iOS wél
   betrouwbaar door laat lopen. `will-change` promoveert de ring bovendien naar een
   eigen laag, zodat hij niet afhankelijk is van het herverven van de kaart
   eronder — daarom hier wél, terwijl het bij de vonken juist weg kon (daar was het
   meetbaar nutteloos; hier is het de kern van de fix).

   Prijs: de boog trekt niet meer samen en uit. Dat is het waard — een spinner die
   altijd draait verslaat een expressievere die soms bevriest. */
.pl-ring {
  position: absolute; inset: 0; width: 100%; height: 100%;
  overflow: visible;
  animation: pl-spin 1.4s linear infinite;
  will-change: transform;
}
.pl-ring circle {
  fill: none; stroke-linecap: round;
  transform-box: fill-box; transform-origin: center;
}
.pl-ring .pl-ring-track { stroke: rgba(255,217,0,0.10); }
/* Vaste boog van ~90 van de 295,3 (omtrek bij r=47): lang genoeg om als beweging
   te lezen, kort genoeg om de rotatie te laten zien. */
.pl-ring .pl-ring-arc   { stroke: var(--gold); stroke-dasharray: 90 206; stroke-dashoffset: 0; }
.pl-word {
  font-family: system-ui, -apple-system, sans-serif; font-size: calc(var(--fs-base) * 0.78);
  letter-spacing: 0.34em; text-transform: uppercase;
  color: rgba(244,239,227,0.62); padding-left: 0.34em;
}

/* Entrance: the chrome arrives rather than being there. Card first, word a beat
   later — a stagger is most of what separates "premium" from "instant". */
.pl-card { animation: pl-in 0.62s var(--ease) both; }
.pl-word { animation: pl-in 0.62s var(--ease) 0.14s both; }

/* Exit: staged, not a flat opacity dump. The card eases UP and out as the
   curtain lifts, so the reveal feels like something opening. Kept under the
   800ms after which master.js removes the node. */
.preloader.pl-done .pl-card { animation: pl-out-card 0.52s var(--ease) forwards; }
.preloader.pl-done .pl-word { animation: pl-out-word 0.4s var(--ease) forwards; }

@keyframes pl-spin  { to { transform: rotate(360deg); } }
@keyframes pl-breathe { 0%,100% { transform: scale(1); opacity: 1; } 50% { transform: scale(0.972); opacity: 0.9; } }
/* Hier stond `pl-arc`: een boog die zichzelf tekende en weer terugtrok door
   `stroke-dasharray` te animeren. Weg, in twee stappen en om twee verschillende
   redenen — allebei het vermelden waard, want ze zijn allebei makkelijk opnieuw
   te maken:
     1. De boog kromp per cyclus tot 10 van de 295,3 — een stip. Bij een gordijn
        van 900ms zag je die cyclus nooit af; bij 5 seconden gebeurt het drie keer
        en leest het als stilstand. Eerst opgelost met een ondergrens van 64.
     2. Dat was niet genoeg: `stroke-dasharray` is een PAINT-eigenschap, en deze
        ring zit in een element met `backdrop-filter`. Op iOS valt zo'n herverving
        stil terwijl transforms doorlopen, en dan bevriest de boog alsnog.
   De ring draait nu met een vaste boog en alleen een transform. Zie .pl-ring. */
@keyframes pl-in       { from { opacity: 0; transform: scale(0.9); }  to { opacity: 1; transform: none; } }
@keyframes pl-out-card { to { opacity: 0; transform: scale(1.07) translateY(-8px); } }
@keyframes pl-out-word { to { opacity: 0; transform: translateY(10px); } }
