/* THE WORLD — the illustration at the base of the home page.

   ⚠️ IT CHANGES NOTHING ELSE. The hero's copy, buttons, icons, eyes and boot
   sequence are untouched; this is a drawing added beneath them. It sits OUTSIDE
   .phosphor at z-index 0 — behind the content, above the field — and is
   aria-hidden, unlinked and unlabelled. Home page only.

   The brief was the Outer Wilds EFFECT (dark space with a small lit world in it)
   and explicitly not its art style. Drawn in CSS and the site's own 8x8 sprites
   rather than rendered: the README bars AI imagery as hero art, canvas is
   rejected, and this way it costs nothing to load on the page that has to be
   fastest. */

.world {
  /* ⚠️ THE WORLD IS GIVEN A BOX, AND THE BOX IS WHATEVER IS LEFT BELOW THE HERO.
     Every previous attempt tried to pick images whose terrain happened to sit
     low enough, or to crop them until it did. Both are the same mistake: they
     make "does this overlap the copy" a property of the PICTURE, so every new
     image re-opens the question and a bigger viewport breaks it again. Juan's
     screenshot was taken on a much larger display than the 1440x900 I had been
     measuring at, and at that width a 2.5:1 scene is 800px tall and swallows the
     page — the images had not changed, the viewport had.

     So the constraint moves into the layout, where it holds for any image at any
     size: the hero is centred and its block is about 195px tall below the middle
     line, so the world starts below that and runs to the bottom of the page.
     Nothing it contains can reach the copy, because it is not allowed to exist
     up there. */
  position: absolute; left: 0; right: 0; bottom: 0;
  top: calc(50vh + 205px);
  overflow: hidden;
  z-index: 0; pointer-events: none;
}

/* a distant body, the way the reference frames always have one */
.world__moon {
  position: absolute; right: 9%; top: 4%;
  width: clamp(80px, 12vw, 180px); aspect-ratio: 1; border-radius: 50%;
  background: radial-gradient(circle at 36% 32%,
    rgba(240, 168, 60, .17), rgba(240, 168, 60, .04) 55%, transparent 72%);
  box-shadow: 0 0 70px rgba(240, 168, 60, .08);
}
.world__moon::after {
  content: ""; position: absolute; inset: 0; border-radius: 50%;
  border: 1px solid rgba(240, 168, 60, .18);
}

/* the planet: one enormous circle with only its crest on screen, which is what
   makes it read as a world rather than as a hill */
