
@font-face{font-family:'Proxima Soft Complete';font-weight:400;font-style:normal;font-display:swap;
  src:url('https://sophicly.b-cdn.net/Fonts%20for%20Sophicly%20Website/Proxima%20Soft/ProximaSoft-Regular.woff2') format('woff2')}
@font-face{font-family:'Proxima Soft Complete';font-weight:600;font-style:normal;font-display:swap;
  src:url('https://sophicly.b-cdn.net/Fonts%20for%20Sophicly%20Website/Proxima%20Soft/ProximaSoft-SemiBold.woff2') format('woff2')}
@font-face{font-family:'Playfair Display Complete';font-weight:400;font-style:italic;font-display:swap;
  src:url('https://sophicly.b-cdn.net/Fonts%20for%20Sophicly%20Website/Playfair%20Display/playfairdisplay-italic-webfont.woff2') format('woff2')}

#swf-science{
  --xs:4px; --sm:8px; --md:12px; --lg:16px; --xl:24px;
  --bg:#1c1d1f; --surface:#242628; --raised:#333437; --border:#2e3033;
  --ink:#f4f5f6; --muted:#7f868c; --teal:#51dacf; --purple:#5333ed;
  /* WD-651b — the literals below used to bypass these tokens; every one now reads a token so the theme can flip it */
  --ink-2:#c9cdd1; --purple-lt:#a78bfa; --surface-glass:rgba(36,38,40,.94); --raised-glass:rgba(48,50,55,.9); --label-glass:rgba(51,52,55,.96);
  --surface-gold:#2c2e35; --scrim:#161719; --mint:#7DF9E9; --btn-ink:#1C1D1F;   /* WD-651b retest: opaque — Neil: "that see-through effect, I don't think that's necessary" */
  --standard:cubic-bezier(0.4, 0, 0.2, 1);      /* MOTION.md: Sophicly Standard */
  --entrance:cubic-bezier(0.22, 1, 0.36, 1);    /* MOTION.md: Sophicly Entrance - settles, never overshoots */
  --stops:7;                      /* SEVEN principles — one viewport per stop, plus a tail. Derived
                                     from the list, never typed twice: the module walks NREG = 7. */
}
#swf-science *{box-sizing:border-box}
#swf-science{background:var(--bg)}
#swf-science{margin:0;background:var(--bg);color:var(--ink);
  font-family:'Proxima Soft Complete',system-ui,-apple-system,"Segoe UI",sans-serif;
  -webkit-font-smoothing:antialiased}
#swf-science h1, #swf-science h2, #swf-science h3, #swf-science p{margin:0;padding:0}
#swf-science /* only 400 and 600 are registered — an unstyled h3 or <strong> defaults to BOLD (700) and the
   browser then SYNTHESISES it, #swf-science which fontgate fails and which reads as a slightly wrong face. */
h1, #swf-science h2, #swf-science h3, #swf-science strong{font-weight:600}
#swf-science .sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}

#swf-science .intro{padding:clamp(28px,6vh,64px) clamp(20px,5vw,80px) clamp(8px,2vh,24px);max-width:1400px;margin:0 auto}
#swf-science .intro__eyebrow{font-size:12px;font-weight:600;letter-spacing:.4em;text-transform:uppercase;color:var(--muted)}
#swf-science .intro h1{margin-top:var(--md);font-family:'Playfair Display Complete',Georgia,serif;
  font-style:italic;font-weight:400;font-size:clamp(30px,4.4vw,56px);line-height:1.1;max-width:18ch}
#swf-science .intro p{margin-top:var(--lg);max-width:60ch;color:var(--muted);font-size:clamp(15px,1.5vw,18px);line-height:1.55}
#swf-science .intro .note{margin-top:var(--md);color:var(--teal)}

#swf-science /* ── the scroll section. One viewport per region, #swf-science plus a tail so the last one can be reached. ─── */
.brain{position:relative;height:calc((var(--stops) + 1) * 100svh)}
#swf-science .brain__stage{position:sticky;top:0;height:100svh;overflow:hidden}
#swf-science .brain__canvas{position:absolute;inset:0;width:100%;height:100%;display:block}

#swf-science .brain__dust{position:absolute;inset:0;width:100%;height:100%;display:block;pointer-events:none}

#swf-science /* ⭐ THE SPOTLIGHT. Geometry is nextbricks' (see the markup); the colour, #swf-science the placement and the
   entrance curve are ours. `mix-blend-mode:screen` is what makes it read as LIGHT rather than as a
   grey shape laid on top — a shaft of light adds, #swf-science it never darkens what is under it. */
.brain__spot{position:absolute;left:-14%;top:-30%;width:128%;height:150%;pointer-events:none;
  opacity:0;transition:opacity 1000ms var(--entrance) 300ms;   /* MOTION.md arrival, top tier */
  mix-blend-mode:screen;z-index:1;
  /* ⭐ THE LIGHT COMES IN FROM THE TOP LEFT ONLY (Neil, 2026-08-27: *keep the spotlight in from top
     left only, not bottom right*). Their ellipse is a long diagonal SHAFT — it necessarily has two
     ends, and the far end was landing bottom-right and reading as a second, unexplained light
     source. Masking is the right tool rather than re-deriving their transform: the geometry stays
     verbatim (rule #13) and we simply stop drawing the half we do not want. The mask runs along
     the same diagonal the shaft does, so it fades the tail out instead of cutting it. */
  -webkit-mask-image:linear-gradient(to bottom right,#000 0%,#000 28%,rgba(0,0,0,.35) 52%,transparent 72%);
  mask-image:linear-gradient(to bottom right,#000 0%,#000 28%,rgba(0,0,0,.35) 52%,transparent 72%)}
#swf-science .brain__spot ellipse{fill:#8fb9ff;fill-opacity:.16}
body.is-live #swf-science .brain__spot.is-on{opacity:1}

#swf-science /* the label layer sits over the canvas; each label is positioned from a projected 3D anchor */
.brain__labels{position:absolute;inset:0;pointer-events:none}
#swf-science .lbl{position:absolute;left:0;top:0;will-change:transform,opacity;opacity:0;
  transition:opacity 300ms var(--standard)}
#swf-science .lbl.is-on{opacity:1}
#swf-science .lbl{pointer-events:auto;appearance:none;background:none;border:0;padding:0;margin:0;
  font:inherit;color:inherit;cursor:pointer;display:block}
#swf-science .lbl:focus-visible{outline:none}
#swf-science .lbl:focus-visible .lbl__inner{outline:2px solid var(--teal);outline-offset:3px}
#swf-science .lbl__inner{display:inline-block;transform:translate(-50%,-50%);white-space:nowrap;
  transition:border-color 200ms var(--standard),color 200ms var(--standard),
             box-shadow 200ms var(--standard),scale 200ms var(--standard);
  font-size:11px;font-weight:600;letter-spacing:.16em;text-transform:uppercase;color:var(--ink);
  background:var(--surface-glass);border:0;border-radius:999px;padding:8px 12px}
#swf-science .lbl.is-active .lbl__inner{background:var(--label-glass);color:var(--teal)}
#swf-science /* ⭐ THE HOVER (Neil: *have a nice hover effect or something*). It scales from the label's own
   centre — the pill is already translated -50%, #swf-science -50% to sit ON its anchor, #swf-science so `scale` is used
   rather than a second transform, #swf-science which would fight that translate and make it jump. */
.lbl__inner:hover{border-color:var(--teal);color:var(--teal);scale:1.06;
  box-shadow:0 0 0 1px #51dacf55, 0 6px 26px -6px #51dacf66}
#swf-science .brain__leaders{position:absolute;inset:0;width:100%;height:100%;pointer-events:none}

#swf-science /* ⭐ THE CAPTION SITS TOP-LEFT (Neil, #swf-science 2026-08-27: *the information's currently at the bottom left.
   Do you think that's the ideal place? I feel like I'm not looking there… I feel like I'm more
   likely to look at the top left maybe, #swf-science but I'm not sure*). He is right and the reason is not
   taste: a reader's eye enters a page top-left and the six labels already cluster down the left of
   the model, #swf-science so the caption now sits at the head of the column the eye is already travelling —
   instead of at the end of a journey it has to make on purpose. Bottom-left is where you put a
   caption you are willing to have missed. */
.cap{position:absolute;left:clamp(20px,5vw,80px);top:clamp(76px,12vh,128px);
  max-width:min(24rem,30vw);pointer-events:none}
#swf-science .cap__n{font-size:11px;font-weight:600;letter-spacing:.36em;text-transform:uppercase;color:var(--teal)}
#swf-science .cap h2{margin-top:var(--sm);font-family:'Playfair Display Complete',Georgia,serif;font-style:italic;
  font-weight:400;font-size:clamp(26px,3.4vw,44px);line-height:1.1}
#swf-science .cap p{margin-top:var(--md);color:var(--muted);font-size:clamp(14px,1.4vw,17px);line-height:1.5}
#swf-science .cap__item{position:absolute;left:0;bottom:0;opacity:0;transform:translateY(8px);
  transition:opacity 300ms var(--standard),transform 300ms var(--standard)}
#swf-science .cap__item.is-active{opacity:1;transform:none;position:relative}

#swf-science /* ⭐ THE STYLE SWITCH — a preview control, #swf-science not part of the shipped component. It is here so the
   three looks can be compared with one tap each; the page ships one of them and this comes out. */
.sw{position:absolute;right:clamp(16px,3vw,40px);top:clamp(16px,3vh,40px);display:flex;gap:4px;
  padding:4px;border-radius:999px;background:var(--surface-glass);border:0;
  backdrop-filter:blur(8px)}
#swf-science .sw button{appearance:none;border:0;background:transparent;color:var(--muted);cursor:pointer;
  font:inherit;font-size:11px;font-weight:600;letter-spacing:.14em;text-transform:uppercase;
  padding:8px 16px;border-radius:999px;transition:color 200ms var(--standard),background 200ms var(--standard)}
#swf-science .sw button:hover{color:var(--ink)}
#swf-science .sw button.is-on{background:var(--raised);color:var(--teal)}
#swf-science .sw button:focus-visible{outline:2px solid var(--teal);outline-offset:2px}
#swf-science .sw--look{top:calc(clamp(16px,3vh,40px) + 42px)}

#swf-science /* ⭐ THE STATIC LIST. This is the content. It is real markup, #swf-science it is readable with no script and no
   WebGL, #swf-science and the canvas only ever ENHANCES it — it never replaces it. */
.list{padding:clamp(24px,5vh,56px) clamp(20px,5vw,80px) clamp(48px,10vh,120px);max-width:1400px;margin:0 auto;
  display:grid;grid-template-columns:repeat(auto-fit,minmax(17rem,1fr));gap:clamp(16px,2vw,28px)}
#swf-science .list__card{background:var(--surface);border:0;border-radius:12px;padding:var(--xl)}
#swf-science .list__n{font-size:11px;font-weight:600;letter-spacing:.3em;color:var(--teal)}
#swf-science .list__card h3{margin-top:var(--sm);font-size:1.125rem}
#swf-science .list__card p{margin-top:var(--md);color:var(--muted);font-size:14px;line-height:1.5}

#swf-science /* when the scene is running, #swf-science the static list is a recap below it rather than the main event */
.is-live .list{display:none}
/* ⭐ WD-519 — the seven cards ARE the seven principles, and the brain now walks all seven one per
   swipe, so on a working page they are a verbatim second copy directly beneath the first. They
   stay in the markup and stay visible whenever the scene is NOT live (no JavaScript, no WebGL,
   a lost context) — which is the whole reason they exist. `.intro` is left alone: it is one
   screen of plain words closing the section, and it is what a crawler and a skimming parent read.
   Hidden only under `.is-live`, never removed. */

