/* work.css — the third scene. Spec: 220, which supersedes 059 §4 and §5 for
   everything in this file.

   Division of labour, the same one the hero and intro already use: work.js
   feeds progress and geometry, this file owns magnitude, colour and direction,
   so the design gate can reach every value without opening a script.

   ⚠️ NO clamp() ANYWHERE, AND THAT IS DELIBERATE (220 §9.4). This file has
   never contained one. The viewport is measured on resize and the globe's size
   is written once per layout — one of the site's two working answers to
   responsive sizing, and the one that survived the overhaul. */

#work {
  /* Pure black. Inherited from the plate era, where the character's PNG had no
     alpha and showed a faint rectangle on base.css's #101010. The plate is
     gone; the black stays because the globe's own glow falls off into it and
     #101010 puts a visible seam at the section boundary. */
  background: #000;

  /* ⚠️ DECLARED ON THE SECTION, NOT ON THE CHILDREN THAT CONSUME IT. The units
     layer reads --sphere-in and work.js writes it here; a declaration on the
     consuming element would SHADOW the inherited value and the build-in would
     be a silent no-op. hero.css's header records the same rule and 035 §5 hit
     that shape twice.

     Defaults to 1 — the finished state. The pre-state is applied by JS and only
     when there is an IntersectionObserver that will take it off again, so a
     browser without one, or a reduced-motion visitor, gets a section that is
     simply present rather than empty. */
  --sphere-in: 1;
}

.work {
  position: absolute;
  inset: 0;
  /* The section's stub .section__inner rules centre their content; this scene
     positions everything itself. */
  width: 100%;
  height: 100%;
}

/* ⚠️ 059 §9's reduced-motion block kept its shape and lost its plate line.
   Stated as a value rather than left to whatever the last frame happened to be:
   a correct static state and a silently broken one look identical (036 §7.5). */
@media (prefers-reduced-motion: reduce) {
  #work { --sphere-in: 1; }
}

/* ── THE GLOBE (220 §5, §9.4) ───────────────────────────────────────────
   ⚠️ COLOUR LIVES HERE AND NOWHERE ELSE. globe.js reads these two and converts
   them to the 0-1 triples cobe wants, on every layout. 036 §0 records the old
   dot field failing partly because its colour was specified nowhere and was
   inferred silently in code.

   ⚠️ THERE IS NO --globe-marker. cobe's markerColor is inert with markers: []
   — swapping it produces a bit-identical frame — and the pulse dot's colour is
   an ordinary CSS property on our own element, further down. A custom property
   feeding a renderer that ignores it is a flag that moves no pixel. */
#work {
  --globe-base: #434857;
  --globe-glow: #1a1f33;
}

/* ⚠️ SIZED AND PLACED BY work.js, not by this file. It is the canvas's
   dedicated parent and must stay exactly the canvas's box: cobe's inserted
   wrapper takes 100% of whatever it finds. */
.work__stage {
  position: absolute;
  z-index: 2;
}

/* ⚠️ CLASS, NOT `.work__stage > canvas`. cobe reparents this element on init
   and a child combinator stops matching — silently, taking its positioning
   with it. Nothing in this file may address the canvas by structure. */
.work__globe {
  display: block;
  width: 100%;
  height: 100%;
  /* ⚠️ HORIZONTAL IS OURS, VERTICAL IS THE PAGE'S (220 §8). pan-y permits the
     browser's vertical scroll and reserves horizontal gestures for script.

     ⚠️ IT IS CONSULTED ON THE ELEMENT THE HIT TEST REACHES, intersected up to
     the nearest scrollable ancestor — hero-trail.css's header records the rule
     and projects.css:131-143 records the same error in the opposite direction.
     The units layer above is pointer-events:none, so the canvas IS the hit
     element; __work.touchAction() reads the resolved value there rather than
     trusting this declaration. */
  touch-action: pan-y;
}

/* ⚠️ THE STILL FRAME (220 §6). One presentation, three triggers: reduced
   motion, no WebGL, and a context lost mid-visit all land here. It is DRAWN
   ONCE and does not turn — the machines that reach this path are the ones with
   no working GPU path, and thousands of fillRect calls per frame is the cost
   they are least able to pay.

   A canvas holds one context type for its lifetime, so this cannot share the
   WebGL one. It is created by JS only when it is needed, so an ordinary visitor
   never has it in the DOM at all. */
.work__globe-flat {
  position: absolute;
  inset: 0;
  display: block;
  visibility: hidden;
}

#work {
  --globe-flat-dot: #8e97a8;
  /* The silhouette the shader gives the WebGL globe for free. */
  --globe-flat-rim: rgba(140, 150, 175, 0.28);
}

/* ── THE UNITS (220 §7) ─────────────────────────────────────────────────
   A unit is one dot and one label, and they appear and disappear together —
   Xinlu's words, 2026-09-08: "all labels should appear and disappear along with
   the pulse dots." So the section's words are never all on screen at once, and
   that is the design rather than a limitation. */
.work__units {
  position: absolute;
  inset: 0;
  /* ⚠️ THE CANVAS IS THE HIT ELEMENT AND THIS MUST NOT TAKE IT. touch-action is
     consulted on the element the hit test actually REACHES, intersected up to
     the nearest scrollable ancestor — hero-trail.css's header records the same
     rule and projects.css:131-143 records the PM getting it wrong in the
     opposite direction. 133 §5.6 measured a pointer-events:none element
     vanishing from elementsFromPoint entirely, which is exactly what is wanted
     here: the drag belongs to the globe underneath. */
  pointer-events: none;
  /* The build-in, consumed from #work — never redeclared here, or it would
     shadow the inherited value and the fade would be a silent no-op. */
  opacity: var(--sphere-in, 1);

  --unit-dot: #4de0a0;
  --unit-ring: rgba(77, 224, 160, 0.45);
  /* ⚠️ READ BY JS AS WELL AS BY THE KEYFRAME (220 §7.2). The placement test
     needs the ring's maximum extent to build a unit's box; if this lived only
     in the animation, ?worklabelgap= would become a number someone hand-tunes
     to match a stylesheet and the two would drift silently. One declaration,
     two consumers — the same reason .work__dots's colour lived in CSS. */
  --unit-ring-max: 15px;
  --unit-dot-size: 9px;
}