.world__body {
  /* ⚠️ the horizon has to clear the CONTENT, not just the viewport. At the
     first placement the crest cut straight through the icon row — measured 111px
     above it — so the drawing was running behind the thing it is meant to sit
     under. It is pushed below the copy now. */
  position: absolute; left: 50%; top: 40%;
  width: 230vw; height: 230vw; translate: -50% 0; border-radius: 50%;
  /* ⚠️ the ground has to be LIT near the horizon or a silhouette has nothing to
     be a silhouette against — the pines were invisible until this band existed,
     and drawing them darker did nothing because they were already dark on dark.
     It falls away fast, so the foreground still reads as night. */
  background: linear-gradient(to bottom,
    rgba(240, 168, 60, .18) 0%, rgba(190, 120, 50, .10) 1.4%,
    rgba(90, 60, 30, .045) 3.4%, #0a0806 7%, #050706 16%);
  box-shadow: 0 -1px 0 rgba(240, 168, 60, .26), 0 -30px 80px rgba(240, 168, 60, .045);
}
/* the terminator — the grazing light that says sphere */
.world__rim {
  position: absolute; left: 50%; top: 40%;
  width: 230vw; height: 230vw; translate: -50% 0; border-radius: 50%;
  border-top: 1px solid rgba(255, 196, 104, .45);
  filter: blur(.5px);
}

/* --------------------------------------------------------- standing on it */

/* ⚠️ A FLAT top:% CANNOT TRACK A CIRCULAR HORIZON, and this was wrong in a way
   nothing reports. Every object was hand-placed at a percentage of the
   container, so its distance from the crest was a different number at every
   aspect ratio — and, measured against the circle .world__body is actually
   drawn with, four of them were simply hanging in the sky: the pines at 6%, 10%
   and 89% floated 32-54px above the ground they were standing on, and the two
   far sprites sat exactly ON the line rather than in front of it. The gaps
   across the row ran from -54px to +113px. It went unseen for the same reason
   the objects did — a drawing that is wrong does not throw.

   Everything is placed on the sphere now, the way a thing standing on a world
   actually is: an ANGLE around it (--a), and a DROP saying how far in front of
   the crest it stands (--d). Both are computed from the same circle the horizon
   is, so both hold at every viewport — the horizon solved exactly, the way the
   rig's crossbar was once it became a grid instead of a tuned percentage. */
.world .sit {
  position: absolute;
  left: 50%; top: calc(40% + 115vw);   /* dead centre of .world__body */
  width: 0; height: 0;
  rotate: var(--a, 0deg);
}
.world .sit > * {
  position: absolute; left: 0;
  /* ⚠️ --d MUST CARRY A UNIT. Two objects were authored `--d:0` and vanished:
     calc(115vw - 0) is invalid CSS — a plain number is not a length — so the
     whole `bottom` declaration was dropped, the element fell back to auto and
     landed on the circle's centre, 1400px below the fold. No error, no warning,
     and in source it reads as the most obviously correct value on the line. */
  bottom: calc(115vw - var(--d, 0px));   /* out to the rim, less the drop */
  translate: -50% 0;
}

/* ⚠️ BUT A PIXEL SPRITE DOES NOT TILT. A pine is a drawn silhouette, and a tree
   on a small world genuinely does stand radially — it keeps the rotation, and
   that lean is most of what makes the ground read as a sphere. A sprite is tier
   one: square, on the grid. Rotating it puts it off the grid and renders it
   soft, which is the rule the blink already follows — a pixel switches, it
   never deforms. So sprites ride the sphere's POSITION and cancel its
   ROTATION. */
.world .sit > .obj { rotate: calc(-1 * var(--a, 0deg)); }

/* ---------------------------------------------------------------- objects */

/* ⚠️ THE CELLS ARE DIRECT CHILDREN OF THE HOST. paintPlates() in os.js appends
   its 64 <i> elements straight onto the [data-sprite] element — that is the
   contract .plate and .page-mark are both written against. This file shipped
   styling a .spr wrapper that nothing ever creates, so the grid columns landed
   on an element that had none: grid-template-columns computed to 0px and every
   cell measured 0×0. The horizon, the pines and the fire all drew, so the scene
   looked finished while the six objects it is ABOUT were absent, with no error
   anywhere — the same silent-drift family as the missing chroma filter.
   width: max-content for the reason .page-mark documents: a block-level grid
   fills its container instead of hugging its columns. */
.world .obj {
  display: grid; width: max-content;
  grid-template-columns: repeat(8, var(--pp));
  grid-auto-rows: var(--pp); gap: calc(var(--pp) * .18); --pp: 3px;
}
.world .obj i { background: transparent; border-radius: .5px; }
.world .obj i.on {
  background: var(--amber);
  box-shadow: 0 0 6px rgba(240, 168, 60, .5);
}
/* depth: further away is smaller and dimmer, which is what turns a row of
   objects into a landscape */
.world .obj--far  { opacity: .40; --pp: 2px; }
.world .obj--mid  { opacity: .62; --pp: 2.6px; }
.world .obj--near { opacity: .9;  --pp: 3.6px; }

/* pines are drawn, not sprited — a tree is a silhouette, not a pixel glyph */
.world .pine {
  display: block;
  width: var(--w, 14px); height: var(--h, 34px);
  background: #040403; opacity: var(--o, .9);
  clip-path: polygon(50% 0, 74% 34%, 60% 34%, 84% 66%, 66% 66%,
                     92% 100%, 8% 100%, 34% 66%, 16% 66%, 40% 34%, 26% 34%);
}
.world .pine::after {
  content: ""; position: absolute; left: 50%; bottom: -3px;
  width: 1.5px; height: 5px; background: #040403; translate: -50% 0;
}

/* the fire: the one warm thing on the surface */
.world .fire {
  display: block;
  width: 8px; height: 8px; border-radius: 50%;
  background: radial-gradient(circle, #FFD9A0, var(--warm) 45%, transparent 72%);
  box-shadow: 0 0 26px 8px rgba(255, 107, 53, .26);
  animation: world-flicker 3.2s ease-in-out infinite alternate;
}
@keyframes world-flicker {
  from { opacity: .72; transform: scale(.94); }
  to   { opacity: 1;   transform: scale(1.08); }
}

/* the whole point of this is atmosphere, so it is the first thing to go quiet */
@media (prefers-reduced-motion: reduce) {
  .world .fire { animation: none; }
}
/* on a phone the hero needs its full height for the copy; the horizon stays,
   the scene on it does not */
@media (max-width: 700px) {
  .world { height: 34vh; }
  .world .obj, .world .pine, .world .fire { display: none; }
  .world__moon { width: 74px; top: 6%; right: 7%; }
}

/* ------------------------------------------------------- the rendered world

   It sits OVER the CSS world, which stays as the base: no-JS, slow connections
   and the moment before this decodes all still get a drawn horizon, exactly the
   way the turntable keeps its still <img> underneath.

   ⚠️ `mix-blend-mode: lighten` is the whole reason there is no visible cut. A
   rendered strip dropped on the page is a RECTANGLE — its black sits around
   #040404, the page's is #050706, and the page lays a dot field over that, so
   the two blacks can never match and the image's boundary reads as a faint box.
   `lighten` draws whichever of the two is brighter, so every pixel of the render
   darker than the page is simply never drawn. There is no seam because there is
   no edge. The gradient mask on top only softens the horizon's own glow. */
.world__art {
  /* ⚠️ object-fit: CONTAIN, not cover and not a width. Contain is the only one
     that satisfies both halves of what Juan asked for at once — the whole scene
     is visible because nothing is cropped, and it cannot overlap the copy
     because it cannot leave its box. "As much scenery as possible, never
     touching the hero" is precisely the definition of contain.
     Anchored to the bottom so the world sits on the floor of the page rather
     than floating in the middle of its box. */
  position: absolute; inset: 0;
  width: 100%; height: 100%; max-width: none;
  object-fit: contain;
  object-position: 50% 100%;
  translate: none;
}
/* the CSS scene underneath would show through the render's dark ground, so the
   drawn objects stand down when the render is present */
.world:has(.world__art) .sit { display: none; }



/* ---------------------------------------------------------- room for it ---

   The hero centres in the whole viewport, which leaves about 255px between the
   icon row and the bottom of the page — not enough for the whole scene at any
   useful size. Lifting the copy is the fourth option, and the only one that
   costs neither the picture nor the width: the band grows to roughly 330px and
   the world can be about 70% of the page instead of 55%.

   ⚠️ SCOPED WITH :has(.world), NOT JUST WRITTEN HERE. world.css is only linked
   from index.html, but the router CARRIES stylesheets across and never removes
   them — so a bare `.stage` rule in this file would start applying to /work and
   /code the moment somebody navigated there from the front door. That is the
   same both-directions trap the world itself was in. :has() ties the rule to
   the one page that actually has a world in it. */
/* ⚠️ REVERTED. Lifting the copy was me solving the wrong constraint. Juan's
   rule is that the hero stays centred in the viewport, full stop; the image is
   allowed to pass BEHIND it as long as only its sky does, because the sky is
   transparent and the stars show through it. Making room by moving his
   composition was never the trade — the trade is which images have a low enough
   middle to sit under centred copy. */
/* .phosphor:has(.world) .stage — deliberately not overridden */

/* ============================================================ HUD LAYOUTS ===
   Juan's idea: if the world is a contained frame rather than full bleed, the
   black either side of it stops being empty and becomes somewhere to put
   things — the way a helmet visor in a science-fiction film projects its
   readouts around the edge of what you are actually looking at.

   These are STUDIES, switched from the localhost-only bar. The site ships one.
   Everything here is scoped under a class on .world's parent so nothing applies
   unless a layout is actually chosen. */

/* ⚠️ 16:9 FOR CONTAINED, AND THE RATIO WAS MEASURED RATHER THAN CHOSEN. Juan
   asked which fits best. 4:3 throws away 24% of the width on five of these six
   pictures and 48% on the sixth; 16:9 throws away NOTHING on five of them,
   because they render natively at 1.75:1. The frame should cost the picture as
   little as possible, and one of these costs nearly nothing.

   The assets are also trimmed to their own content now. world-ve was 70% empty
   padding — 712px of transparent sky above a thin band of scenery — and
   object-fit: contain was dutifully fitting all that emptiness, so the picture
   drew at a third of the size it could. Every asset is its own content and
   nothing else, which is what makes the fit correct PER IMAGE rather than on
   average. */

/* ⚠️ THE ART'S GEOMETRY IS WRITTEN BY world-fit.js, NOT HERE, and that is not
   laziness. Its position depends on the hero's measured rect AND on where the
   picture itself stops being sky — two runtime facts. A `calc()` cannot see
   either, and the last attempt to express this rule in CSS was a guess at the
   hero's half-height that broke silently the moment the type wrapped.

   What stays here is the part that is genuinely static: no cropping, ever.
   `object-fit: contain` used to size the art, and it sized it from the LEFTOVER
   STRIP'S HEIGHT — a 16:9 picture in a 1440x280 strip fits by height and draws
   498px wide, so the viewport's width never entered the calculation and "full
   bleed" rendered as a ~640px stamp in the middle of the page. Width leads now,
   height follows from the picture's own ratio, and the two layouts differ in
   NOTHING but that width. */
.w-full .world__art,
.w-fit  .world__art {
  position: absolute;
  right: auto; bottom: auto;
  max-width: none; max-height: none;
  object-fit: fill;        /* the box is already the picture's own ratio */
  translate: none;
  /* the sky fades out at the top rather than ending at a border — see the
     note in world-fit.js, which sets the distance from the measured lift.
     0px is a no-op, so an unstyled or unmeasured art is unchanged. */
  /* ⚠️ THE FADE MASK IS GONE, AND IT WAS THE SEAM ALL ALONG.
     It was added when the illustration's sky was OPAQUE: the art was a
     rectangle laid on the page, so its top edge needed softening. Keying the
     sky out made that job obsolete — the picture now ends wherever its own
     alpha says, which is along the actual silhouette of the trees and rock.

     But the mask stayed, and a mask that starts at the art's top and runs
     nearly half its height does not fade a SKY any more. It fades the top of
     everything in that band: the conifers, the rock, the glow. Juan kept
     reporting a transition that was not smooth, and every attempt to smooth it
     further — longer, eased, dithered — made the veil larger. The fix was to
     stop, because the asset had already solved it.

     Rendered side by side at 1440x900: with the mask the treetops carry a grey
     haze and dissolve mid-trunk; without it they are crisp and simply end. The
     transition is now the picture's own edge, which is the only kind that can
     look like nothing at all.

     ⚠️ IF AN UNKEYED ASSET IS EVER USED AGAIN, this has to come back — an opaque
     sky needs the softening. Everything shipped is keyed; check before assuming. */}
.w-full .stage, .w-fit .stage { padding-block: 10vh; }

/* how much of the page width the contained layout is allowed — the ONLY thing
   that separates it from full bleed. It stops short of the edges so the rail of
   icons has real margin to stand in; nothing is cropped to achieve that. Read
   by world-fit.js and driven by the picker's width buttons. */
.w-fit { --fit-w: 72; }

/* --- the icon row in its default place ------------------------------------ */
.w-row #icons { /* unchanged — the shipped layout */ }

/* --- FLANK: three icons down each side, framing the world ------------------
   The closest thing to a visor: the readouts sit at the edge of vision and the
   thing you are looking at stays uncovered in the middle. */
.w-flank #icons {
  position: fixed; left: 0; right: 0; bottom: 0; top: 0;
  display: block; pointer-events: none;
  z-index: 3;
}
.w-flank #icons .icon {
  position: absolute; pointer-events: auto;
  width: 74px;
}
.w-flank #icons .icon:nth-child(-n+3) { left: clamp(18px, 4.2vw, 84px); }
.w-flank #icons .icon:nth-child(n+4)  { right: clamp(18px, 4.2vw, 84px); }
.w-flank #icons .icon:nth-child(1),
.w-flank #icons .icon:nth-child(4) { top: 44vh; }
.w-flank #icons .icon:nth-child(2),
.w-flank #icons .icon:nth-child(5) { top: 58vh; }
.w-flank #icons .icon:nth-child(3),
.w-flank #icons .icon:nth-child(6) { top: 72vh; }
/* a tick rule running out toward the frame, the way a callout does */
.w-flank #icons .icon::after {
  content: ""; position: absolute; top: 50%; width: clamp(14px, 3vw, 46px);
  height: 1px; background: linear-gradient(to right, rgba(240,168,60,.42), transparent);
}
.w-flank #icons .icon:nth-child(-n+3)::after { left: 100%; }
.w-flank #icons .icon:nth-child(n+4)::after  {
  right: 100%; background: linear-gradient(to left, rgba(240,168,60,.42), transparent);
}