#swf-science /* ⚠️ THIS RULE IS #B-534's AND DOES NOT APPLY HERE. Its mesh was CC BY 4.0, #swf-science so the credit was a
   licence CONDITION and could not be deleted. This file's mesh comes through Envato Elements, #swf-science which requires no attribution, #swf-science so the credit paragraph is gone and these styles are unused —
   kept only so that re-pointing this file at a CC-licensed model needs markup and not CSS. */
.credit{padding:0 clamp(20px,5vw,80px) clamp(40px,8vh,80px);max-width:1400px;margin:0 auto;
  color:var(--muted);font-size:12px;line-height:1.6}
#swf-science .credit a{color:var(--muted)}

/* ⛔⛔ WD-539 — NO BARE SELECTOR TOKEN MAY DANGLE BEFORE AN @media. A stray `#swf-science` sitting
   here made the at-rule part of an invalid qualified rule's PRELUDE, and the browser absorbed and
   DISCARDED this whole media block with no error — every mobile rule inside was dead on staging
   (Neil's ~404px screenshot: card over the logo, seven labels piling up, desktop rail). Four
   blocks died this way (147/265/349/362). perf-gate.mjs now fails the build on the pattern. */
/* ⭐⭐ MOBILE (Neil asked: *how will this work on mobile as well?*). Three things break on a narrow
   screen and each needs its own answer — none of them is "make the type smaller".
    1. SIX PILLS DO NOT FIT. At 390px they overlap the model and each other and become unreadable
       noise. Only the ACTIVE label is drawn; the other five are still in the DOM, #swf-science still
       focusable, #swf-science still announced — they are visually suppressed, #swf-science not removed.
    2. THE CAPTION GOES BACK TO THE BOTTOM, #swf-science and that is not a contradiction of the ruling above.
       Top-left is right where the eye enters a WIDE page beside a model; on a tall narrow one the
       model fills the upper screen, #swf-science so a caption up there sits ON it. The bottom is also where a
       thumb is. It gets a scrim so it stays legible over the sculpt.
    3. THE MODEL HAS TO BE SMALLER — handled in JS, because it is the camera, not the CSS.
   (WD-539: the height arm is here because phone LANDSCAPE has the same three problems — measured
   4 labels drawn at 844x390 when this block was width-gated only.) */
@media (max-width:767px), (max-height:539px), (max-aspect-ratio:1/1){
  /* ⚠️ every rule here carries the #swf-science prefix ON PURPOSE: the base rules were prefixed
     to id-specificity, so an unprefixed mobile override now LOSES to them regardless of source
     order. Prefix or die. */
  #swf-science .cap{left:0;right:0;top:auto;bottom:0;max-width:none;
    padding:clamp(56px,12vh,112px) 20px clamp(20px,5vh,32px);
    background:linear-gradient(to top,#151617f2 34%,#15161700)}
  #swf-science .cap h2{font-size:clamp(24px,7vw,32px)}
  #swf-science .cap p{font-size:15px}
  /* ⚠️ ONLY THE ACTIVE PILL IS DRAWN — and it must out-specify `.lbl.is-on`, not merely come after
     it. The first cut wrote `.lbl{opacity:0}` inside the media query and four labels stayed
     visible: `.lbl.is-on` is two classes to one, so it wins on SPECIFICITY regardless of source
     order, and a media query does not change that. Cascade order is not a tie-breaker until
     specificity ties. (Caught by the mobile check counting visible labels — a CSS rule existing
     proves nothing about what is on the screen.) */
  #swf-science .lbl.is-on:not(.is-active){opacity:0}
  #swf-science .lbl.is-active{opacity:1}
  #swf-science .lbl__inner{font-size:10px;padding:4px 8px}
  #swf-science .sw{top:auto;bottom:calc(100svh - clamp(16px,3vh,40px));right:12px;transform:translateY(-100%)}
  #swf-science .sw--look{top:auto;bottom:calc(100svh - clamp(16px,3vh,40px) - 42px)}
}
@media (prefers-reduced-motion:reduce){
  #swf-science .lbl, #swf-science .cap__item, #swf-science .lbl__inner{transition:none}
  #swf-science .brain__spot{transition:none;opacity:1}
}
#swf-science .cap{display:none}
#swf-science /* ════════════════════════════════════════════════════════════════════════════════════════════════
   THE PRINCIPLE PILLS — ported from Neil's reference, #swf-science jh3y's "Details & Summary" pen.

   ⭐ RULE #13: THE MECHANISM IS COPIED VERBATIM, #swf-science ONLY THE SKIN IS OURS. Every number that makes the
      morph work is theirs and is unchanged — the 56px pill, #swf-science --speed .5s, #swf-science the 17-stop linear()
      bounce, #swf-science height 56px → fit-content via `interpolate-size`, #swf-science and the -56px margin-top. What is
      restyled is colour, #swf-science type and width, #swf-science because those are Sophicly's and theirs are Apple's.

   ⭐⭐ AND THE ONE DECLARATION THAT ACTUALLY SELLS IT is `margin-top: calc(var(--sizing) * -1)` on
      ::details-content. It pulls the opening panel UP underneath the fading summary, #swf-science so the pill
      appears to BECOME the panel rather than growing a body beneath a header. Delete that single
      line and the whole thing reads as an ordinary accordion. It is the least obvious declaration
      in their stylesheet and the most load-bearing — worth naming, #swf-science because a future tidy-up that
      "removes a negative margin" would silently destroy the effect and leave every timing intact.

   ⚠️ EXCLUSIVITY IS `<details name=principle>`, #swf-science NATIVE. No JavaScript decides which pill is open;
      the script only LISTENS. So with scripting dead or still loading, #swf-science the pills already open and
      close correctly and every principle is readable — the component degrades to a plain
      disclosure list rather than to nothing. */
.principles{
  --speed:.24s; --sizing:56px;   /* x1.6 below = 384ms, inside MOTION.md's 400ms feedback ceiling */ --pillw:clamp(260px,26vw,340px);
  --pill-ease:var(--standard);
  --bounce:var(--entrance);   /* was a linear() spring peaking at 1.0527 - see the note in B538 */
  position:absolute;left:clamp(16px,4vw,64px);top:50%;translate:0 -50%;
  display:grid;gap:.5rem;justify-items:start;z-index:20}

#swf-science .principles details{display:inline-block;border-radius:calc(var(--sizing) * .5);overflow:hidden;
  min-height:var(--sizing);
  /* ⛔ NO HAIRLINE BORDER. BRAND.md, "Things explicitly NOT loved": *thin outline borders on
     default buttons* and *hairline borders around cards, panels or tiles — separate surfaces by
     BACKGROUND CONTRAST*. Both of my first-cut rules had one. The pill now reads as a surface
     because it is LIGHTER than the ground (#1c1d1f Dark Base against the near-black stage), which
     is the documented Sophicly way of doing it and is also what survives a light theme. */
  background:var(--surface);   /* WD-651b: was #242628 */
  -webkit-backdrop-filter:blur(20px) saturate(180%);backdrop-filter:blur(20px) saturate(180%);
  color:var(--ink);
  transition:background .2s var(--pill-ease),box-shadow .2s var(--pill-ease)}
#swf-science /* the hover lifts the SURFACE, #swf-science it does not draw an edge */
.principles details:hover:not([open]){background:var(--raised-glass)}
/* ⛔ WD-674 (Neil, 2026-09-02, phone: "if I click on one or two it actually shows the label of the rail"): NEVER put
   `::details-content` in a selector LIST with anything else. A browser that does not know the pseudo (Safari < 18.4,
   i.e. most iPhones for a while yet) drops the WHOLE rule — here that only cost the height morph, but the rule at
   `.is-rail` below cost the phone its label-hiding, and the open pill showed its content. One selector per rule. */
#swf-science .principles details{interpolate-size:allow-keywords}
#swf-science .principles details::details-content{interpolate-size:allow-keywords}

#swf-science .principles summary{display:inline-flex;gap:.65rem;align-items:center;padding:0 1.3rem;
  border-radius:calc(var(--sizing) * .5);height:var(--sizing);min-height:var(--sizing);
  cursor:pointer;z-index:20;position:relative;white-space:nowrap;font-weight:600;font-size:14px;
  transition-property:opacity,width;
  transition-duration:calc(var(--speed) * .5),calc(var(--speed) * 1.5);
  transition-timing-function:var(--pill-ease),var(--bounce);
  transition-delay:calc(var(--speed) * 1.05),0s}
#swf-science .principles summary::-webkit-details-marker{display:none}
#swf-science .principles summary::marker{content:'';font-size:0}
#swf-science /* ⚠️ THE FOCUS RING STAYS, #swf-science and it is not a contradiction of the no-borders rule. BRAND.md's
   objection is to DECORATIVE hairlines; a focus indicator is the one border a keyboard user
   cannot do without. */
.principles summary:focus-visible{outline:2px solid var(--purple-lt);outline-offset:2px}
#swf-science /* ⭐ #a78bfa is BRAND.md's "Brand Purple Light — the dark-theme purple accent": the same hue as
   #5333ed at a lighter TONE, #swf-science which is what a purple accent is supposed to be on a dark surface.
   Raw #5333ed on near-black fails contrast and reads muddy, #swf-science which is exactly the trap the token
   exists to stop. */
.principles .n{font-size:11px;letter-spacing:.2em;color:var(--purple-lt);font-variant-numeric:tabular-nums}
#swf-science .principles [open] summary{opacity:0;pointer-events:none;transition-delay:0s;width:var(--pillw)}

#swf-science .principles details::details-content{opacity:0;
  transition-property:content-visibility,height,width,opacity;
  transition-duration:var(--speed),calc(var(--speed) * 1.6),calc(var(--speed) * 1.6),calc(var(--speed) * .5);
  transition-behavior:allow-discrete;
  transition-timing-function:var(--pill-ease),var(--bounce),var(--bounce),var(--pill-ease);
  overflow:visible;height:var(--sizing);margin-top:calc(var(--sizing) * -1);
  width:140px;min-height:var(--sizing)}
#swf-science .principles [open]::details-content{height:fit-content;width:var(--pillw);opacity:1;
  transition-delay:0s,0s,0s,calc(var(--speed) * 1)}
#swf-science .principles .content{width:var(--pillw);min-height:var(--sizing);padding:1.15rem 1.35rem 1.35rem}
#swf-science .principles .content h3{font-size:15.5px;margin-bottom:.45rem}
#swf-science .principles .content .n{display:block;margin-bottom:.4rem}
#swf-science .principles .content p{font-size:13.5px;line-height:1.58;color:var(--muted)}

#swf-science /* the walk controls — theirs in shape: 36px circles, #swf-science present only while a pill is open */
.pctl{position:absolute;display:grid;place-items:center;width:36px;aspect-ratio:1;padding:0;
  border-radius:50%;background:var(--surface);
  -webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);
  border:0;color:var(--ink);cursor:pointer;z-index:21;
  transition:opacity .2s var(--standard),transform .2s var(--standard),background .2s var(--standard)}
#swf-science .pctl:hover{background:var(--raised)}
#swf-science .pctl:focus-visible{outline:2px solid var(--purple-lt);outline-offset:2px}
#swf-science .pctl svg{width:17px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
#swf-science /* ⚠️ HIDDEN, #swf-science NOT ABSENT — and disabled with it. A control that is only faded out still takes
   clicks and still lands in the tab order, #swf-science so a keyboard reader meets three unlabelled buttons
   that appear to do nothing. */
.principles:not(:has([open])) .pctl{opacity:0;transform:translateY(.75rem);pointer-events:none;
  visibility:hidden}