.work__unit {
  position: absolute;
  /* left/top are written per frame by units.js as percentages of this box,
     which is the canvas's box. translate keeps the DOT on the point. */
  translate: -50% -50%;
  will-change: transform, opacity;
}

.work__unit-dot {
  position: absolute;
  left: 50%;
  top: 50%;
  width: var(--unit-dot-size);
  height: var(--unit-dot-size);
  margin: calc(var(--unit-dot-size) / -2) 0 0 calc(var(--unit-dot-size) / -2);
  border-radius: 50%;
  background: var(--unit-dot);
}

/* Two rings on staggered delays. Pure CSS: this is the thing Route A could not
   have done without driving a WebGL marker's size per frame in JS. */
.work__unit-dot::before,
.work__unit-dot::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 50%;
  animation: work-unit-pulse var(--unit-pulse, 2s) ease-out infinite;
}
.work__unit-dot::after { animation-delay: calc(var(--unit-pulse, 2s) / 2); }

@keyframes work-unit-pulse {
  0%   { box-shadow: 0 0 0 0 var(--unit-ring); }
  70%  { box-shadow: 0 0 0 var(--unit-ring-max) rgba(77, 224, 160, 0); }
  100% { box-shadow: 0 0 0 0 rgba(77, 224, 160, 0); }
}

/* ⚠️ THE SIZE IS A DIAL NOW AND IT WAS NOT ONE (246 §3.1). This rule set no
   font-size at all and inherited 16 px, so 245 could only measure what a dial
   would buy with a harness override — 11 px gave 1.54 mean labels against 0.89
   and halved the disc overflow. On a phone the globe is nearly screen-width, so
   a label leaving the disc leaves the SCREEN.

   ⚠️ THE FALLBACK IS 16px BECAUSE THAT IS WHAT IT INHERITED. place.js writes
   --unit-label-size every layout from the per-breakpoint flag, and the desktop
   arm resolves to 16 — introducing a dial must not move a signed-off
   composition, and C91 asserts the desktop label box is unchanged. */
.work__unit-label {
  font-size: var(--unit-label-size, 16px);
  position: absolute;
  left: 50%;
  /* Label ABOVE the dot (212 §4.2), clear of the outermost ring. */
  bottom: calc(50% + var(--unit-ring-max) + 4px);
  translate: -50% 0;
  white-space: nowrap;
  /* ⚠️ UPPERCASE VIA text-transform, NOT IN THE SOURCE. The source list is the
     accessible text and the copy pass's one file; casing it here keeps it
     readable prose and keeps a screen reader reading it as prose. */
  text-transform: uppercase;
  letter-spacing: 0.08em;
  font-weight: 500;
  color: #fff;
  text-shadow: 0 1px 3px #000;
}

/* ⚠️ RANKING IS XINLU'S DEFAULT, NOT A HOUSE STYLE (220 §7.2). The eight
   client-facing words render brighter than the eight technical ones; his words,
   2026-09-08: "We can try making the eight client-facing words brighter to see
   how it looks." ?workrank=0 renders every unit equal and both behaviours are
   built, because it is a looking question. */
.work__unit[data-pool='technical'] { opacity: var(--unit-dim, 0.55); }
.work__units[data-rank='0'] .work__unit[data-pool='technical'] { opacity: 1; }

/* The measuring row. Off-screen, never painted, and it exists so the 16 label
   widths are measured ONCE PER LAYOUT rather than per spawn. */
.work__measure {
  position: absolute;
  left: -9999px;
  top: 0;
  visibility: hidden;
  white-space: nowrap;
}
.work__measure span { position: absolute; white-space: nowrap; }

/* ⚠️ THE RINGS STOP, THE WORDS DO NOT (220 §9.1). Under `reduce` the units are
   placed once and held: no pulse, no rotation, and every one of them still
   readable. The animation is removed, not the element — the words are the
   section's content and a reduced-motion visitor is not one who wants an empty
   section. Last in the file so it wins over the .work__unit-dot rules above
   without needing a stronger selector. */
@media (prefers-reduced-motion: reduce) {
  .work__unit-dot::before,
  .work__unit-dot::after { animation: none; box-shadow: none; }
}

/* ── THE ARCS (225) ─────────────────────────────────────────────────────
   ⚠️ THIS STYLESHEET OWNS THE ARCS' COLOUR AND NOTHING ELSE OF THEM, and that
   is the price of the canvas ruling (225 §1). The fade, the width, the reveal
   and the sample count are numbers in js/sections/work/arcs.js and in the
   ?workarc* flags — a rule added here for stroke width or opacity will do
   nothing. Canvas was chosen on 224's measurement: one SVG path per arc, its
   `d` rebuilt and re-parsed every frame, cost 0.41–0.54 ms per frame at 15 arcs
   against canvas's 0.135, which at the top of the mesh's range is the same
   order as the globe's entire GPU cost.

   The fifth custom property this section reads into a rendering context,
   alongside --globe-base, --globe-glow, --globe-flat-dot and --globe-flat-rim. */
#work { --work-arc: #4de0a0; }