/* --- RAIL: one column down the left, a targeting list ---------------------- */
.w-rail #icons {
  position: fixed; left: clamp(20px, 4.5vw, 92px); top: 50%;
  translate: 0 -50%;
  display: grid; gap: clamp(12px, 1.8vh, 20px);
  justify-items: start; z-index: 3;
  padding-left: 14px;
  border-left: 1px solid rgba(240, 168, 60, .22);
}
.w-rail #icons .icon { flex-direction: row; align-items: center; gap: 10px; }
.w-rail #icons .icon .spr { order: 0; }
.w-rail #icons .icon__l  { order: 1; }
/* corner ticks on the rail, so it reads as an instrument rather than a menu */
.w-rail #icons::before, .w-rail #icons::after {
  content: ""; position: absolute; left: -1px; width: 9px; height: 1px;
  background: rgba(240, 168, 60, .5);
}
.w-rail #icons::before { top: 0; }
.w-rail #icons::after  { bottom: 0; }

/* --- RING: removed Aug 30 2026, on Juan's call ----------------------------
   The lying-down composition — nature ringing all four edges, the copy in a
   transparent hole in the middle — was built, rendered in three scenes and
   judged. He does not want it. The layout, the three `world-r*` assets and the
   D/E/F ring scenes in every form came out together; the six remaining assets
   are the landscapes, A/B/C. Kept in git history rather than in the tree. */