#swf-science .pctl[data-a=prev]{right:-52px;top:calc(50% - 58px)}
#swf-science .pctl[data-a=next]{right:-52px;top:calc(50% - 18px)}
#swf-science .pctl[data-a=exit]{right:-52px;top:calc(50% + 22px)}

/* ⭐ MOBILE: the column becomes a tray at the bottom. On a narrow screen a left rail would sit on
   top of the brain, and the brain is the thing being explored. (⛔ no dangler before @media) */
@media (max-width:900px){
  .principles{left:0;right:0;top:auto;bottom:0;translate:0 0;
    padding:0 16px calc(16px + env(safe-area-inset-bottom));justify-items:stretch;
    --pillw:100%}
  .principles details{width:100%}
  .pctl[data-a=prev]{right:14px;top:-50px}
  .pctl[data-a=next]{right:58px;top:-50px}
  .pctl[data-a=exit]{right:102px;top:-50px}
}

#swf-science /* ⚠️ THE CONTROLS MOVED OUT OF THE BRAIN. Sitting them at right:-52px of the pill column put all
   three ON the model, #swf-science and with a pill open they landed on top of its panel and its label — three
   dark circles floating over the artwork. They now sit UNDER the stack, #swf-science in the column's own flow, #swf-science where they cannot collide with anything however wide the open panel gets. */
.principles{position:relative}
#swf-science .principles .pctl{position:static;display:inline-grid}
#swf-science .principles .pctl-row{display:flex;gap:.5rem;margin-top:.35rem;
  transition:opacity .2s var(--standard),transform .2s var(--standard)}
#swf-science .principles:not(:has([open])) .pctl-row{opacity:0;transform:translateY(.75rem);
  pointer-events:none;visibility:hidden}
#swf-science .principles:not(:has([open])) .pctl{opacity:1;transform:none;visibility:visible}
#swf-science .principles{position:absolute}


#swf-science /* ⛔⛔ NEVER WRITE THE LITERAL CLOSING STYLE TAG INSIDE CSS SPLICED INTO A PAGE — not even inside a
      CSS COMMENT. An HTML parser ends a style element at the first occurrence of that string, #swf-science full stop: CSS comments do not exist to it, #swf-science because it has not started parsing CSS yet.
      Writing it in a comment here terminated the stylesheet early and every rule BELOW the comment
      was reparsed as document text — so the rail silently stopped applying while the file still
      contained all thirteen of its rules and nothing errored. It is the same class as this file's
      backtick guard: a delimiter mentioned in prose is still a delimiter.
/* ── the rail: six numbers, #swf-science no prose ───────────────────────────────────────────────────────────
   ⚠️⚠️ EVERY SELECTOR HERE IS ID-QUALIFIED, #swf-science AND THAT IS NOT STYLE PREFERENCE. This block is spliced
      at the FIRST closing style tag in the document, #swf-science which is an EARLIER block than the one holding
      principles.css — so on a specificity TIE the original rule wins on source order. Measured, #swf-science the
      first cut lost exactly that way: '.principles.is-rail details' (0, #swf-science 2, #swf-science 1) tied with
      '.principles details[open]' (0, #swf-science 2, #swf-science 1), #swf-science the open pill kept expanding to full width, #swf-science and its
      number sat at x=220 while its neighbours sat at x=120. '#principles' makes it (1, #swf-science x, #swf-science y), #swf-science which
      cannot tie with anything class-based.
   ⚠️ AND THE OPEN SUMMARY MUST BE FORCED BACK TO opacity 1. jh3y's morph deliberately fades the
      summary out as the panel opens — that is what makes the pill appear to BECOME the panel. In
      rail mode there is no panel, #swf-science so the fade just deletes the number of the one principle the
      reader has selected: measured at opacity 0 on the open pill and 1 on every other. */
#principles.is-rail{gap:.45rem}
#swf-science /* ⚠️⚠️ display:block, #swf-science NOT grid — measured. With 'display:grid;place-items:center' on the details, #swf-science a CLOSED pill put its summary in a SECOND COLUMN: the details box spanned x 58-104 and its own
   summary sat at x=105, #swf-science outside the parent's overflow:hidden, #swf-science so the digit was clipped away
   entirely. The open pill looked fine only because its disclosure content was already hidden, #swf-science so
   it had one grid item instead of two. Nothing errored and the computed colour, #swf-science opacity and
   font-size of the invisible digits were all perfectly correct — which is why "is it visible?"
   has to be answered by GEOMETRY, #swf-science not by reading opacity off the element. */
#principles.is-rail details, #swf-science #principles.is-rail details[open]{width:46px;min-width:46px;max-width:46px;height:46px;
  min-height:46px;border-radius:50%;display:block;overflow:hidden;padding:0}
#swf-science #principles.is-rail summary{display:flex;align-items:center;justify-content:center;
  width:46px;height:46px;min-height:46px;padding:0;gap:0;border-radius:50%;
  opacity:1;transition:none}
#swf-science /* the NAME is the second span; the number is the first. Hiding the name is the whole rail. */
#principles.is-rail summary span:last-child{display:none}
#swf-science #principles.is-rail summary .n{font-size:12px;letter-spacing:.04em;opacity:.7;color:var(--ink)}
#swf-science #principles.is-rail details[open]{background:#5333ed}
#swf-science #principles.is-rail details[open] summary .n{opacity:1;color:#fff}
#swf-science #principles.is-rail details:hover:not([open]){background:var(--raised)}
#swf-science /* ⚠️ the disclosure body must not paint in rail mode — the text lives on the brain now — but it
   stays in the DOM for the no-script path and for anything reading the page. Hidden in EVERY
   state, #swf-science not just [open]: a closed one still takes part in layout. */
#principles.is-rail .content{display:none}                                   /* WD-674: its own rule — see the note at the top of this block */
#swf-science #principles.is-rail details::details-content{display:none}
#swf-science #principles.is-rail .pctl-row{margin-top:.5rem}
#swf-science #principles.is-rail details:hover, #swf-science #principles.is-rail details:focus-within{width:auto;max-width:none;border-radius:999px}
#swf-science #principles.is-rail details:hover summary, #swf-science #principles.is-rail details:focus-within summary{width:auto;padding:0 1.15rem 0 .9rem;gap:.6rem;
  justify-content:flex-start}
#swf-science #principles.is-rail details:hover summary span:last-child, #swf-science #principles.is-rail details:focus-within summary span:last-child{display:inline;font-size:13px;
  font-weight:600;letter-spacing:0;text-transform:none;white-space:nowrap}

#swf-science /* ── the explainer, #swf-science on the brain ───────────────────────────────────────────────────────────── */
.brain__card{position:absolute;left:0;top:0;width:min(300px,62vw);padding:.95rem 1.15rem 1.1rem;
  border-radius:12px;background:var(--surface-glass);
  -webkit-backdrop-filter:blur(20px) saturate(180%);backdrop-filter:blur(20px) saturate(180%);
  color:var(--ink);z-index:22;pointer-events:none;opacity:0;
  transition:opacity .3s var(--entrance)}
#swf-science .brain__card.is-on{opacity:1}
/* ⭐⭐ WD-589 — THE PROVISIONAL SPOT. Before the module's render loop exists there is nothing to
   project the card's anchor from, and its rest state is `left:0;top:0` — which is exactly the
   top-left flash Neil reported. boot-reveal.js adds `.is-provisional` when it paints principle 01,
   and the loop skips its per-frame left/top writes while the class is on; the brain-ready handoff
   removes it under a 0.3s opacity dip. Stage-centred: pre-brain the centre is empty, so the card
   reads as the stage's caption. Media-gated to the COMPLEMENT of the bottom-sheet block at ~416
   (`max-width:719px, max-height:539px, max-aspect-ratio:1/1`) — this rule outguns that one on
   specificity, so it must never be live on a sheet viewport. (A perfectly square viewport matches
   both 1/1 gates; it gets the centred card pre-brain and self-heals to the sheet at handoff.) */
@media (min-width:720px) and (min-height:540px) and (min-aspect-ratio:1/1){
  #swf-science .brain__card.is-provisional{left:50%;top:50%;transform:translate(-50%,-50%)}
}
#swf-science .brain__card .n{display:block;font-size:11px;letter-spacing:.2em;color:var(--purple-lt);
  font-variant-numeric:tabular-nums;margin:0 0 .3rem}
#swf-science .brain__card h3{margin:0 0 .45rem;font-size:17px;font-weight:600;line-height:1.25}
#swf-science .brain__card p{margin:0;font-size:13.5px;line-height:1.5;color:var(--muted)}

/* ⭐⭐ WD-579 — THE CARD SWAPS, IT DOES NOT CUT. Neil: "when you're clicking on the rail at the
   bottom, they just sort of quickly switch between each other... I want a really nice smooth
   transition. If Michael Sevilla were building this for his Risen church website, how would he
   do it?"

   ⭐ ANSWERED FROM HIS SOURCE, NOT FROM TASTE (`reference_wwmsd_what_would_michael_sevilla_do`;
   root §14b: open the reference first and write the numbers down). The mirror is at
   `Menus for Sophicly/references/risen-church-2015/risenchurch.com/`. What it actually says:

     · `cubic-bezier(0.23,1,.32,1)` is his curve — 300+ declarations use it and almost nothing else.
     · `.aboutFUI3` IS his content reveal: `opacity:0; transform:translateY(10px)` + that curve.
       Content ARRIVES FROM BELOW, by 10px. (style.css)
     · Things LEAVE UPWARD: `TweenMax.to(t,0,{autoAlpha:0,y:-8,...})`. (theme.js)
     · A SWAP ladders at +0.1s (`svlaBB2` delay .2 / `svlaBB1` delay .3, and again .5/.6). His
       +0.2s ladder (.5/.7/.9) is the FIRST-LOAD hero, which this is not — a rail tap is an
       interaction, so it takes the swap cadence, not the entrance one.
     · Duration: 500ms is his most-used value by a distance (82 declarations); 1500ms is reserved
       for the big page reveal. Closing/《leaving》 is quicker — `closeShareMod` uses .3s.
     · ⚠️ In his JS every tween is `TweenMax.to(el, 0, {...})` — duration ZERO. The motion lives in
       the CSS class and the JS only sets the end state and the DELAY. That is why this is written
       as CSS transitions driven by a class, and not as a JS animation.

   ⇒ OUT 300ms upward -8px, all three lines together · content swaps · IN 500ms from +10px on a
     +0.1s ladder (number, then heading, then the paragraph).

   ⚠️ THE HEIGHT IS PART OF THE MOTION. The card is a bottom-anchored sheet and the seven texts are
   different lengths (131px vs 151px measured), so swapping content JUMPS its top edge. Fading text
   over a jumping box is not a smooth transition, so the height is carried on the same curve. */
#swf-science #brainCard{transition:height 500ms cubic-bezier(.23,1,.32,1)}
#swf-science #brainCard > *{opacity:0;transform:translateY(10px);
  transition:opacity 500ms cubic-bezier(.23,1,.32,1), transform 500ms cubic-bezier(.23,1,.32,1)}
#swf-science #brainCard.is-on > *{opacity:1;transform:none}
/* his +0.1s swap ladder, in reading order */
#swf-science #brainCard.is-on > *:nth-child(1){transition-delay:0s}
#swf-science #brainCard.is-on > *:nth-child(2){transition-delay:.1s}
#swf-science #brainCard.is-on > *:nth-child(3){transition-delay:.2s}
/* The outgoing text leaves TOGETHER and upward — he never ladders a hide.
   ⚠️ `:nth-child` IS REQUIRED HERE, and leaving it off was a measured bug, not a tidiness issue.
   The ladder rules above are `.is-on > *:nth-child(n)`, which outranks a plain `.is-swapping > *`
   on specificity — so the outgoing lines kept their 0s/.1s/.2s delays and the third was still at
   0.56 opacity when the text was replaced underneath it, giving a visible pop. Matching the
   selector shape lets source order decide, and all three now finish inside the 300ms. */