/* ⚠️ CLASS SELECTOR, AND pointer-events: none IS LOAD-BEARING (225 §2).
   This layer sits above the globe canvas and below the units. Without
   pointer-events: none it becomes the hit element, and the drag — which is
   bound to the globe canvas and relies on its touch-action: pan-y — would
   silently stop passing vertical scroll to the page. */
.work__arcs {
  position: absolute;
  inset: 0;
  display: block;
  pointer-events: none;
  /* Under the dots and the labels (225 §6.2): a crossing should read as depth,
     not as collision. Both layers are in .work__stage, so DOM order decides and
     this only makes the intent explicit. */
  z-index: 0;
  opacity: var(--sphere-in, 1);
}

/* ── THE RIGHT HALF (230) ───────────────────────────────────────────────
   ⚠️ THE PANEL IS GIVEN NO z-index, AND THE REASON IS WRITTEN HERE BECAUSE THE
   INSTINCT WHEN TEXT IS HIDDEN IS TO RAISE IT.

   .work__stage carries z-index: 2. That — not the panel's pointer-events — is
   what keeps the globe canvas as the hit element and the drag alive. Measured
   (229 §5.2): a panel with NO z-index leaves elementFromPoint returning
   .work__globe with a resolved touch-action of pan-y; the SAME panel at
   z-index: 3 makes elementFromPoint return .work__panel and the resolved
   touch-action `auto`, and the drag dies silently.

   ⚠️ SO THE HAZARD IS THE INVERTED ONE: if the panel and the stage ever overlap,
   the globe paints ON TOP OF this text — and raising the panel's z-index, which
   is the obvious fix for that, is the exact change that kills the drag.

   The fix is not a z-index. The panel's left edge IS the stage's right edge,
   both derived from ?worksplit=, so overlap is impossible by construction.
   C70 asserts the two rects do not intersect — the cause, not the symptom — and
   C53's control is the z-index: 3 arm, which is the defect itself. */
.work__panel {
  position: absolute;
  top: 0;
  bottom: 0;
  /* left and width are written once per layout by panel.js from ?worksplit=.
     ⚠️ clamp() stays out of this file. */
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: 0 5.5%;
  /* The build-in, consumed from #work — never redeclared on a child, or it
     shadows the inherited value and the fade becomes a silent no-op. */
  opacity: var(--sphere-in, 1);

  /* ⚠️ THE SIXTH CUSTOM PROPERTY THIS SECTION READS INTO A RENDERING CONTEXT,
     beside --globe-base, --globe-glow, --globe-flat-dot, --globe-flat-rim and
     --work-arc. Based on --accent (#6E7FB8) and tuned AGAINST the globe's glow,
     not alone: the section has two light sources facing each other and different
     hues read as a mistake. ?worklampc= overrides it at runtime. */
  --work-lamp: #6E7FB8;
  /* ⚠️ THE SEVENTH CUSTOM PROPERTY THIS SECTION READS INTO A RENDERING CONTEXT,
     and the only one that is deliberately NOT tuned against the globe. The bar
     is an emitter and reads white-hot; --work-lamp is the spill. See
     .work__lamp-bar for why a hairline of the muted hue does not work.
     ?worklampbarc= overrides it, ?worklampc= still overrides the spill, and
     234 §5 requires both to be judged across the section rather than alone. */
  --work-lamp-bar: #E8EEFF;
  --crease: 1;
}

/* ⚠️ MEASURED AND NOT ADJUSTED: on this hardware the lamp is below the
   instrument's resolution — a blur(300px) control raised raster 2.5x and the
   realistic gradient sat in the noise, so the instrument is not blind and the
   effect is genuinely small here. Blur lands in RASTER and raster is where a
   mid-range Android is slowest, so ?worklampblur= is a flag and goes on Xinlu's
   phone before it is settled. No dpr ceiling needed on an M4. */
/* ── THE LAMP IS A FIXTURE, AND IT IS FIVE PARTS (234 §2) ──────────────────
   ⚠️ WHAT WAS HERE WAS ONE RADIAL GRADIENT ANCHORED AT 50% 0% OF THE PANEL. It
   had no bar, no cone and no hard edge, so what it drew was the top of the box
   glowing — light from somewhere off-screen. 230 §5 specified a light's
   PROPERTIES (colour, spread, blur, floor) and never its ANATOMY, and this is
   what that produced.

     1. the BAR     a hairline, sharp-edged, the brightest thing in the panel.
                    ⚠️ IT IS AN OBJECT, NOT A GRADIENT. Without it there is no
                    emitter and the light has no source.
     2. the CORE    small, intense, tightly blurred, under the bar's centre.
     3. the CONE    ⚠️ DIRECTIONAL, NOT RADIAL. Two conic gradients whose apex
                    is the bar's line, spreading down and outward from it.
     4. the HALO    faintest and broadest.
     5. DARKNESS ABOVE THE LINE. ⚠️ THIS IS THE PART THAT MAKES IT A LAMP.
                    Light glowing in every direction is a blob in space; light
                    with a hard top edge and a visible emitter is a fixture.

   ⚠️ AND THE LAMP IS NOW THE FIRST ITEM IN A VERTICAL STACK, NOT A BACKDROP
   (234 §3). Xinlu asked for the copy to move down "so that it leaves space for
   this lamp and the light source beneath the lamp". Moving the sentence down
   while the lamp stayed pinned to top:0 would have moved them FURTHER APART —
   the sentence would slide away from the light rather than into it. So the lamp
   joins the flex column the copy is in, and the whole stack centres together:

       bar -> light spilling DOWN -> the sentence, sitting IN it -> the figures

   Its own box is the bar's thickness and nothing more. Every light layer is
   absolutely positioned inside it, so the spill costs no layout and the gap
   below is ?workleadtop= alone. */