/* ============================================================= THE GLOW ===
   Drawn, not loaded. Only the X option uses it: that illustration is a
   nature-only render with a genuinely black sky, so its key is a threshold
   rather than an inference, and the light it is missing gets put back here.
   Shape and colour are G's own, measured (see the README); the centre is moved
   to this render's horizon at 60.7% rather than G's 45.4%. */
.wglow { position: absolute; left: 0; top: 0; z-index: 0; pointer-events: none;
         mix-blend-mode: screen; display: none; }
/* ⚠️ THE GLOW IS TIED TO THE ILLUSTRATION, and :has() is what ties it.
   .wglow lives outside .phosphor so that `screen` is not isolated by the
   filter — but the router replaces .phosphor's CONTENTS and nothing else, so
   anything out here SURVIVES A NAVIGATION. The world does not: it is inside
   .phosphor and goes when the page changes. Without this rule the glow stayed
   lit on /work and /code, a horizon light for an illustration that is no longer
   on screen. Scoped to the presence of a .world, it appears and leaves with the
   picture it belongs to. */
body:has(.world) .wglow--on { display: block; background: radial-gradient(115.5% 22.4% at 50.7% 63.0%,
      rgb(255 202 82) 0.0%,
      rgb(255 179 72) 5.6%,
      rgb(255 144 58) 11.1%,
      rgb(211 108 44) 16.7%,
      rgb(150 77 31) 22.2%,
      rgb(102 52 21) 27.8%,
      rgb(67 34 14) 33.3%,
      rgb(42 21 9) 38.9%,
      rgb(26 13 5) 44.4%,
      rgb(15 8 3) 50.0%,
      rgb(9 4 2) 55.6%,
      rgb(5 2 1) 61.1%,
      rgb(3 1 1) 66.7%,
      rgb(1 1 0) 72.2%,
      rgb(1 0 0) 77.8%,
      rgb(0 0 0) 83.3%,
      rgb(0 0 0) 88.9%,
      rgb(0 0 0) 94.4%,
      rgb(0 0 0) 100%); }

/* ⚠️ THE HOME PAGE IS ONE VIEWPORT, AND IT WAS SCROLLING 166px.
   world-fit hangs the picture off the bottom on purpose when it is taller than
   the room below the copy — "losing the near foreground, never the peaks". That
   is right, but the overhang was creating SCROLLABLE OVERFLOW rather than being
   clipped: the art's box ran to 1519 in a 1353 viewport, and .wglow mirrors that
   box, so the document grew and the page scrolled to a strip of nothing.

   Clipped at the body, and scoped with :has(.world) for the reason the rest of
   this file is — .tube and .phosphor are on every page, and /work and /code
   have real content that MUST scroll. Only the page with a world in it is
   pinned to the viewport. */
html:has(.world), body:has(.world) { height: 100%; overflow: hidden; }