#swf-science #brainCard.is-swapping > *:nth-child(1),
#swf-science #brainCard.is-swapping > *:nth-child(2),
#swf-science #brainCard.is-swapping > *:nth-child(3){opacity:0;transform:translateY(-8px);
  transition-duration:300ms;transition-delay:0s}
@media (prefers-reduced-motion:reduce){
  /* ⚠️ NOT `transition:none` on the children — they rest at opacity:0, so killing the transition
     without also asserting the end state leaves the card permanently BLANK (§4d: a refusal must
     still say what the reader sees). Assert the end state, remove only the motion. */
  #swf-science #brainCard,
  #swf-science #brainCard > *{transition:none}
  #swf-science #brainCard.is-on > *,
  #swf-science #brainCard.is-swapping > *{opacity:1;transform:none;transition:none}
}
/* ⚠️ ON A NARROW SCREEN THE BRAIN IS TOO SMALL TO CARRY A PARAGRAPH, so the card stops chasing the
   anchor and becomes a bottom sheet. The JS checks the same breakpoint and skips positioning —
   one number, declared twice, so keep them in step. (⛔ no dangler before @media; prefixed to beat
   the id-level base rule.) */
@media (max-width:719px), (max-height:539px), (max-aspect-ratio:1/1){
  #swf-science .brain__card{left:12px;right:12px;top:auto;bottom:12px;width:auto;max-width:none;
    /* WD-594 (hypothesis, stated as such — not measured on his phone): the sheet's height tween
       and its text ladder run under a 20px backdrop blur over a canvas that repaints every frame,
       which on a mobile GPU is a full re-composite of the blur per frame. The sheet's own fill is
       94% opaque on a near-black stage, so the blur was buying nothing a reader could see. Off
       here; the desktop card (which moves) keeps it. */
    -webkit-backdrop-filter:none;backdrop-filter:none}
}

#swf-science #principles.is-rail details:hover, #swf-science #principles.is-rail details:focus-within{width:fit-content;max-width:min(320px,58vw)}

/* ⭐⭐ SHORT COUNTS AS SMALL, and this is a HEIGHT bug wearing a width bug's clothes. A phone in
   LANDSCAPE is 844 x 390: too WIDE for the phone breakpoint, so it kept the desktop vertical
   column - and a column of seven 44px pills plus their gaps is 430px tall inside a 390px viewport,
   sitting on top of the card. Measured, on both builds: v1 (six) was already 377px in that
   viewport and overlapping; the seventh only made an existing defect impossible to miss. So the
   rail switches to the horizontal row on a short viewport as well as a narrow one.
   ⚠️ 539px is chosen to sit under every phone landscape height (375-430) and well clear of iPad
      portrait (768) and any laptop, so no device that has room for the column loses it.
   ⛔ the prelude is a MEDIA QUERY LIST — selector junk in a member parses as not-all and kills
      that member silently (a stray prefix here disabled the landscape arm). Gated. */
@media (max-width:719px), (max-height:539px), (max-aspect-ratio:1/1){
  /* #B-572 — Neil, with a screenshot: *"the rail on mobile needs a bit of space. It's overlapping
     the text a little bit."* The rail sat 12px off the bottom at 44px tall, and the card sat at
     bottom:70px - a 14px gap between two separate things, while the card's own internal padding is
     24px. BRAND.md's proximity rule is exactly this: *the gap BETWEEN two sections must be visibly
     larger than any gap INSIDE either one*, so 14px reads as touching however correct it measures.
     ⭐ The clearance is now COMPUTED from the rail's own numbers rather than typed, so changing the
        pill size cannot silently re-close the gap. left/right/gap move onto the 4px scale too. */
  /* ⚠️ WD-578 — `--rail-gap` IS DECLARED HERE, ON THE RAIL, so a value written to :root by JS loses
     to this local declaration, and `#brainCard` (a sibling, not a descendant) never inherited it at
     all — its `var(--rail-gap, 12px)` silently resolved to the fallback every time. The first cut of
     the nav clearance set the root var, changed nothing, and the gate caught it.
     ⇒ ONE root-level var, `--swf-rail-gap`, is the single source both read. JS writes only that. */
  #principles.is-rail{--rail-h:44px; --rail-gap:var(--swf-rail-gap, 12px);
    /* ⚠️ THE RAIL'S BOX IS TALLER THAN ITS PILLS. The portrait rule below gives it a bottom padding
       (16px + the safe area), so the box measures 60px for a 44px pill - and the first cut of the
       clearance below computed from the PILL height, landed 8px short, and the gate caught it. The
       total is written ONCE here and both the rail and the card read it. Also: the safe-area inset
       belongs to the PADDING, not to the offset, or it is counted twice. */
    --rail-total:calc(var(--rail-h) + 16px + env(safe-area-inset-bottom));
    position:fixed;left:12px;right:12px;bottom:var(--rail-gap);
    top:auto;translate:none;
    display:flex;flex-direction:row;justify-content:center;align-items:center;gap:8px;z-index:23}
  #principles.is-rail details,
  #principles.is-rail details[open],
  #principles.is-rail details:hover,
  #principles.is-rail details:focus-within{width:44px;min-width:44px;max-width:44px;height:44px;
    min-height:44px;border-radius:50%}
  #principles.is-rail summary,
  #principles.is-rail details:hover summary,
  #principles.is-rail details:focus-within summary{width:44px;padding:0;gap:0;
    justify-content:center;border-radius:50%}
  /* the name stays hidden on a phone even when focused — the card names the principle anyway */
  #principles.is-rail details:hover summary span:last-child,
  #principles.is-rail details:focus-within summary span:last-child{display:none}
  /* the rail IS the navigation here; nine 44px targets do not fit across 390px */
  #principles.is-rail .pctl,
  #principles.is-rail .pctl-row{display:none}
  /* ⚠️ id-qualified: the original card mobile rule sets bottom:12px and is LATER in the file, so a
     class-level rule here loses the tie and the card sat on top of the rail. */
  /* rail gap + rail height + a full xl step, so the space BETWEEN card and rail beats the space
     inside the card (24px padding). Derived, not typed. */
  /* double-id: must now ALSO beat the prefixed `#swf-science .brain__card{bottom:12px}` sheet rule */
  /* ⭐⭐ WD-578 — READ THE RAIL'S OWN GAP, DO NOT RE-TYPE IT. This line said `12px` where the rail
     says `bottom:var(--rail-gap)`, so the two agreed only by coincidence. The moment the rail has to
     move (it now lifts over the floating nav on a portrait tablet) a typed 12 leaves the card
     behind and the rail rises straight into it. The comment above already claimed this chain was
     "derived, not typed" — one number in it was typed, and that is exactly the half that broke. */
  #swf-science #brainCard{bottom:calc(var(--swf-rail-gap, 12px) + var(--rail-h, 44px) + 16px + env(safe-area-inset-bottom) + 24px)}
}

/* ⭐ WD-580 — ONE INSET, DECLARED ONCE, READ BY THE TRIGGER AND BY EVERY GUTTER THAT HAS TO CLEAR
   IT. 12px is `md` on BRAND.md's 4px scale and is already the page's own edge — `#brainCard` and
   `#principles.is-rail` are both `left:12px;right:12px`. Declared on the BODY so the notes plugin's
   own elements (which live outside `#swf-science`) inherit it. */
body.swf-page-science{--swf-notes-inset:12px}

/* ⚠️ THE VERTICAL TAB IS PULLED OFF THE EDGE TOO, AND THAT FORCES ONE DISCLOSED CHANGE (§13). The
   ported tab is `right:0`, flush, with radius on its LEFT corners only — correct for something
   fused to the screen edge. Inset by 12px it is no longer fused, and square outer corners against
   open background read as a rendering fault rather than a decision, so the outer pair is rounded to
   match. That is the minimum needed to avoid CREATING a defect while satisfying the alignment Neil
   asked for; nothing else about the ported component is touched.
   ⛔ Do NOT "simplify" this by returning the tab to right:0 — the mismatch between its margin and
   the card's is exactly what he reported. */
body.swf-page-science #snTabTrigger{right:var(--swf-notes-inset)}
body.swf-page-science #snTabTrigger .sn-tab-btn .btn-main__outer{border-radius:10px}

/* ⭐⭐ WD-575 — THE NOTES FAB SITS ON THE CARD ON A PHONE. Neil, phone pass at 1.119.0: *"the notes
   tab is sitting ON TOP of the information. So it needs to just be pushed higher up so it's not
   overlapping the information about the programme."*

   WHOSE BUTTON IT IS: `sophicly-notes` hides its 46x168 right-edge tab below 768px and swaps in a
   56x56 FAB (`#snFabTrigger.sn-fab-trigger`, sophicly-notes.css:312) at `bottom:148px;right:24px`.
   That plugin is not ours, and its number is right on every OTHER page — the collision is this
   page's bottom sheet, so the override is scoped to this page and the plugin is left alone.

   ⭐ THE ARITHMETIC, before any code (root CLAUDE.md §ALIGNMENT/GEOMETRY). Everything in this
   layout is bottom-anchored, so measure every band from the viewport's bottom edge:
       rail          12 -> 72          (12px offset + a 60px box)
       card BOTTOM   96 + safe-area    (the derived rule directly above)
       card TOP      96 + card height  (~234 at principle 01, 419x902)
       FAB           148 -> 204        <- wholly inside 96 -> 234
   So it is not a near miss, it is inside the card band BY CONSTRUCTION.

   ⛔ AND A TYPED NUMBER WOULD FIX 01 AND RE-BREAK 03. The card is a bottom sheet: it grows UPWARD
   as the principle text gets longer, and those seven texts were last rewritten yesterday (WD-572).
   Any constant tuned to one principle is a fossil. So the offset is DERIVED from the card's live
   rect by a ResizeObserver (science-panels.js) and lands here as `--swf-fab-bottom`.

   ⚠️ THE CLAMP AGAINST THE DOORS STAYS, even though this block is portrait-only. It is cheap, and
   the band it protects is real: at iPad portrait the doors end at y141 and the lift lands the button
   at y717, but a future card long enough to fill the screen would otherwise push it through them.
   ⛔ IT IS NOT WHAT SAVES LANDSCAPE — I first assumed it was, from a paper estimate of a ~76px band.
   MEASURED, that band is 37px for a 168px tab: unsatisfiable, and no clamp rescues it. Landscape is
   handled horizontally instead, by the gutters in WD-575b lower down this file.

   THE FALLBACK IS NOT DECORATION (§4d — a refusal is half a change). If the observer never runs, a
   var with no value would fall back to the plugin's 148px, i.e. straight back onto the card. The
   fallback therefore re-states the card's own offset in pure CSS and allows a generous 200px card,
   so the button is clear of the text even with no JS at all. */
/* ⚠️ PORTRAIT ONLY, and the prelude is load-bearing. In LANDSCAPE there is no vertical room to lift
   into (see WD-575b below) and the fallback would have thrown the button off the top of a 375px-tall
   viewport. `max-width:768px` is the notes plugin's OWN swap breakpoint, so this block cannot fire
   for a trigger that is not the FAB. The JS carries the identical predicate. */