.work__lamp {
  position: relative;
  flex: 0 0 auto;
  align-self: stretch;
  height: var(--lamp-bar-t, 2px);
  /* ⚠️ THE NEGATIVE TOP MARGIN IS ?worklampgap= AND IT IS NOT A HACK (236 §4).
     The panel centres its column, so adding headroom above the bar would grow
     the stack and push the COPY DOWN by half of it — Xinlu asked for the bar to
     rise, not for the sentence to move again. -G against +G leaves the stack's
     height unchanged, so the copy stays exactly put and only this box rises.
     panel.js records the ceiling: the bar meets the panel's top edge at
     ?worklampgap= equal to its resting y. */
  margin: calc(-1 * var(--lamp-gap, 0px)) 0
          calc(var(--lamp-lead-top, 2.4em) + var(--lamp-gap, 0px)) 0;
  pointer-events: none;
  /* ⚠️ THE ONE VALUE THE TIMELINE WRITES, AND BOTH HALVES READ IT (234 §4).
     Xinlu: "at the start the lamp is a short horizontal bar centred (not a
     broken bar), and then it extends to both sides. At the same time, as the
     bar extends, the light source changes along with it."

     The bar's width and the light's width are each calc()'d from this ONE
     inherited number, so "the cone, the core and the halo widen on the same
     tween" is structural rather than a convention two tweens have to keep.
     Defaults to 1 so a lamp nothing has animated is a finished lamp, never a
     stub — same rule as --sphere-in above. */
  --lamp-ext: 1;
}

/* ⚠️ EVERY LIGHT LAYER LIVES IN HERE, AND THE MASK IS PART 5 (234 §2.1).
   The reference occludes with opaque blocks painted in the page's background
   colour. Do not copy that: it puts #000 in a SECOND place, and the first drift
   between the two shows up as a visible rectangle across the panel. A mask is
   background-independent — it removes light rather than painting over it, so it
   stays correct if #work's ground ever changes.

   The box's TOP EDGE IS THE BAR'S LINE, so an opaque mask the size of this box
   is exactly "nothing glows above the bar" — including the blur bleed from the
   core and the halo, which paint outside their own boxes and would otherwise
   escape upward. The bottom stop is soft because ?worklamph= is where the light
   RUNS OUT, and a hard edge there would read as a cut rather than as falloff. */