@media (max-width:768px) and (orientation:portrait){
  body.swf-page-science #snFabTrigger.sn-fab-trigger{
    --swf-card-bottom:calc(var(--swf-rail-gap, 12px) + var(--rail-h, 44px) + 16px + env(safe-area-inset-bottom) + 24px);
    /* 24px = the card's own internal padding, so the gap BETWEEN the two beats the gap INSIDE the
       card and they read as separate things (BRAND.md proximity, the same rule as the rail gap). */
    --swf-fab-gap:24px;
    /* ⭐ WD-580 — SHARE THE CARD'S RIGHT EDGE. Neil: "its alignment to the edge of the screen is
       different in terms of the spacing... ideally be aligned with the information containers."
       BRAND.md §4 puts BOTH on the 4px scale — the card at `md` 12px, the plugin's FAB at `xl`
       24px — so neither was off-brand; they simply disagreed, and two different right margins on
       one screen is what he can see. The card's 12px wins because it is the page's own inset,
       set on `#brainCard` and the rail alike. */
    right:var(--swf-notes-inset);
    bottom:var(--swf-fab-bottom, calc(var(--swf-card-bottom) + 200px + var(--swf-fab-gap)));
  }
}

/* ═══ WD-519 — THE TWO DOORS, AND THE PANELS THEY OPEN ════════════════════════════════════════
   The programme page is this section and nothing else now, so price and "how it runs" have to be
   reachable from inside the pinned brain. */
#swf-science .brain__acts{position:absolute;left:clamp(16px,3vw,40px);bottom:clamp(16px,3vh,40px);
  z-index:6;display:flex;gap:10px;flex-wrap:wrap}
/* ⭐ WD-530 — THE DOORS ARE BRAND BUTTONS NOW. Neil: "those are not great, they're not within our
   brand guidelines." He was right, and BRAND.md names both faults exactly:
     · "Thin outline borders on default buttons" — the old pill was a 1px white outline;
     · glassmorphism as a default — it was a translucent white wash with a blur, chosen for nothing.
   These are the documented HALO BUTTON (BRAND.md §Buttons): solid brand fill, Proxima Soft 700
   uppercase, our rounded-square 14px rather than a full pill, and on hover a same-colour ::after
   that scales scaleX(1.35) scaleY(1.55) and fades out — no lift, retired site-wide 2026-07-26.
   Everything on .5s cubic-bezier(.22,1,.36,1), the brand curve.
   HIERARCHY, deliberately: the price is the question a parent came with, so it takes the teal fill
   (dark-theme halo per BRAND.md); "How it runs" is the same button on the raised surface step
   (#242628 on the near-black stage) so the two do not compete, and so neither fights the nav's own
   teal BOOK SESSION. Surfaces separate by CONTRAST, never by an outline.
   ⭐⭐ WD-570 — THE OMISSION IS OVER. The note that used to sit here said the two-copy roll was
   left out because a duplicated label is read twice by textContent (regression ledger
   `reference_sb_roll_button_textcontent_doubles_the_label`). That trap is real, but dropping the
   feature was the wrong answer to it — Neil: "the halo button is not just the halo, it's the halo
   AND the text rolling over." The right answer is the one THIS PLUGIN ALREADY USES in
   swf_cta_block(): duplicate the label and mark the second copy aria-hidden. Rules below are
   `.swf-btn__roll`'s, reused rather than re-derived (Gate 0: reuse before port). */
#swf-science .bact{position:relative;appearance:none;border:0;isolation:isolate;touch-action:manipulation;
  /* ⚠️ NOT the `font:` shorthand — MEASURED at weight 500. `--sans` is not declared in this scope,
     so `font: 700 13px/1 var(--sans)` was invalid at computed-value time and the whole declaration
     was dropped, silently taking the 700 with it. Longhand cannot fail that way. */
  font-family:'Proxima Soft Complete',-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;
  font-weight:700;font-size:13px;line-height:1;letter-spacing:.08em;text-transform:uppercase;
  padding:15px 22px;min-height:46px;border-radius:14px;cursor:pointer;
  background:var(--surface);color:var(--ink);
  transition:background .5s cubic-bezier(.22,1,.36,1),color .5s cubic-bezier(.22,1,.36,1)}
/* ⭐⭐ WD-566 — THE HALO WAS DEAD, AND THE BUG WAS ARITHMETIC, NOT TASTE. Neil: "that button is
   supposed to have the halo effect. It doesn't have the halo effect at all."
   BRAND.md §Halo button: "a same-colour ::after copy that SCALES scaleX(1.35) scaleY(1.55) and
   FADES opacity->0". A fade TO zero has to start ABOVE zero — this rule had `opacity:0` at rest
   AND `opacity:0` on hover, so the copy scaled from invisible to invisible and nothing was ever
   drawn. It is invisible at rest anyway without being transparent, because it is the same colour,
   the same size and behind the button (z-index:-1); it only becomes visible as it grows past the
   button's own edges, which IS the halo. */
#swf-science .bact::after{content:'';position:absolute;inset:0;z-index:-1;border-radius:inherit;
  background:var(--surface);opacity:1;
  transition:transform .5s cubic-bezier(.22,1,.36,1),opacity .5s cubic-bezier(.22,1,.36,1)}
#swf-science .bact:hover::after,#swf-science .bact:focus-visible::after{
  transform:scaleX(1.35) scaleY(1.55);opacity:0}
#swf-science .bact:hover{background:var(--raised)}
#swf-science .bact--go{background:var(--teal,#51dacf);color:var(--btn-ink,#1C1D1F)}
#swf-science .bact--go::after{background:var(--teal,#51dacf)}
#swf-science .bact--go:hover{background:var(--mint,#7DF9E9)}
#swf-science .bact:focus-visible{outline:2px solid #a78bfa;outline-offset:3px}
/* the halo has to be able to leave the button, so the row cannot clip it */
#swf-science .brain__acts{overflow:visible}
@media (max-width:767px), (max-height:539px), (max-aspect-ratio:1/1){
  /* the bottom of a small screen belongs to the card sheet + the rail, so the doors go to the top */
  #swf-science .brain__acts{bottom:auto;top:clamp(12px,2vh,20px);left:clamp(12px,4vw,20px)}
  #swf-science .bact{padding:11px 14px;font-size:13px}
}

/* ⭐⭐ WD-542 — THE PANEL IS A FULL-SCREEN TAKEOVER NOW, WWMSD-ANSWERED FROM THE RISEN SOURCE.
   Neil: "I don't like the style. I don't like the way they animate in. They just sort of pop in…
   they need to complement our style… really nice and stylish and minimalistic" + "our brand
   guidelines". Sevilla's own overlay (the Risen menu) is the model, read from his mirror, not from
   taste: a full-screen surface on the dark ground, content as a typographic column, every item a
   WHOLE-ELEMENT fade from translateY(10px) on a staggered delay ladder, master ease
   cubic-bezier(.23,1,.32,1) — ours is the brand's own Entrance (.22,1,.36,1), the house twin.
   The floating grey sheet, its raised title bar and its backdrop blur are gone; type does the
   hierarchy (tracked caps eyebrow → Playfair italic display title → rows), no hairlines.
   This also answers his "not practical": on a phone or iPad a full-screen surface with page-normal
   scrolling is simply a page, not a box floating in a box.
   Scroll isolation is still all three SOP layers: overscroll-behavior on the takeover (the one
   scroller), the body scroll-locked by JS while open, and [hidden] display:none.
   ⚠️ WD-532 ("centred, not a bottom sheet") is SATISFIED, not overturned: the column is centred
   in the viewport; what died is the floating card, not the centring. */
#swf-science .bpanel{position:fixed;inset:0;z-index:2147483000;background:var(--scrim);
  overflow-y:auto;overscroll-behavior:contain;-webkit-overflow-scrolling:touch;
  opacity:0;transition:opacity .5s var(--entrance)}
#swf-science .bpanel.is-in{opacity:1}
#swf-science .bpanel[hidden]{display:none!important}
/* ⚠️ WD-557 — the vertical padding was 99px top + 90px bottom at 900h, which is 189px of air in a
   takeover whose content already wanted 132px more than the viewport. Trimmed to a range that
   still breathes at the top of a long column but stops pushing a short one off the bottom. */
#swf-science .bpanel__sheet{max-width:1120px;margin:0 auto;
  padding:clamp(40px,5.4vh,64px) clamp(20px,5vw,64px) clamp(40px,5.4vh,64px)}
/* the body is plain flow inside the takeover — the takeover itself is the scroller */
#swf-science .bpanel__body{margin-top:clamp(20px,3.2vh,34px)}
/* ⭐⭐ WD-545 — THE CLOSE IS THE WORD, PORTED FROM THE MOBILE LANDING PAGE. Neil: "the x button is
   not optimal… look at how the close button is styled on the mobile landing page. Can we borrow
   anything from that rather than just having a generic x?"
   Opened the file he named (CSS Shine Effect/Sophicly Mobile Landing - BRANDED v85 FUNNEL SHIP.html,
   `.sophClose`, line 235) — and the answer is that his close is not an icon at all, it is the
   lowercase WORD "close". Ported verbatim per rule #13: font-size 22px, line-height 27px, white on
   the dark ground, `transition: all .2s ease-out`, and it arrives from ABOVE (his GSAP tweens it in
   at y:-16 and out at y:-12), which is why the entrance below is a downward settle.
   STATED DEVIATIONS, both forced by the container: it is FIXED top-right rather than absolute at
   top:10px/left:6px (his sits in the burger's slot, which this takeover does not have), and it
   keeps `aria-label` duties via its own visible text, so the old aria-label is redundant and gone.
   No circle, no plate, no ×: a word needs none of them, which is the whole point of borrowing it. */
#swf-science .bpanel__x{position:fixed;right:clamp(16px,3vw,34px);top:clamp(14px,2.4vh,26px);
  z-index:3;appearance:none;background:none;border:0;padding:6px 2px;cursor:pointer;
  font-family:'Proxima Soft Complete',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
  font-size:22px;line-height:27px;font-weight:400;letter-spacing:.01em;color:var(--ink);
  transition:all .2s ease-out}
#swf-science .bpanel__x:hover{color:var(--teal)}
#swf-science .bpanel__x:focus-visible{outline:2px solid var(--teal);outline-offset:4px;border-radius:4px}
/* the dialog takes focus on open (see science-panels.js) and must not draw a ring for it */
#swf-science .bpanel:focus,#swf-science .bpanel:focus-visible{outline:none}
#swf-science .bpanel__body > *{margin-top:0!important;margin-bottom:0!important}

/* ⚠️ DELIBERATELY UNSCOPED — legacy guards for page components that once moved into the panels;
   both panels ship their own content now, but if a chrome blob still renders these ids they must
   never flash as full sections. */
#compare, #swf-timetable, #swf-stages{display:none}

/* the takeover's heading is typographic, not a bar: tracked caps, then the display line */
#swf-science .bpanel__eyebrow{margin:0 0 12px;font-size:11px;font-weight:600;letter-spacing:.36em;
  text-transform:uppercase;color:var(--teal)}
#swf-science .bpanel__title{margin:0;padding:0;background:none;
  font-family:'Playfair Display Complete',Georgia,serif;font-style:italic;font-weight:400;
  font-size:clamp(30px,3.4vw,46px);line-height:1.08;letter-spacing:0;color:var(--ink)}
#swf-science .bpanel__body .section-title,
#swf-science .bpanel__body .swf-prog__title{display:none!important}

/* ⭐ THE LADDER — Sevilla's entrance verbatim in shape (whole-element fade, 10px rise, staggered
   delays), on the brand curve. Calibrated at +.12s per rung so eight rows settle inside ~1.3s. */
#swf-science .bpanel__eyebrow,#swf-science .bpanel__title,
#swf-science .bpanel .pl__row,#swf-science .bpanel .tier,#swf-science .bpanel .tiers__cta{
  opacity:0;transform:translateY(10px);
  transition:opacity .8s var(--entrance),transform .8s var(--entrance)}
#swf-science .bpanel__x{opacity:0;transform:translateY(-16px)}
#swf-science .bpanel.is-in .bpanel__x{opacity:1;transform:none;transition-delay:.1s}
#swf-science .bpanel.is-in .bpanel__eyebrow{opacity:1;transform:none;transition-delay:.05s}
#swf-science .bpanel.is-in .bpanel__title{opacity:1;transform:none;transition-delay:.15s}
#swf-science .bpanel.is-in .pl__row,#swf-science .bpanel.is-in .tier,
#swf-science .bpanel.is-in .tiers__cta{opacity:1;transform:none}
#swf-science .bpanel.is-in :is(.pl__row,.tier):nth-child(1){transition-delay:.28s}
#swf-science .bpanel.is-in :is(.pl__row,.tier):nth-child(2){transition-delay:.40s}
#swf-science .bpanel.is-in :is(.pl__row,.tier):nth-child(3){transition-delay:.52s}
#swf-science .bpanel.is-in :is(.pl__row,.tier):nth-child(4){transition-delay:.64s}
#swf-science .bpanel.is-in :is(.pl__row,.tier):nth-child(5){transition-delay:.76s}
#swf-science .bpanel.is-in :is(.pl__row,.tier):nth-child(n+6){transition-delay:.88s}
#swf-science .bpanel.is-in .tiers__cta{transition-delay:.9s}
@media (prefers-reduced-motion:reduce){
  #swf-science .bpanel,#swf-science .bpanel__eyebrow,#swf-science .bpanel__title,
  #swf-science .bpanel .pl__row,#swf-science .bpanel .tier,#swf-science .bpanel .tiers__cta,
  #swf-science .bpanel__x{
    transition:none;opacity:1;transform:none}
  #swf-science .bpanel:not(.is-in):not([hidden]){opacity:1}
}

/* ⭐ WD-543 — THE FOUR TIER CARDS (Neil's pick over the comparison matrix). Surfaces separate by
   CONTRAST (BRAND.md): cards one step up from the ground, Gold one step again — never a border.
   Names in Playfair italic exactly as the week rows set theirs; money in tracked caps teal. */
/* ⚠️ 14rem, MEASURED: at 1440 the sheet's content box is 992px and 15.5rem tracks fitted only
   THREE, wrapping Platinum onto a lonely second row. 4x224 + 3x14 = 938 fits. iPad portrait still
   lands 2x2, which is the right shape there. */
#swf-science .tiers{display:grid;grid-template-columns:repeat(auto-fit,minmax(14rem,1fr));
  gap:14px;align-items:stretch}   /* stretch, not start: four cards of one height read as a SET;
     ragged bottoms read as four unrelated boxes. Gold's extra line sets the height for all. */
#swf-science .tier{background:var(--surface);border-radius:16px;padding:22px 22px 24px;margin:0}
#swf-science .tier--gold{background:var(--surface-gold)}
#swf-science .tier__eyebrow{margin:0 0 10px;font-size:11px;font-weight:600;letter-spacing:.3em;
  text-transform:uppercase;color:#a78bfa}
#swf-science .tier__name{margin:0;font-family:'Playfair Display Complete',Georgia,serif;
  font-style:italic;font-weight:400;font-size:clamp(24px,2.4vw,30px);line-height:1.1}
#swf-science .tier__price{margin:10px 0 6px;font-size:13px;font-weight:700;letter-spacing:.08em;
  text-transform:uppercase;color:var(--teal)}
#swf-science .tier ul{list-style:none;margin:0;padding:0;display:grid;gap:8px}
#swf-science .tier li{position:relative;padding-left:18px;font-size:14px;line-height:1.55;
  color:var(--muted)}
#swf-science .tier li::before{content:'';position:absolute;left:0;top:.6em;width:6px;height:6px;
  border-radius:50%;background:var(--tier,#51dacf);opacity:.55}
#swf-science .tier--gold li{color:var(--ink-2)}

/* ⭐⭐ WD-552 — THE TIER COLOURS ARE THE SITE'S OWN, READ FROM `etch_styles`. Neil: "the cards —
   Silver, Gold, Platinum — should have some colours on them, shouldn't they? Have a look at the
   following section from the original programme page." That section is the Compare Plans block,
   and its colours are stored as `--cp-check-*` in the Etch styles option on staging. Pulled the
   DARK half of each `light-dark()` pair verbatim rather than picking anything by eye (#13):
       free #51dacf · silver #c8ced8 · gold #fedb37 · platinum #c4a0ff
   Platinum's underline additionally uses that block's own holographic sweep, copied exactly.
   ⚠️ RESTRAINT IS THE POINT (BRAND.md, and the takeover is deliberately minimal): the colour
   appears in the NAME RULE, the price and the bullet dots — never as a card fill or a border,
   which would turn four quiet surfaces into four competing blocks. */
#swf-science .tier[data-tier=free]{--tier:#51dacf}
#swf-science .tier[data-tier=silver]{--tier:#c8ced8}
#swf-science .tier[data-tier=gold]{--tier:#fedb37}
#swf-science .tier[data-tier=platinum]{--tier:#c4a0ff}
#swf-science .tier__price{color:var(--tier,#51dacf)}
/* the rule under the name — their `.cp-tier-underline`, at card scale */
#swf-science .tier__name{position:relative;padding-bottom:10px}
#swf-science .tier__name::after{content:'';position:absolute;left:0;bottom:0;width:34px;height:2px;
  border-radius:2px;background:var(--tier,#51dacf)}
#swf-science .tier[data-tier=platinum] .tier__name::after{width:44px;
  background:linear-gradient(90deg,#ff0080,#ff8c00,#40e0d0,#8a2be2,#ff0080);
  background-size:200% 100%}
#swf-science .tier__tag{margin:0 0 12px;font-size:13px;line-height:1.4;color:var(--muted)}
/* ⚠️ ONE LINE ONLY. "Most families choose this" wrapped to two lines and pushed Gold's name a row
   below the other three — measured on the desktop shot. Their own block solves it with a
   one-word badge (`.cp-popular-badge`, "Popular"), so the label matches that. */
#swf-science .tier--gold .tier__eyebrow{color:var(--tier,#fedb37);white-space:nowrap}
#swf-science .tiers__cta{margin-top:clamp(28px,5vh,44px)}
#swf-science a.tiers__book{display:inline-block;text-decoration:none;text-align:center}

/* ⭐ WD-521 — THE ACTIVE PILL SAYS ITS NAME. Neil: "on the left side on the indicator, it doesn't
   show which technique we're on. It just shows the number… What I want to see is the label when
   we're on that particular thing."
   The rail already had this shape for :hover — an open pill just never inherited it, so scrolling
   through seven principles moved a purple dot down a column of dots. These are the hover rules
   applied to [open], and they must out-specify `#principles.is-rail details[open]{width:46px}`
   above, which is why they carry the #swf-science id as well. */
#swf-science #principles.is-rail details[open]{width:auto;min-width:46px;max-width:none;border-radius:999px}
#swf-science #principles.is-rail details[open] summary{width:auto;padding:0 1.15rem 0 .9rem;gap:.6rem;
  justify-content:flex-start}
#swf-science #principles.is-rail details[open] summary span:last-child{display:inline;font-size:13px;
  font-weight:600;letter-spacing:0;text-transform:none;white-space:nowrap;color:#fff}

/* ⭐ WD-522 — THE DOORS MOVE OFF THE NAV'S BASELINE. Neil: "I don't like the buttons for see the
   price and how it runs at the bottom because it clashes with the floating navigation." The site
   nav floats bottom-centre, so the bottom belongs to it. Top-left is free: `.cap` is display:none
   in this layout and `.sw` owns the top-right. */
/* ⚠️ NOT top-left — MEASURED. The site logo sits at x40 y12 (133x41) and its <li> letters take
   pointer events, so a door placed there was UNCLICKABLE: Playwright reported
   "sophicly-logo__letter … intercepts pointer events" and timed out trying to press it. A button
   you cannot click is worse than one in an awkward place (§4d). Hit-tested afterwards at both
   widths with elementFromPoint on each button's own centre.
   Bottom-RIGHT is the free corner: the floating nav is bottom-CENTRE, the notes tab is mid-right,
   the rail owns the left. */
#swf-science .brain__acts{top:auto;left:auto;right:clamp(16px,3vw,40px);
  bottom:calc(clamp(16px,3vh,40px) + 76px)}
/* ⭐⭐ WD-539 — ON A SMALL SCREEN THE DOORS GO TOP-RIGHT, AS A ROW. Measured at 390x844 with every
   media block alive: the bottom band is fully owned — card sheet y597-748, rail y770-832 — and the
   old bottom-right column (bottom + 96px) sat ON the card's text; on Neil's phone the Take Notes
   tab (mid-right) half-covered SEE THE PRICE on top of that. Top-right under the header is the one
   band nothing else claims: the logo owns top-left, the nav pill ends ~y65, the brain sits centred
   below ~y180. 72px clears the header; a ROW (not a column) keeps both doors out of the notes
   tab's mid-right lane. Same placement for phone landscape via the height arm. */
@media (max-width:767px), (max-height:539px), (max-aspect-ratio:1/1){
  #swf-science .brain__acts{top:clamp(72px,11vh,96px);left:auto;right:12px;bottom:auto;
    flex-direction:row;align-items:center;justify-content:flex-end}
}

/* ⭐⭐ WD-575b — ON A SHORT VIEWPORT THE RIGHT EDGE BELONGS TO THE NOTES TRIGGER. Nothing is lifted
   here; the card and the doors step inboard instead, and the button stays exactly where its own
   plugin puts it.

   ⛔ WHY LIFTING CANNOT WORK, measured on staging rather than reasoned (root CLAUDE.md §19, and
   §ALIGNMENT/GEOMETRY: prove the constraint is satisfiable BEFORE writing code):
       667x375  doors end y126, card starts y148  ->  available  22px, FAB needs  56px
       844x390  doors end y126, card starts y163  ->  available  37px, TAB needs 168px
   The first cut DID lift, and failed at both sizes for exactly this reason. Vertical space is not
   the free variable on a landscape phone; horizontal space is.

   ⚠️ TWO ARMS, BECAUSE IT IS TWO DIFFERENT BUTTONS. `sophicly-notes` swaps its trigger at
   `max-width:768px` (sophicly-notes.css:356), so a landscape phone at 844 wide is ABOVE that line
   and shows the 46x168 vertical TAB, while a 667-wide one is below it and shows the 56x56 FAB.
   Same defect, two geometries — a single gutter would be wrong for one of them.
     <=768 wide : FAB occupies right 24 + 56 = 80px, and sits LOW (y171-227 at H=375), so it clears
                  the doors on its own — only the card needs the gutter. 80 + 24 = 104.
     >=769 wide : TAB occupies right 0 + 46 = 46px and is CENTRED (y111-279), so it crosses the
                  doors as well. Card 46 + 24 = 70; doors 46 + 12 = 58, i.e. the doors keep their
                  own existing offset and simply step inboard of the tab.
   24px is the card's own padding, so the gap BETWEEN two things beats the gap INSIDE one of them —
   the same proximity rule as the rail gap (#B-572) and the portrait FAB above.

   ⭐ A REAL SIDE BENEFIT, stated so it is not mistaken for an accident: at 844px the card was
   running 820px of 13.5px text on one measure, far past a readable line length. The gutter pulls it
   back. There is a case for going further on a wide short viewport; not done here, not asked for.

   ⚠️ THIS BLOCK SITS BELOW WD-539 ON PURPOSE. Both door rules are `#swf-science .brain__acts`, i.e.
   identical specificity, so SOURCE ORDER decides. The first cut of this block sat 240 lines higher,
   lost the tie silently, and measured as a straight FAIL — the card moved and the doors did not. */