.work__lamp-light {
  position: absolute;
  top: 0;
  left: var(--lamp-cx, 50%);
  width: calc(var(--lamp-light-w0, 20%) +
              (var(--lamp-light-w, 80%) - var(--lamp-light-w0, 20%)) * var(--lamp-ext, 1));
  height: var(--lamp-h, 60%);
  translate: -50% 0;
  /* ?worklampblur= lands HERE and not on .work__lamp — blurring the parent
     would blur the bar, and a blurred hairline is not an emitter. */
  filter: var(--lamp-blur, none);
  /* ⚠️ TWO MASK LAYERS INTERSECTED, AND THE SECOND ONE IS A BUG FIX. With the
     vertical mask alone, the halo's 78 px blur was cut square at this box's left
     and right edges and the light's RECTANGLE became visible as two hard
     verticals — the occlusion drawing attention to its own boundary, which is
     the failure 234 §2.1 is about even though it is not painting a colour. The
     horizontal layer fades those two edges out; the vertical layer keeps the
     hard top, because that edge is part 5 and MUST stay hard. */
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, #000 58%, transparent 100%),
                      linear-gradient(to right, transparent 0%, #000 14%, #000 86%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 0%, #000 58%, transparent 100%),
                      linear-gradient(to right, transparent 0%, #000 14%, #000 86%, transparent 100%);
  -webkit-mask-composite: source-in;
          mask-composite: intersect;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
}

/* ⚠️ CONIC, ANCHORED AT THE BAR'S LINE — the apex of each half is the point
   where the two halves meet, which is the bar's centre. A radial gradient would
   spread light in every direction from a point; these spread it DOWNWARD and
   OUTWARD from a line, which is what a bar-shaped emitter does.

   The angles are absolute: `from 180deg` starts the sweep pointing straight
   down and CSS sweeps clockwise, so the left half runs down -> left over 72deg.
   The right half is the mirror and has to run the other way round its stops,
   because clockwise from its apex goes right -> down. */
/* ⚠️ ONE SYMMETRIC CONE, NOT TWO MIRRORED ONES, AND TWO BUILDS FAILED BEFORE
   THIS. 234 §2.3 describes the reference's two conic gradients; a single conic
   anchored at the bar's centre draws the same cone and cannot do either of the
   things the paired version actually did here.

   ⚠️ FAULT 1 — A CONIC GRADIENT IS CONSTANT ALONG EVERY RAY FROM ITS APEX, so
   light 500 px from the bar came out exactly as bright as light 10 px from it:
   the panel filled with a flat wash inside a visible rectangle. A cone falls off
   with ANGLE and with DISTANCE and the conic supplies only the first. The mask
   below is the second, and the two multiply.

   ⚠️ FAULT 2 — THE SPREAD MUST FIT WHAT THE BOX SUBTENDS. Each half was
   252 x 558 px, so from its apex it spanned only atan(252/558) = 24.3deg. The
   first build faded over 72deg, three times the available wedge, so the falloff
   happened entirely outside the element and every pixel took the first stop.

   ⚠️ FAULT 3, AND IT IS WHY THIS IS ONE ELEMENT — the mirrored half's stops ran
   backwards through 0deg, so its outermost sliver wrapped past 360deg, took the
   LAST stop instead of the first and painted a hard bright wedge into the
   corner. The two halves rendered visibly different from identical intent. A
   single gradient centred on straight-down is symmetric BY CONSTRUCTION: there
   is no second element to disagree with, and no seam down the middle where they
   met. `from 180deg` puts gradient position 0 on the straight-down ray and the
   stops fall away to transparent on both sides of it. */
.work__lamp-cone {
  position: absolute;
  inset: 0;
  background: conic-gradient(from 180deg at 50% 0%,
              var(--work-lamp) 0deg,
              color-mix(in srgb, var(--work-lamp) 38%, transparent) 9deg,
              transparent 21deg,
              transparent 339deg,
              color-mix(in srgb, var(--work-lamp) 38%, transparent) 351deg,
              var(--work-lamp) 360deg);
  /* The distance falloff. Separate from the container's mask, which is doing a
     different job — that one cuts the top dead, this one is the light running
     out as it travels. */
  -webkit-mask-image: linear-gradient(to bottom, #000 0%, rgba(0, 0, 0, 0.42) 46%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 0%, rgba(0, 0, 0, 0.42) 46%, transparent 100%);
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  filter: blur(var(--lamp-cone-blur, 10px));
}

/* ⚠️ FAINTEST AND BROADEST, AND THE FIRST BUILD HAD IT DOMINATING. At 0.34 it
   was most of what the panel showed and it flattened the cone it was supposed to
   sit behind. It is the part you should not be able to point at. */
.work__lamp-halo {
  position: absolute;
  top: 0;
  left: 50%;
  width: 78%;
  height: var(--lamp-halo-h, 74%);
  translate: -50% -30%;
  border-radius: 50%;
  background: var(--work-lamp);
  opacity: 0.16;
  filter: blur(var(--lamp-halo-blur, 44px));
}

/* ⚠️ THE ONE PLACE THE SPILL IS ALLOWED TO GO NEAR WHITE. It is the filament
   seen through its own glow — the emitter reads white-hot and the colour
   returns within a few pixels of it (234 §5). */
.work__lamp-core {
  position: absolute;
  top: 0;
  left: 50%;
  width: var(--lamp-core-w, 30%);
  height: var(--lamp-core-h, 26%);
  translate: -50% -34%;
  border-radius: 50%;
  background: radial-gradient(closest-side,
              color-mix(in srgb, var(--work-lamp-bar) 82%, var(--work-lamp)) 0%,
              var(--work-lamp) 46%,
              transparent 100%);
  opacity: 0.9;
  filter: blur(var(--lamp-core-blur, 16px));
}

/* ⚠️ THE BAR IS THE LAMP (234 §2.1). Sharp-edged and unblurred — nothing above
   may soften it, which is why ?worklampblur= is on the light and not here.

   ⚠️ IT IS NEAR-WHITE WHILE THE SPILL CARRIES THE COLOUR (234 §5). --work-lamp
   is #6E7FB8, muted on purpose so it sits with the globe, and a hairline of
   #6E7FB8 on near-black reads as a grey-blue line rather than as a filament.
   A real lamp behaves this way: the emitter is white-hot and only the spill is
   tinted. That buys the bar its intensity without putting a foreign hue into a
   section whose two light sources were tuned against each other. */
.work__lamp-bar {
  position: absolute;
  top: 0;
  left: var(--lamp-cx, 50%);
  width: calc(var(--lamp-bar-w0, 8%) +
              (var(--lamp-bar-w, 46%) - var(--lamp-bar-w0, 8%)) * var(--lamp-ext, 1));
  height: 100%;
  translate: -50% 0;
  border-radius: var(--lamp-bar-t, 2px);
  background: var(--work-lamp-bar, #E8EEFF);
}

.work__lead {
  position: relative;
  /* ⚠️ ?workstatsgap= — THE SECOND OF 234 §3's TWO DIALS, and they tune
     different quantities: ?workleadtop= is the headroom the bar and its light
     get, this is the gap that CLOSES as the sentence comes down. Written in px
     by panel.js from the lead's own size, so it scales with the column. The
     fallback is the 2.6em this shipped as. */
  margin: 0 0 var(--lamp-stats-gap, 2.6em) 0;
  /* Blinker for titles — 156 §1.12's standing rule. */
  font-family: 'Blinker Subset', system-ui, -apple-system, sans-serif;
  font-weight: 300;
  /* ⚠️ NO clamp(). This file has never contained one and it is one of the
     site's two working answers to responsive sizing: the viewport is measured on
     resize and the size is written once per layout by panel.js. */
  line-height: 1.22;
  letter-spacing: -0.005em;
}
.work__lead-a { color: #8A8F9C; }
/* ⚠️ ?worktint= EXISTS BECAUSE THE LAMP NO LONGER DIMS (232 §3), AND THE NEED
   FOR IT IS MEASURED RATHER THAN ASSERTED. The painted 99.5th-percentile glyph
   luminance of this line and of the four figures were BOTH 255.0, because both
   were #FFFFFF. So "the figures are the brightest thing in the panel" was not
   true at any lamp setting; they were merely TIED, and the old 0.18 floor hid
   it by leaving the whole panel too dim for the tie to show.

   ⚠️ IT IS THE TIE, NOT THE LAMP, AND THE DIFFERENCE MATTERS. The glyphs are
   opaque, so the lamp adds nothing to them: measured, the ground inside both
   text boxes is 0.0 at floor 0.18 AND at floor 1. What the new floor changes is
   the panel AROUND them — mean luminance across the lamp's top rows goes 8.0 ->
   46.3 — which is why the tie only becomes visible now. Do not reach for the
   lamp to fix it.

   ⚠️ THE TINT IS ON THE SENTENCE, NOT ON THE FIGURES. Dimming the figures to
   restore the order would make the board — the thing the sequence exists to
   show — the thing that gets darker. 0 is the untinted #FFFFFF this shipped as,
   so ?worktint=0 recovers 231's rendering exactly. */
.work__lead-b {
  color: color-mix(in srgb, #8A8F9C calc(var(--work-tint, 0) * 100%), #FFFFFF);
}

/* ── ⚠️ THE ACCENT PHRASE (248 §5.1) ────────────────────────────────────────
   Xinlu: "a slightly more vibrant colour that still pairs with and accents the
   aurora colours in our hero section."

   ⚠️ DERIVED FROM THE AURORA, NOT CHOSEN TO RESEMBLE IT, and the derivation is
   recorded because the value is meaningless without it. Read out of
   js/sections/hero/sky/index.js — the shader's three stops are #06B6D4, #B497CF
   and #5227FF. This holds stop0's hue EXACTLY at 188.7, takes 92 % saturation
   against its 94.5 %, and raises lightness 42.7 % -> 58 % for legibility on
   near-black. That is the whole of it: one hue read from the artefact and one
   axis moved for contrast.

   ⚠️ THE HIERARCHY IS PROTECTED ON LUMINANCE AND ONLY ON LUMINANCE. Measured
   luma 184.1, against the sentence's white at 204.7 and the four figures at
   255.0, so it cannot out-rank them by brightness — which is what 232 §3 and 233
   exist to keep. Saturation attracts independently of luminance, so that is a
   floor rather than a proof, and 249 reports how it actually reads.

   ⚠️ THIS IS THE SECOND DELIBERATE EXCEPTION TO 248 §6.1's DESKTOP FREEZE.
   Colour lives on markup and markup renders at both breakpoints; the sentence
   itself is the first. His, dated 2026-09-14, and everything else in §6.1 holds. */
#work { --work-accent: #31DAF6; }

.work__panel[data-accent='tail'] .work__lead-c { color: var(--work-accent); }
/* core puts the accent on the phrase the line exists for and returns the tail to
   the sentence's own white — the editorial objection 248 §5.1 raised, built so he
   can see both rather than be argued at. */
.work__panel[data-accent='core'] .work__lead-b { color: var(--work-accent); }
.work__panel[data-accent='core'] .work__lead-c {
  color: color-mix(in srgb, #8A8F9C calc(var(--work-tint, 0) * 100%), #FFFFFF);
}

/* ⚠️ THE CREASE IS WHAT SELLS THE FOLD AS A PHYSICAL OBJECT, not as a CSS
   rotation, and it is re-tuned for a DARK ground: the reference blends its
   crease with `multiply`, which assumes a light surface and turns to mud here.
   A dark overlay fading out as the panel opens does the same job on black. */
/* ⚠️ THE WORD GROUP IS WHAT KEEPS LINE BREAKS AT SPACES. Per-glyph inline-block
   cells let the line break between any two letters, and the first build wrapped
   "action." as "to a / ction." — which is exactly the rendering fault 230 §6
   asks whether char-folding would look like. It would have, and not for the
   reason anyone was watching for. */
.work__word {
  display: inline-block;
  white-space: nowrap;
}

.work__fold {
  display: inline-block;
  transform-origin: 50% 0%;
  will-change: transform, opacity;
  position: relative;
}
.work__fold::after {
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom, rgba(0, 0, 0, 0.65), rgba(0, 0, 0, 0));
  opacity: calc(var(--crease, 0) * var(--crease-amt, 0.9));
  pointer-events: none;
}

/* ── THE BOARD AND ITS THREE RULES (236 §5) ─────────────────────────────────
   Thin, low-alpha, spanning the stats block's width: one under the sentence, one
   between the two rows, one below the bottom row.

   ⚠️ THREE, NOT FOUR. The reference's topmost rule belongs to an eyebrow we do
   not have and 230 §4 forbids inventing one.

   ⚠️ THEY ARE DECORATION — aria-hidden in the markup, and C69's accessible text
   must not change. And they are positioned rather than bordered because a border
   on a grid item cannot span both columns, and a background-image on the <dl>
   would need the row height hard-coded. The middle rule's y is MEASURED from the
   real gap between the rows in panel.js on every layout. */
.work__board {
  position: relative;
}
.work__rule {
  position: absolute;
  left: 0;
  right: 0;
  height: 1px;
  background: var(--work-rule, rgba(174, 182, 200, 0.16));
  pointer-events: none;
}
.work__rule--top { top: 0; }
.work__rule--mid { top: var(--rule-mid, 50%); }
.work__rule--bot { bottom: 0; }

/* ⚠️ ?workcols=1 IS 242 §7's FALLBACK, BUILT AND NOT DEFAULTED. Two columns
   keep the lamp's light falling down the channel BETWEEN them, which is what
   Xinlu means by "penetrate straight down from the centre"; one column puts the
   light through the slots. 245 §5 measured 2x2 as fitting the vertical budget at
   every supported width, so this exists as a comparison rather than as a rescue
   — and note it does NOT shrink the type, because the figure and caption sizes
   are driven by the PANEL's width, not the column's. */
.work__panel[data-cols='1'] .work__stats { grid-template-columns: 1fr; }

.work__stats {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 2.2em 2.4em;
  margin: 0;
  padding: var(--board-pad, 1.4em) 0;
}
/* ⚠️ THE FOUR SLOTS SHARE ONE LEFT EDGE, AND SO DO THE THREE PARTS OF EACH SLOT
   (236 §6). Xinlu drew one vertical line against the slots. `start` rather than
   `left` because the site has no RTL build yet and this is the value that would
   still be correct if it gained one. */
.work__stat { margin: 0; }

/* ⚠️ ?workalign= COVERS THE WHOLE PANEL, AND THE DEFAULT IS left BECAUSE §6's
   SLOT RULE IS A REQUIREMENT WHILE THE SENTENCE'S IS A QUESTION. 236 §6 rules
   the slots — one vertical edge, figure, unit and caption sharing it — and
   explicitly leaves the sentence to Xinlu, whose own screenshot has it centred.

   Making this one dial for the panel rather than two is what lets C79's control
   work: 236 §11 names ?workalign=center as the arm that must go red, and a flag
   that moved only the sentence would leave the slots left-aligned in both arms
   and prove nothing. `center` is also exactly the centred-columns layout this
   shipped as, so the two settings are a before and an after he can compare. */
.work__panel[data-align='left'] .work__lead,
.work__panel[data-align='left'] .work__stat { text-align: start; }
.work__panel[data-align='left'] .work__figure { justify-content: flex-start; }
.work__panel[data-align='center'] .work__lead,
.work__panel[data-align='center'] .work__stat { text-align: center; }
.work__panel[data-align='center'] .work__figure { justify-content: center; }

/* ⚠️ BLINKER 400, AND THE WEIGHT IS THE POINT (230 §8). Exactly one of the five
   webfaces on this site has tabular figures: this one. Blinker 300 does NOT —
   same family, same subsetter — and Poppins has none at any weight, worst spread
   33.7%. Measured at font-size 100px: Blinker 400 proportional digits span
   20.703 px and tabular-nums collapses that to 0.000.

   ⚠️ DO NOT MOVE THE FIGURES TO 300 FOR VISUAL REASONS. A light figure jitters
   21% of its font size as a digit changes — 13.4 px at a 64 px figure — and
   nothing on screen will say why. If the lighter weight is ever wanted, the
   tiles need a measured fixed width instead, which is JS, not this file. */
.work__figure {
  /* ⚠️ A FLEX ROW SO THE UNIT CAN SIT ON THE TILES' BASELINE (236 §7). The tiles
     are boxes, not text, so an inline-block unit aligns to the LINE BOX and rides
     up to the top of the plate — it did on the first build. flex-end puts it on
     the row's bottom edge, which is where a baseline is on a board like this. */
  display: flex;
  align-items: flex-end;
  justify-content: flex-start;
  font-family: 'Blinker Subset', system-ui, -apple-system, sans-serif;
  font-weight: 400;
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
  color: #FFFFFF;
  line-height: 1;
  margin: 0 0 0.28em 0;
}
/* ⚠️ THE CSS KEYFRAME FLAP IS DELETED AND THAT IS 236 §8.1 (@keyframes
   work-flap, restarted by toggling data-flip). A CSS animation OWNS ITS OWN
   CLOCK: it would not settle on tl.progress(1), would not advance under
   __work.step(dtMs), and would leave the still path showing tiles frozen
   part-way through a rotation. The glyph swap was on the one timeline and the
   MOTION was not — 226 §4 one layer down. Every rotation, brightness shift and
   glyph swap is now a tween in flap.js. C77's control re-creates this defect.

   ⚠️ NOTHING IN THIS FILE MAY ANIMATE A TILE. If a transition or an animation
   appears on .work__tile or its children, the still path is broken again and no
   check outside C77 will notice. */
.work__tiles {
  display: inline-flex;
  align-items: flex-end;
  gap: var(--tile-gap, 2px);
}

/* ── THE PLATE (236 §8.2) ───────────────────────────────────────────────────
   ⚠️ THE REFERENCE'S PLATES ARE #111827 WITH A HEAVY BEVEL AND SIT BESIDE
   NOTHING. Ours sit under a lamp and beside a dotted globe, so they are pitched
   DOWN from it: dark enough to read as an object, never bright enough to
   out-shout the figure it carries. The bevel is two inset hairlines rather than
   the reference's shadow stack, because a heavy shadow under a lamp reads as a
   second light source. */
#work {
  --tile-plate: #14171F;
  --tile-plate-lo: #0E1015;
  --tile-hinge: rgba(0, 0, 0, 0.66);
  --tile-glyph: #FFFFFF;
}

.work__tile {
  position: relative;
  display: inline-block;
  width: var(--tile-w, 0.68em);
  height: var(--tile-h, 1.06em);
  /* ⚠️ THE PERSPECTIVE IS ON THE TILE, NOT ON A PARENT. Set on .work__tiles it
     would share ONE vanishing point across the whole slot, so the outer cards
     would fall at a visibly different angle from the inner ones. */
  perspective: var(--tile-persp, 220px);
  border-radius: var(--tile-round, 3px);
  overflow: hidden;
  background: var(--tile-plate);
  font-variant-numeric: tabular-nums;
  font-feature-settings: 'tnum' 1;
  color: var(--tile-glyph);
}

/* The two static halves. Each clips half the plate; the glyph inside is
   positioned so that the top half shows a glyph's top and the bottom half the
   same glyph's bottom. */
.work__tile-half,
.work__tile-flap {
  position: absolute;
  left: 0;
  right: 0;
  height: 50%;
  overflow: hidden;
  backface-visibility: hidden;
}
.work__tile-half--top,
.work__tile-flap--front {
  top: 0;
  background: var(--tile-plate);
  border-radius: var(--tile-round, 3px) var(--tile-round, 3px) 0 0;
}
.work__tile-half--bot,
.work__tile-flap--back {
  bottom: 0;
  background: var(--tile-plate-lo);
  border-radius: 0 0 var(--tile-round, 3px) var(--tile-round, 3px);
}

/* ⚠️ THE HINGE IS WHERE THE CARD PIVOTS, AND THE ORIGINS ARE THE WHOLE
   MECHANISM. The front card hangs from the hinge and falls toward the viewer;
   the back card stands behind it and falls down into place. Reversing either
   origin makes the cards pivot about their far edges and the illusion is gone.
   flap.js drives both rotations; this file only says where they turn. */
.work__tile-flap--front { transform-origin: center bottom; z-index: 2; }
.work__tile-flap--back  { transform-origin: center top;    z-index: 1; }

.work__tile-g {
  position: absolute;
  left: 0;
  right: 0;
  height: 200%;
  line-height: var(--tile-h, 1.06em);
  text-align: center;
}
.work__tile-half--top .work__tile-g,
.work__tile-flap--front .work__tile-g { top: 0; }
/* The bottom half shows the SAME glyph's lower half, so its box is pulled up by
   one half-tile rather than the glyph being redrawn. */
.work__tile-half--bot .work__tile-g,
.work__tile-flap--back .work__tile-g { top: -100%; }

/* ⚠️ ?workflapbox=0 IS THE PLATELESS BOARD 230 §7.5 SHIPPED, KEPT REACHABLE.
   The default is now 1 (236 §8) because Xinlu pointed at the boxed tile, but a
   flag whose old value renders identically is a flag that moves no pixel. The
   cards still fall — only the plate, the bevel and the hinge come off. */
.work__panel[data-box='0'] .work__tile,
.work__panel[data-box='0'] .work__tile-half,
.work__panel[data-box='0'] .work__tile-flap {
  background: transparent;
  border-radius: 0;
}
.work__panel[data-box='0'] .work__tile-hinge { display: none; }

.work__tile-hinge {
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 1px;
  margin-top: -0.5px;
  background: var(--tile-hinge);
  z-index: 3;
  pointer-events: none;
}

/* ── THE INLINE UNIT (236 §7) ───────────────────────────────────────────────
   Small, beside the figure, on the same baseline. Not a tile: it never flips,
   so it must not be in the tile row's box or it would inherit the plate.

   ⚠️ NAMED work__uom BECAUSE work__unit IS ALREADY THE GLOBE'S. The units layer
   above uses .work__unit / .work__unit-dot / .work__unit-label for the pulsing
   dots on the sphere, and the first build of this reused .work__unit — which
   silently inherited `position: absolute; translate: -50% -50%` and laid the
   figure's unit on top of its own tiles. 236 §10 asks for every new FLAG to be
   checked against the inventory; the same check was owed to every new CLASS. */
.work__uom {
  display: block;
  font-size: var(--unit-size, 0.42em);
  font-weight: 400;
  line-height: 1.5;
  margin-left: 0.34em;
  color: var(--work-unit, #AEB6C8);
  letter-spacing: 0.01em;
}
/* 236 §7's table puts slot 3's unit to the RIGHT of the figure, which produces
   "0$". Raised in 236 §7 and again in 237; it is Xinlu's to settle. The rule
   exists so the flag is visible in the stylesheet and not only in a document. */
.work__figure[data-unit-side='right'] .work__uom { margin-left: 0.28em; }

/* ⚠️ Poppins for body — the labels are body text and do not change (156 §1.12). */
/* ── THE CAPTION (236 §7.1) ─────────────────────────────────────────────────
   Xinlu asked for lighter AND smaller than it was.

   ⚠️ LIGHTER IS NOT AVAILABLE AND THE REASON IS AN ASSET ONE. Poppins is
   subsetted at 300, 400 and 500 only, and the caption was ALREADY 300 — the
   lightest face this site carries. A `font-weight: 200` here would not fail; CSS
   font matching would silently serve the 300 face, so the flag would read
   nothing and look like it worked. That is the entry-10 sub-case fonts.css's
   header describes, and it is why ?workcapweight= is restricted to the three
   weights that exist. 000 §6 makes payload binding: Poppins 200 would be a new
   ~4.6 KB woff2 on a path cached immutable for a year, and it is not shipped on
   a preference.

   ⚠️ CONTRAST IS A FLOOR, NOT A PREFERENCE. These are already the dimmest text
   in the panel and 236 §7.1 asks for both variables to move the wrong way at
   once. The colour is therefore LIFTED as the size drops — measured in 237 — so
   the shipped default clears the site's floor rather than clearing it by
   accident at one size and failing at the next. */
.work__label {
  font-family: 'Poppins Subset', system-ui, -apple-system, sans-serif;
  font-weight: var(--cap-weight, 300);
  color: var(--work-caption, #A7AEBC);
  margin: 0;
  line-height: 1.4;
  letter-spacing: 0.01em;
}

/* ⚠️ THE SENTENCE THAT STOOD HERE WAS STALE AND IS DELETED: "$0 is the one
   figure that cannot animate — its final value is its starting value — so it is
   sequenced last and lands instantly." 236 §9 removed the instant slot and
   238 §7 gave that slot a starting figure of 989, so it now rolls for longer
   than any other. panel.js's own comment already recorded the first half of
   that; this file kept asserting the behaviour it describes. 000 §5 entry 46 —
   a comment that enumerates current behaviour goes stale, and this one was
   found by looking at what a change was adjacent to rather than by reading it.

   What survives is the reason the caption is tied tightly to the figure: a
   large 0 in a grid of claims can be read at a glance as "the project costs
   nothing", so the label has to be legible in the same glance that catches the
   figure. That is still true and is now load-bearing, because the roll ENDS on
   that 0 rather than sitting on it from the start. */

/* ⚠️ Under reduce the panel is the sequence's SETTLED END STATE, reached by
   tl.progress(1) in JS — not by rules here. The only thing this block does is
   stop the tile flap, which is a CSS animation and therefore this file's. */
@media (prefers-reduced-motion: reduce) {
}

/* ⚠️ ?workflapbox=1 — THE PLATES, AND THE NAME MATTERS. Not ?workplate=: "plate"
   was the removed character and the name would mislead whoever greps for it.
   My instinct is no plates — the motion and Xinlu's typography, no boxes,
   because four blocks of chrome beside a dotted globe is a different visual
   language — but it is his to look at, so both are built and 0 is the default. */