/* ⚠️ WD-580 MOVED THE TRIGGER, SO THESE GUTTERS MOVED WITH IT. They are the trigger's inset plus
   its own width plus the 24px separation, and getting them from the same var is what stops the two
   from drifting apart the next time the inset is touched:
     <=768  FAB  12 + 56 + 24 = 92      >=769  TAB  12 + 46 + 24 = 82  (doors: 12 + 46 + 12 = 70) */
@media (max-width:768px) and (max-height:539px){
  #swf-science #brainCard{right:calc(var(--swf-notes-inset) + 56px + 24px)}
}
@media (min-width:769px) and (max-height:539px){
  #swf-science #brainCard{right:calc(var(--swf-notes-inset) + 46px + 24px)}
  #swf-science .brain__acts{right:calc(var(--swf-notes-inset) + 46px + 12px)}
}

/* WD-533 — the plain `.ttbl` timetable that used to live in this panel is gone; the week is a
   ported component now (`science/week-panel.*`). Its styles went with it, hairline row rules
   included. */

/* ⭐ WD-539 — ON A SMALL SCREEN THE OPEN PILL STAYS A DOT. WD-521's name-on-open rules sit above
   and win any tie by source order, so the phone shape is re-asserted HERE, below them — measured:
   the row's active pill leaked its name ("02 R…") through the 44px circle. On a phone the card
   already names the principle (the row comment says exactly this); the name would also not fit
   seven-up across 390px. */
@media (max-width:719px), (max-height:539px), (max-aspect-ratio:1/1){
  #swf-science #principles.is-rail details[open]{width:44px;min-width:44px;max-width:44px;border-radius:50%}
  #swf-science #principles.is-rail details[open] summary{width:44px;padding:0;gap:0;justify-content:center}
  #swf-science #principles.is-rail details[open] summary span:last-child{display:none}
}

/* ⚠️ TABLET PORTRAIT GETS A FIXED 2x2 — auto-fit fitted THREE at 820 and left Platinum alone on a
   second row, the same lonely-row shape the 14rem track fixed on desktop. Two even rows read as a
   set; three-plus-one reads as a mistake. Phone stacks to one, as auto-fit already does. */
@media (max-width:1024px) and (max-aspect-ratio:1/1){
  #swf-science .tiers{grid-template-columns:repeat(2,minmax(0,1fr))}
}
@media (max-width:560px){
  #swf-science .tiers{grid-template-columns:minmax(0,1fr)}
}

/* the CTA's promise, in the demo page's own words — a claim about price belongs beside the button */
#swf-science .tiers__note{margin:14px 0 0;max-width:46ch;font-size:13.5px;line-height:1.55;
  color:var(--muted)}

/* ⭐⭐ WD-548 — THREE BUTTONS COMPETING BECOMES ONE. Neil: "it's got Book Session, See the Price,
   How it Runs. Do you think that's too many buttons? Do we need to make that a bit more
   minimalistic?" Measured on his screenshot: the nav's gradient BOOK SESSION, a teal-filled SEE
   THE PRICE and a filled HOW IT RUNS all stack in the top-right within 200px of each other —
   three filled shapes, no hierarchy, and the two page doors shout as loudly as the site's own
   conversion CTA.
   ⭐ WWMSD, from the source rather than taste: Sevilla does NOT delete furniture, he DENIES IT THE
   HEADLINE'S TREATMENT (memory `reference_wwmsd…`, the Risen hero: five information items, each
   demoted by size and a whole-element fade, none of them competing with the headline). So both
   doors stay — they are the only route to price — and they simply stop being buttons: no fill, no
   plate, muted type that inks to white on hover, with the teal reserved for the ONE thing on the
   stage that is a real call to action. Nothing is hidden and nothing moves.
   ⚠️ They keep their 46px min-height and their padding, so the tap targets are unchanged. */
#swf-science .bact[data-panel]{background:none;color:var(--muted);padding:12px 4px;min-height:44px;
  font-size:12px;letter-spacing:.14em}
#swf-science .bact[data-panel]:hover{background:none;color:var(--ink)}
#swf-science .bact[data-panel]::after{display:none}
/* the row's own spacing carries the separation now that there are no plates to butt together */
#swf-science .brain__acts{gap:22px}

/* ⭐⭐ WD-559 — THE ENTRANCE, ANSWERED FROM SEVILLA'S SOURCE (WWMSD, not taste). Neil: "we need to
   work on an entrance animation for the brain and the rail, based on the Risen Church parameters
   in our brand guideline."
   His hero does NOT give its furniture bespoke motion. Every information item is a WHOLE-ELEMENT
   fade supplied by ONE class — `opacity:0; translateY(10px); transition:all 1500ms
   cubic-bezier(.23,1,.32,1)` — separated by a strict +0.2s DELAY LADDER (his: .5 / .7 / .9).
   Both numbers are his, verbatim (memory `reference_wwmsd_what_would_michael_sevilla_do`); only
   the ORDER is ours, because the elements are ours: the model arrives, then the rail that walks it,
   then the words about it, then the two doors out.
   ⚠️ THE CANVAS FADES ONLY — NO TRANSLATE. Root SOP landmine: a parent transform animation
   rasterises what is under it and a WebGL surface pays that as a full re-composite; the brain also
   has nowhere to travel from, being the whole stage. Opacity is the honest half of his effect here.
   ⚠️ The rest state is armed by boot-reveal.js BEFORE the cover lifts, so nothing is ever seen
   sitting at opacity 0 — and if that script never runs, no element ever gets `.swf-enter` and the
   page is simply visible, which is the §4d half that matters. */
#swf-science .brain__stage.swf-enter .brain__canvas,
#swf-science .brain__stage.swf-enter #principles,
#swf-science .brain__stage.swf-enter .brain__card,
#swf-science .brain__stage.swf-enter .brain__acts,
#swf-science .brain__stage.swf-enter .brain__labels{
  opacity:0;transition:opacity 1500ms cubic-bezier(.23,1,.32,1),
                       transform 1500ms cubic-bezier(.23,1,.32,1)}
#swf-science .brain__stage.swf-enter #principles,
#swf-science .brain__stage.swf-enter .brain__acts{transform:translateY(10px)}

/* ⭐⭐ WD-576 — ARMING IS A STATE, NOT AN ANIMATION. `.swf-arming` is held for exactly one frame
   while boot-reveal.js applies `.swf-enter`, so the rest state is COMMITTED rather than travelled
   to. Without it, adding `.swf-enter` to already-painted elements fades them out over 1500ms —
   measured on staging: the rail held opacity 1.00 to 1343ms, then ramped to 0 by ~2843ms, which is
   the "it appears, then disappears" half of what Neil reported. */
#swf-science .brain__stage.swf-arming,
#swf-science .brain__stage.swf-arming *{transition:none !important}

/* ⭐⭐ THE CANVAS IS OUT OF THE LADDER — IT ARRIVES ON ITS OWN CLOCK. The reveal no longer waits for
   WebGL (see boot-reveal.js), so the brain cannot be step one of a ladder that has already run. It
   fades in wherever it lands, on the house curve, into a page that is already correct and readable.
   ⚠️ Keep it opacity-only: a transform on a WebGL surface costs a full re-composite, and the brain
   has nowhere to travel from, being the whole stage. */
#swf-science .brain__stage.swf-enter.brain-ready .brain__canvas{opacity:1;transition-delay:0s}
/* the labels are projected from the model's own anchors, so they belong to the brain's arrival,
   not to the page's — revealing them before there is a brain under them points at nothing */
#swf-science .brain__stage.swf-enter.brain-ready .brain__labels{opacity:1;transition-delay:.15s}
/* ⚠️ THE LADDER IS RE-BASED, NOT RE-TIMED. His +0.2s spacing is untouched (.5/.7/.9 in the Risen
   hero); what changed is that it no longer waits behind a 6-second WebGL boot, so the rail now
   leads at .1s instead of trailing the canvas at .5s. Same intervals, same curve, same order —
   model-furniture, then the words, then the doors out. */
#swf-science .brain__stage.swf-enter.is-in #principles{opacity:1;transform:none;transition-delay:.1s}
#swf-science .brain__stage.swf-enter.is-in .brain__card{opacity:1;transition-delay:.3s}
#swf-science .brain__stage.swf-enter.is-in .brain__acts{opacity:1;transform:none;transition-delay:.5s}
/* the card owns its own .is-on opacity once the walk is running — the entrance must not out-rank
   it, or a card hidden between regions would be forced visible for ever after */
#swf-science .brain__stage.swf-enter.is-in .brain__card:not(.is-on){opacity:0}
/* ⭐ WD-589 — the brain-ready handoff dip (see boot-reveal.js). The shorthand REPLACES the
   1500ms entrance transition list on purpose: opacity dips in 300ms with no ladder delay, and
   left/top/transform get NO transition at all, so the provisional spot's release is a snap made
   while the card is invisible — never a 1.5s drift across the stage. Must sit after (and
   out-rank) the `.is-in` delay rule and the `:not(.is-on)` guard above. */
#swf-science .brain__stage.swf-enter.is-in .brain__card.is-handoff{opacity:0;
  transition:opacity 300ms cubic-bezier(.23,1,.32,1) 0s}
@media (prefers-reduced-motion:reduce){
  /* ⚠️ the canvas and labels are keyed on `.brain-ready` now, so they MUST be asserted here too —
     a reader on reduced motion would otherwise sit at opacity 0 until the brain happened to load,
     which is the §4d failure of a guard that says what it refuses and not what it shows. */
  #swf-science .brain__stage.swf-enter .brain__canvas,
  #swf-science .brain__stage.swf-enter #principles,
  #swf-science .brain__stage.swf-enter .brain__card,
  #swf-science .brain__stage.swf-enter .brain__acts,
  #swf-science .brain__stage.swf-enter .brain__labels{
    transition:none;opacity:1;transform:none}
}

/* the platform lede sits above the cards — one paragraph, measured column, never a wall */
#swf-science .tiers__lede{margin:0 0 clamp(22px,3.4vh,34px);max-width:62ch;
  font-size:15px;line-height:1.6;color:var(--ink-2)}
#swf-science .tiers__lede + .tiers{margin-top:0}

/* ⭐ WD-570 — the halo button's SECOND part. Same numbers as `.swf-btn__roll` in funnel.css:
   a 1em window, line-height 1 so the glyphs cannot clip (that clip was a real bug, WD-493f.3),
   and the pair sliding a full window on the brand curve. */
#swf-science .bact__roll{position:relative;display:block;overflow:hidden;height:1em}
#swf-science .bact__roll span{display:block;line-height:1;
  transition:transform .5s cubic-bezier(.22,1,.36,1)}
#swf-science .bact__roll span + span{position:absolute;inset:0;transform:translateY(100%)}
#swf-science .bact:hover .bact__roll span,
#swf-science .bact:focus-visible .bact__roll span{transform:translateY(-100%)}
#swf-science .bact:hover .bact__roll span + span,
#swf-science .bact:focus-visible .bact__roll span + span{transform:translateY(0)}

/* ⭐ WD-569 — BRONZE. Neil approved the shape ("bronze, silver, gold and platinum as the four main
   plans, and then an extra line underneath for free"), so Free leaves the card row.
   ⚠️ The site had no bronze token — the Compare Plans block only defines free/silver/gold/platinum
   — so this is a NEW value, stated rather than smuggled: #d99a6c is classic bronze (#cd7f32)
   lifted for contrast on the near-black stage, the same move the block already makes for silver
   (#c8ced8) and platinum (#c4a0ff). */
#swf-science .tier[data-tier=bronze]{--tier:#d99a6c}
/* the free plan is a LINE now, not a card — it is the entry, not something a parent chooses between */
#swf-science .tiers__free{margin:clamp(20px,3vh,28px) 0 0;max-width:62ch;font-size:14px;
  line-height:1.6;color:var(--muted)}
#swf-science .tiers__free strong{color:var(--ink-2);font-weight:600}

/* ⭐ WD-573 — A SHORT LAPTOP MUST STILL SEE THE BUTTON. Neil: "I want a bit of space at the top and
   a bit at the bottom — you can see we're running out of space at the bottom."
   Measured after the trim: 1440x900 fits with 31px under the CTA and 1728x1080 with 181px, but at
   1440x820 the CTA sat 36px BELOW the fold. Height — not width — is what decides this, so the
   guard keys on height: on a short viewport the takeover gives back its own generous padding and
   the cards tighten, because a call to action under the fold is the one thing that must not
   happen (§4d: the way forward has to be visible). */
@media (max-height:880px) and (min-width:1024px){
  #swf-science .bpanel__sheet{padding-top:28px;padding-bottom:28px}
  #swf-science .bpanel__body{margin-top:16px}
  #swf-science .bpanel__title{font-size:clamp(28px,2.6vw,38px)}
  #swf-science .tier{padding:18px 20px 20px}
  #swf-science .tiers__free{margin-top:16px}
  #swf-science .tiers__cta{margin-top:18px}
}

/* ⭐⭐ WD-595 — ON A PORTRAIT PHONE THE DOORS LEAVE THE STAGE'S TOP BAND AND GET A ROW OF THEIR OWN.
   Neil, with a screenshot: "see the price, and how it runs, they're actually overlapping the brain
   anyway. So what can we do then?" MEASURED on staging at 390x664: the WD-539 top-right band put the
   doors at y73-117, and the model fills the full width — its crown crosses that band, so SEE THE
   PRICE was drawn over the cortex and every tap on it reached the stage first (the hit-test half is
   guarded in science.mjs). The bottom stack was card y417-568 · rail y590-652, so the one place
   nothing owns is BETWEEN the sheet and the rail, and that is where the row goes: fixed, full
   width, centred, the same text-only doors WD-548 made them. The sheet lifts by exactly the row's
   height plus the two gaps, derived from the same rail numbers the sheet already reads (WD-578:
   never re-type a number the rail owns).
   Landscape phones keep WD-539/575b's top-right — measured there, and the constraint is different.
   ⚠️ Source order: below every other `.brain__acts` and `#brainCard` rule, on purpose (WD-575b). */
@media (max-aspect-ratio:1/1) and (min-height:540px){
  #swf-science .brain__acts{position:fixed;top:auto;left:12px;right:12px;
    bottom:calc(var(--swf-rail-gap, 12px) + 44px + 16px + env(safe-area-inset-bottom) + 8px);
    flex-direction:row;align-items:center;justify-content:center;gap:28px;z-index:23}
  #swf-science #brainCard{bottom:calc(var(--swf-rail-gap, 12px) + 44px + 16px + env(safe-area-inset-bottom) + 8px + 44px + 8px)}
}

/* ══ WD-651b · LIGHT THEME (Neil, 2026-09-02: "Yes, do it"). THE MECHANISM IS funnel.css's OWN (lines 211–260):
   light is the site DEFAULT (a first-time visitor has no data-theme at all), so the light values sit under the
   three light selectors and out-rank the bare root; then dark is RE-ASSERTED explicitly, because a stale
   `class="light"` can sit on <html> while `data-theme="dark"` is set (funnel.css 3118). The VALUES are
   funnel.css's measured Risen light palette (WD-386: #ffffff ground · #f7f7f8 base · #efefef raised · #555555
   ink 7.46:1 · #636363 soft 6.01:1 · #4D76FD accent · #5333ed purple), written out because this section is not
   inside `.swf` and cannot inherit the `--swf-*` tokens. Same numbers, one source, cited. ══ */
html[data-theme='light'] #swf-science, html.light #swf-science, html:not([data-theme='dark']):not(.dark):not(.dark-mode) #swf-science { --bg:#ffffff; --surface:#f7f7f8; --raised:#efefef; --border:#e3e4e8; --ink:#555555; --muted:#636363; --ink-2:#636363; --purple-lt:#5333ed; --surface-glass:rgba(247,247,248,.94); --raised-glass:rgba(239,239,239,.9); --label-glass:rgba(239,239,239,.96); --surface-gold:#f6f1e1; --scrim:#ffffff; --teal:#4D76FD; --mint:#2c003e; --btn-ink:#ffffff; }
/* WD-667 (Neil: "the rail might just need a bit more contrast" in light) — the pills and the rail controls sit one
   surface step up on the white stage; a raised surface, never a hairline (BRAND). */
html[data-theme='light'] #swf-science .principles details, html.light #swf-science .principles details, html:not([data-theme='dark']):not(.dark):not(.dark-mode) #swf-science .principles details,
html[data-theme='light'] #swf-science .pctl, html.light #swf-science .pctl, html:not([data-theme='dark']):not(.dark):not(.dark-mode) #swf-science .pctl { background: #e9e9ee; }
html[data-theme='dark'] #swf-science .principles details, html.dark #swf-science .principles details, html.dark-mode #swf-science .principles details,
html[data-theme='dark'] #swf-science .pctl, html.dark #swf-science .pctl, html.dark-mode #swf-science .pctl { background: var(--surface); }
/* the tier accents are DARK-TONE metals (silver #c8ced8 1.6:1, platinum #c4a0ff 1.9:1, gold #fedb37 on cream 1.5:1 — all measured
   on the first light screenshot). Light needs the same hues at a tone that reads on white: free → the site's light accent;
   platinum → brand purple (BRAND: #c4a0ff/#a78bfa are the dark-theme tones of #5333ed); silver → the light soft ink (a grey
   metal, 6.0:1); gold #7a6000 and bronze #96562a → the same hue darkened until they pass 4.5:1 (measured 4.36/4.37 at the first cut) — TWO NEW VALUES, stated rather than smuggled. */
html[data-theme='light'] #swf-science .tier[data-tier=free], html.light #swf-science .tier[data-tier=free], html:not([data-theme='dark']):not(.dark):not(.dark-mode) #swf-science .tier[data-tier=free]{--tier:#4D76FD}
html[data-theme='light'] #swf-science .tier[data-tier=silver], html.light #swf-science .tier[data-tier=silver], html:not([data-theme='dark']):not(.dark):not(.dark-mode) #swf-science .tier[data-tier=silver]{--tier:#636363}
html[data-theme='light'] #swf-science .tier[data-tier=gold], html.light #swf-science .tier[data-tier=gold], html:not([data-theme='dark']):not(.dark):not(.dark-mode) #swf-science .tier[data-tier=gold]{--tier:#7a6000}
html[data-theme='light'] #swf-science .tier[data-tier=platinum], html.light #swf-science .tier[data-tier=platinum], html:not([data-theme='dark']):not(.dark):not(.dark-mode) #swf-science .tier[data-tier=platinum]{--tier:#5333ed}
html[data-theme='light'] #swf-science .tier[data-tier=bronze], html.light #swf-science .tier[data-tier=bronze], html:not([data-theme='dark']):not(.dark):not(.dark-mode) #swf-science .tier[data-tier=bronze]{--tier:#96562a}
html[data-theme='dark'] #swf-science, html.dark #swf-science, html.dark-mode #swf-science { --bg:#1c1d1f; --surface:#242628; --raised:#333437; --border:#2e3033; --ink:#f4f5f6; --muted:#7f868c; --ink-2:#c9cdd1; --purple-lt:#a78bfa; --surface-glass:rgba(36,38,40,.94); --raised-glass:rgba(48,50,55,.9); --label-glass:rgba(51,52,55,.96); --surface-gold:#2c2e35; --scrim:#161719; --teal:#51dacf; --mint:#7DF9E9; --btn-ink:#1C1D1F; }
/* WD-651b RETEST — the dark-island rule that stood here is WITHDRAWN. Neil: "the background behind the brain
   should also… I think EVERY element should support light theme." The stage ground is now painted by the 3D
   scene from the theme (science.mjs `applyStageTheme`), and the chips/cards on it take the section tokens
   like everything else. */
html[data-theme='dark'] #swf-science .tier[data-tier=free], html.dark #swf-science .tier[data-tier=free], html.dark-mode #swf-science .tier[data-tier=free]{--tier:#51dacf}
html[data-theme='dark'] #swf-science .tier[data-tier=silver], html.dark #swf-science .tier[data-tier=silver], html.dark-mode #swf-science .tier[data-tier=silver]{--tier:#c8ced8}
html[data-theme='dark'] #swf-science .tier[data-tier=gold], html.dark #swf-science .tier[data-tier=gold], html.dark-mode #swf-science .tier[data-tier=gold]{--tier:#fedb37}
html[data-theme='dark'] #swf-science .tier[data-tier=platinum], html.dark #swf-science .tier[data-tier=platinum], html.dark-mode #swf-science .tier[data-tier=platinum]{--tier:#c4a0ff}
html[data-theme='dark'] #swf-science .tier[data-tier=bronze], html.dark #swf-science .tier[data-tier=bronze], html.dark-mode #swf-science .tier[data-tier=bronze]{--tier:#d99a6c}


/* ══ WD-678 (consent 1.4.0) · THE COOKIE CARD IS A BOTTOM SHEET ON PHONES AGAIN — LIFT IT ABOVE THIS PAGE'S RAILS ══
   This is the ONE page with bottom-pinned controls on a phone (WD-650: the card used to cover "See the price" / "How it
   runs"). The consent card reads `--swc-lift` from <html>; the numbers are the rails' own (WD-575b/578 above):
   the pill rail = rail-gap + 44 + 16, the acts row above it = + 8 + 44, and the card wants 8px of air. The card already
   adds 12px + the safe-area inset itself, so 12 is taken back here. Same media as the rails, so the lift exists exactly
   when the rails do. */
@media (max-width:480px){
  html.swf-page-science{--swc-lift:calc(var(--swf-rail-gap, 12px) + 44px + 16px + 8px - 12px)}
}
@media (max-width:480px) and (max-aspect-ratio:1/1) and (min-height:540px){
  html.swf-page-science{--swc-lift:calc(var(--swf-rail-gap, 12px) + 44px + 16px + 8px + 44px + 8px - 12px)}
}

/* ══ WD-681 (Neil, 2026-09-02: "they enter nicely, but when you close them it's quite abrupt. I want them to animate out
   smoothly like the mobile menu.") — THE MOBILE MENU'S CLOSE, MEASURED (sophicly-mobile-menu.php, CLOSE_TUNE):
   links leave UPWARD 8px, laddered i/15 s, on their 800ms cubic-bezier(.23,1,.32,1) · the panel fades over .30s ease-out
   · teardown at .5s. Same numbers here. Before: `hidden` landed at 300ms and cut the .8s entrance transitions mid-fade —
   that was the pop. science-panels.js sets the per-child delay (i/15) and adds `.is-out`; it clears both on teardown. */
#swf-science .bpanel.is-out{opacity:0;transition:opacity .3s ease-out}
#swf-science .bpanel.is-out .bpanel__x,#swf-science .bpanel.is-out .bpanel__eyebrow,#swf-science .bpanel.is-out .bpanel__title,
#swf-science .bpanel.is-out .pl__row,#swf-science .bpanel.is-out .tier,#swf-science .bpanel.is-out .tiers__cta{
  opacity:0;transform:translateY(-8px);
  transition:opacity .8s cubic-bezier(.23,1,.32,1),transform .8s cubic-bezier(.23,1,.32,1)}
