/* GLOO — site-dark.
 *
 * Two layers, and the boundary between them is the .js class:
 *
 *   · everything up to "THE MOTION LAYER" is the page as it is when nothing
 *     moves — with scripting off, or under prefers-reduced-motion. It is not
 *     a degraded page. It is the finished state of every gesture on it: the
 *     constellation drawn, the four panels stacked and readable, the light
 *     band open and lit.
 *   · everything after it is scoped to .js and does nothing but map two
 *     numbers, --p and --q, onto transforms. No colour, no size, no layout
 *     decision lives down there that is not already resolved above.
 *
 * The ground is #0a0a0a everywhere except one band. That band is the whole
 * argument of the design and it is section 5.
 */

/* ═══ base ══════════════════════════════════════════════════════════════ */

*, *::before, *::after { box-sizing: border-box; }

html {
  background: var(--void);
  -webkit-text-size-adjust: 100%;
}
/* NOT overflow-x on this element. body's overflow-x below propagates to the
   viewport (the root's own is visible, so the viewport takes body's), which is
   what actually clips, and it clips without cost. Setting overflow-x on html
   instead makes the root a scroll container and every sticky pin on the page
   stops sticking — measured: the hero and the constellation both scrolled
   straight past their pins. */
@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }
}

body {
  margin: 0;
  /* one horizontal guard for the whole document. Nothing below is allowed to
     rely on it — every band is measured to fit — but the conveyor track is
     400% wide and one mistake there must not become a scrollbar. */
  overflow-x: hidden;
  background: var(--void);
  color: var(--ink);
  font-family: var(--font-body);
  font-weight: 300;
  font-size: var(--text-base);
  line-height: 1.9;
  letter-spacing: 0.03em;
  font-feature-settings: "palt" 1;
  -webkit-font-smoothing: antialiased;
}

::selection { background: var(--amber); color: var(--void); }

img, svg, picture { display: block; max-width: 100%; }
h1, h2, h3, p { margin: 0; }

a { color: inherit; text-decoration: none; }
:focus-visible {
  outline: 2px solid var(--amber);
  outline-offset: 3px;
  border-radius: 1px;
}

.skip {
  position: absolute;
  left: var(--space-md);
  top: -4rem;
  z-index: calc(var(--z-shutter) + 1);
  padding: var(--space-sm) var(--space-md);
  background: var(--ink);
  color: var(--void);
  font-size: var(--text-sm);
  transition: top var(--dur-micro) var(--ease-out);
}
.skip:focus { top: var(--space-md); }

/* ── shared primitives ──────────────────────────────────────────────────
   .plate is the museum wall label: 11px, wide-tracked, --dim (6.29:1). It
   appears once per section and is the page's whole navigation aid — in a
   room this dark the reader needs to be told what they are looking at. */
.plate {
  font-size: var(--text-plate);
  font-weight: 300;
  line-height: 1.6;
  letter-spacing: var(--track-plate);
  text-transform: uppercase;
  color: var(--dim);
}

/* A .frame is a box of a known aspect with a photograph covering it. Every
   photograph on this page has already been crushed to the ground colour and
   vignetted into it, so no frame ever needs a border, a scrim or a filter. */
.frame { position: relative; overflow: clip; }
.frame__img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 50%;
}

.section-h {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: var(--text-section);
  line-height: 1.02;
  letter-spacing: 0.02em;
  margin-top: var(--space-sm);
}

/* 六's body spec for ABOUT, which is the only place .prose is used: 14–16px,
   line-height 1.8–2.0, 34–40em. It was --text-md at 17px on --measure. Smaller
   type with a wider measure is the trade a pinned screen wants — the column now has
   one screen of height for five paragraphs and a 64px heading above them. */
.prose { max-width: 36em; }
/* balance on the closing paragraph only, and only this one because it is the only one
   that had an orphan — measured, the other four end at 0.21, 0.52, 1.0 and 0.34 of
   their widest line, which is ordinary rag. Balancing body copy everywhere would be a
   device; here it is a repair. It needs the nowrap on .prose strong above to not break
   the thesis phrase while it evens the lines. */
.prose p:last-child { text-wrap: balance; }

/* Every paragraph the same. The first one used to be --ink, and with ABOUT's
   confirmed copy that put three bright things in one column — the 明朝 lead, this
   paragraph, and the closing 「つながりを機能させる」 — so the client read the first
   paragraph as wrong: 「これの色は何かおかしいと思うねんけど」.

   He is right, and the cost was on the phrase rather than on the paragraph: the one
   line lifted for emphasis was no brighter than an ordinary opening sentence, so the
   emphasis meant nothing. And the paragraph it was competing with is a scene-setter
   — 「プロジェクトには、異なる立場や専門性…」 — which is the least emphatic sentence in the
   section.

   So the body is one register now and the arc is legible: the lead states it in 明朝
   at --ink, five paragraphs carry it evenly at --quiet, and exactly one phrase lifts
   back to --ink at the end. .prose is used in one place on the site, so this is a
   change to ABOUT and nothing else. */
.prose p {
  margin-top: var(--space-lg);
  color: var(--quiet);
  font-size: clamp(0.875rem, 1vw, 1rem);
  line-height: 1.9;
}
/* Emphasis by ink, not by weight. The only <strong> in the prose is ABOUT's closing
   「つながりを機能させる」, which is the HERO's own line named inside the body — it has to
   lift, and on a page whose Japanese is set at one weight throughout, bolding it
   would be the only bold on the site. --ink against --quiet is the step the lead
   already uses against the body, so the page reads it as emphasis without a new
   device — and it is now the ONLY lift in the body, which is what makes it one. 500
   rather than the body's weight is a half-step, not a shout. */
.prose strong {
  font-weight: 500;
  color: var(--ink);
  /* AND IT DOES NOT BREAK — but on its own that fixes nothing, and the record of how
     I found that out matters more than the rule.

     Set as it came, ABOUT's last line ran 30 characters and then 「す。」 alone on the
     second: a two-character orphan closing the section that states the site's thesis.
     Three candidates, measured at 1440 in one session:

       default                 30 / 2    …ということで / す。
       text-wrap: balance      17 / 15   even, but it splits 「つながりを機能させる」
                                         across the lines, inside its own quotes
       white-space: nowrap     12 / 20   ← WRONG. This reading was contaminated.

     The third step ran after the second in the same page without resetting
     text-wrap, so what it measured was balance AND nowrap together. nowrap alone
     leaves the paragraph at 30 / 2, verified afterwards with the computed style read
     back: white-space is nowrap, the phrase is a single rect, and the break still
     falls after it, at 「で / す。」. Of course it does — nowrap forbids breaking INSIDE
     the phrase and says nothing about the text following it.

     So it takes both, and they do different jobs: balance evens the two lines and
     removes the orphan, nowrap stops balance from breaking the one phrase on this page
     that must not break. Neither is sufficient. See .prose p:last-child. */
  white-space: nowrap;
}

.rule-x {
  display: block;
  height: 1px;
  background: linear-gradient(to right, var(--amber), rgb(255 181 74 / 0));
}

.band { position: relative; padding-block: clamp(5rem, 14svh, 9rem); }

/* ═══ masthead ══════════════════════════════════════════════════════════
   Fixed, and the only fixed thing on the page. It used to be absolute at the
   top of the document, which meant it left with the first screen; now that the
   first screen is a pin that inverts from white to black under the reader, the
   mark has to survive that inversion in place — and being the one element that
   never moves is what the rest of the hero's movement is measured against. A
   page where everything travels has no scale.

   The mark is larger than it was (27px → up to 34px): at the old size it was
   competing with a 48px headline for the top-left corner and losing, and it is
   now the only thing in the corner rather than one of two.

   The nav is a plain row here, in the resting layer, because the resting layer
   is the finished state of every gesture — with scripting off the room never
   goes dark and the nav is simply present, in paper ink. Under .js it now
   fades in early instead (see the motion layer): held for a beat while the
   copy's own entrance settles, then in over the white room, well before the
   room goes dark. The mobile MENU button is part of the same row and stays
   display:none above the mobile breakpoint. */
.masthead {
  position: fixed;
  z-index: var(--z-raised);
  top: 0;
  left: 0;
  right: 0;
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--space-md);
  padding: clamp(1.1rem, 3.4svh, 2rem) var(--gutter);
  /* the bar is a layout box over six sections of scrolling content and must
     never intercept a press meant for any of them; the two things in it opt
     back in */
  pointer-events: none;
}
.masthead > * { pointer-events: auto; }
/* Found while measuring the new grey ground, and it is a resting-layer bug
   that predates this review: fixed is only correct once the ink flip exists
   to carry the mark across the inversion. Without .js the room never goes
   dark, the mark stays --paper-ink and the nav stays --paper-dim — and a bar
   fixed over five dark sections in paper ink measures 1.09:1 and 1.63:1, i.e.
   invisible. Absolute puts it back on the one screen it is legible on and
   lets it leave with that screen, which is what the resting layer is: the
   finished state of the gesture, and the finished state of this gesture is
   the masthead in the white room. */
html:not(.js) .masthead { position: absolute; }
.masthead__mark {
  display: block;
  width: clamp(22px, 2.6vw, 34px);
  /* the resting layer is the white room, so the resting ink is paper ink.
     .js hands this over to the flip curve — see the motion layer. */
  color: var(--paper-ink);
}
.masthead__mark svg { width: 100%; height: auto; fill: currentColor; }
.masthead__nav {
  display: flex;
  gap: clamp(0.9rem, 2.4vw, 2.2rem);
  font-size: var(--text-plate);
  letter-spacing: var(--track-plate);
  /* 6.36:1 on --paper, which is what the white room measures as */
  color: var(--paper-dim);
}
/* HOVER IS INK ONLY, AND THAT IS A CORRECTION. This rule used to open the
   tracking to --track-plate + --track-open on hover, and letter-spacing is a
   layout property: CSS adds it after every character INCLUDING the last, so a
   five-character link grows by 5 × 0.04em and a seven-character one by 7 ×
   0.04em. The row is a right-aligned flex line, so the growth of whichever
   link is under the pointer pushes the whole row left — measured at 1835:
   3.52px → 3.96px of tracking, the first link's x going 1070 → 1068, on every
   one of the three. The client's word for it was ビビュって動く.

   The gesture could not be saved. Reserving the width needs the character
   count of each link baked into the CSS (the growth is n × 0.04em, not a
   constant), which breaks the next time the copy is edited — and what is
   being defended is 0.44px of extra space per gap at 11px, which is under
   the threshold at which a tracking change reads AS a tracking change while
   its sum is a 2px shift that plainly does not. So the tracking is dropped
   and the colour lift is the whole gesture. Nothing in this rule or the .js
   one below it touches a property that participates in layout. */
.masthead__nav a {
  transition: color var(--dur-micro) var(--ease-out), opacity var(--dur-micro) var(--ease-out);
}
.masthead__nav a:hover { color: var(--paper-ink); }

/* The MENU button is the mobile stand-in for the row above (see the mobile
   block further down) — same corner, same voice, hidden entirely above the
   mobile breakpoint so it never competes with the three-link row. */
.masthead__menu {
  display: none;
  background: none;
  border: 0;
  padding: 0;
  font: inherit;
  font-size: var(--text-plate);
  letter-spacing: var(--track-plate);
  text-transform: uppercase;
  color: var(--paper-dim);
  cursor: pointer;
}

/* ═══ 1 · hero ══════════════════════════════════════════════════════════
   Full bleed on all four sides. There is no frame, no plate and no ground
   colour showing round the photograph, which is why every one of the old
   hero's crop-window numbers is gone from the tokens: nothing here needs to
   know the file's aspect. The band picker beside the <img> serves whichever
   of the six renders is nearest the screen's own shape, and every render
   carries at least 20% of margin round the subject, so a dead-centre cover
   crop cannot cut it at any aspect.

   This section is the resting state — the white room, lit, with its copy set
   in paper ink at the foot. That is what a reader with scripting off or
   prefers-reduced-motion on gets, and it is complete: the room simply never
   goes out. Everything that darkens it is in the motion layer. */
.hero {
  position: relative;
  height: 100svh;
  min-height: 34rem;
  overflow: clip;
  /* the room's own tone, sampled off the renders. It is what shows for the
     one frame before the photograph decodes, so it is the photograph's
     colour and not the page's. */
  background: #f4f1ee;
}
.hero__media, .hero__img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
.hero__img {
  object-fit: cover;
  object-position: 50% 50%;
}
/* Only in the resting layer. With the front of darkness switched off there is
   nothing to blend the white room's bottom edge into the black page below it,
   and a hard white-to-black seam across the middle of a scroll reads as a
   broken image rather than as a section boundary. Under .js the front does
   that job properly and this fade would only wash out the foot of the room
   before the front got there, so it is scoped out. */
html:not(.js) .hero__media {
  -webkit-mask-image: linear-gradient(to bottom, #000 0 86%, transparent 100%);
          mask-image: linear-gradient(to bottom, #000 0 86%, transparent 100%);
}

/* ── the front of darkness ──────────────────────────────────────────────
   One flat layer, one static gradient, moved by transform and nothing else.
   The client's note on the old fade was that darkening the screen evenly
   read as a dip to black rather than as anything happening, so what travels
   here is an edge: a warm leading edge with the dark ramping in behind it,
   rising from below the foot of the screen to above its head.

   ── what changed at this review, and why ────────────────────────────────
   The client's note was that the boundary was monotonous, and it was: it was
   a dead-level horizontal line with 32px of perfectly even gradient behind
   it, which is the shape of a CSS gradient and not the shape of a room going
   dark. Four things were wrong with it and all four are answered by geometry
   rather than by another layer of gradient:

   1 · IT WAS LEVEL. The photograph is lit from the upper right — its own
       shadows run down and to the left, measured off the renders — so the
       corner of the room that loses the light first is the lower left. A
       front that arrives dead level contradicts the picture it is crossing.
       The whole front is now cut from one large radial gradient whose centre
       is up and to the right of the frame, at 62% / −16%, so its contours
       bow: the boundary is highest at the left edge, lowest at 62% across,
       and comes back up a little at the right. Measured at 1280×800 the left
       edge leads the middle by 69px and the right edge by 25px — about 3.1°
       at the left, which is the "a few degrees" end of the brief, arrived at
       as an arc rather than as a rotation.

   2 · IT WAS UNIFORM ACROSS THE WIDTH. It is not any more, and not because
       anything is varied by hand: the ramp is a constant thickness measured
       perpendicular to the contour, and the contour is slanted at the edges
       and level in the middle, so its *vertical* thickness comes out 13.8%
       greater at the left edge than at 62%. The dark falls a little softer
       where it is running downhill. That is the same one decision paying
       twice, which is why it does not read as an effect.

   3 · THE TAPE DID NOT SHOW. The old comment claimed the tape's column was
       the last thing covered and nothing in the picture said so. It is now
       a hole in this layer's own mask, an ellipse standing on the measured
       --tape-l column: 8% of the screen wide, and the point where the mask
       is half open sits 5.8% of the box below the boundary, so the front
       arrives there 9.0% of a screen — 72px at 800 — after it has arrived
       either side. Because it is a mask on this element it travels with the
       front and it takes the warm edge and the grain with it, which is what
       makes it read as the front being held back rather than as a hole.

   4 · THE EDGE WAS CLEAN. A gradient with no noise in it is the one thing
       that always looks like a gradient. ::before carries the site's own
       grain tile — the same turbulence the light site runs page-wide, the
       same 128px, no request — masked to a band that follows the boundary,
       so the grain exists only where the edge is and nowhere else.

   ── the geometry ────────────────────────────────────────────────────────
   Declared once, here, and read by main.js so the ink flip below can be
   solved against it rather than guessed at. The box's top runs from +1.0
   screen heights (its head exactly on the foot of the screen) to
   1 − --dk-travel, so its foot lands exactly on the foot of the screen at the
   end regardless of what --dk-travel is; and the front's midpoint sits at a
   fixed fraction M of the box, which makes

     front(dk) = --dk-lead − --dk-travel · dk        (screen fractions from
                                                     the top of the hero, at
                                                     the reference column)

     --dk-travel  1.55   the box is 155% of the screen tall
     M            0.30   the front sits 30% down it, at the reference column
     --dk-lead    1.465  = 1 + 0.30 × 1.55. At --dk 0 the front is 46.5% of a
                         screen *below* the foot, which is the whole point of
                         M: the box's transparent head is the runway. Without
                         it the front reaches the copy — which sits at 86% of
                         the screen — within the first 12% of the pin, and the
                         white room the client approved is gone in one notch
                         of the wheel. With it, the room holds for the first
                         30% and the crossing is the middle half.
     --dk-span    0.78   the share of the pin the crossing takes. The rest is
                         the landing: the afterimage coming up and the nav
                         rising, on a screen that is already out.

   The reference column is the screen's own centre and not the gradient's:
   the bow is measured relative to x=50%, so --dk-lead keeps its meaning and
   every number above is unchanged from the build the client approved. The
   bow is added on top of it, per element, by --dk-bow below.

   The stops below are fractions of the gradient's ending radius, not of the
   box. At the reference column 1% of radius is 0.62% of the box, so the dark
   ramps over 4.20% of radius = 2.60% of the box = 4.0% of the screen, which
   is the same 32px at 1280×800 the client saw. */
.hero__veil {
  position: absolute;
  /* the ±1px is the same sub-pixel guard the shutter blades use: a layer
     whose transform is rewritten every frame will find a half-pixel gap at
     its own left and right edges */
  left: -1px;
  right: -1px;
  top: 0;
  height: 155%;
  z-index: 1;
  pointer-events: none;
  background-image: radial-gradient(var(--bow) at var(--bow-at),
    transparent           0 72.09%,
    rgb(10 10 10 / 0.72)    74.19%,
    var(--void)             76.29%,
    var(--void)             100%);
  /* the tape's column, held back. One layer, and the reason it can be one
     layer is that a radial-gradient's last stop extends outward for ever —
     so "transparent inside the ellipse, opaque everywhere else" is a single
     gradient and needs no compositing. */
  -webkit-mask-image: radial-gradient(8% 11% at var(--tape-l) 28%, transparent 0 42%, #000 100%);
          mask-image: radial-gradient(8% 11% at var(--tape-l) 28%, transparent 0 42%, #000 100%);
  /* resting layer: parked entirely below the screen. It only ever moves
     under .js, and the resting page is the lit room. */
  transform: translate3d(0, 100%, 0);
}
/* the warm leading edge. It is cut from the same radial as the dark, so it
   bows with the front instead of ruling a level line across the picture, it
   peaks at 0.30 alpha rather than the 0.92 hairline this replaced, and it is
   29px of soft band instead of two pixels of edge. It sits a touch ahead of
   the dark (peak at 71.9% against the dark's 72.09% start) so it is the
   first thing to cross a given row.

   ── the third review, and the note was the same note ────────────────────
   Twice now: "この線が主張しすぎている" and then "水平線も変わってない".
   Lowering the alpha from 0.92 to 0.30 did not answer it, and measuring the
   render says why. Tracked per column — the front is bowed by up to 67px
   across the width so it cannot be read off one row — the warm edge held
   84–87% of the screen's width at better than half its peak strength all the
   way through the crossing, and it was WEAKEST at x≈50%:

     --dk    span at ≥½ peak     warmth (r−b) at 25% / 50% / 75%
     0.40         85.0%                62  /  39  /  62
     0.55         84.4%                60  /  39  /  60
     0.80         86.9%                62  /   9  /  60

   x≈49% is --tape-l. The dip is this layer's own parent mask: the tape's
   column is a hole in .hero__veil and ::after is inside it, so the one place
   the light actually comes from was the one place the light was cut out,
   and the remaining 85% of the width was at full strength. That is not a
   quieter line, it is an unmotivated one — which is exactly what "主張しすぎ"
   describes. Alpha was never the problem; the horizontal extent was.

   So the mask is no longer a pair of end-fades on a band that crosses the
   picture. It is an ellipse standing on --tape-l: the warm edge now exists
   only where the room's light is coming from, and is gone before it reaches
   either screen edge. The parent's tape hole is kept, and against a peak
   that now sits ON that column it stops reading as a bite out of a bar and
   starts reading as what it always was — the front arriving late around the
   object, with the light wrapping the hole. Thickness is not varied by hand
   either: a soft horizontal falloff on a band of constant perpendicular
   thickness thins visibly as it dies, which is the same one decision paying
   twice.

   Radii: 38% of the width is ±486px at 1280 and ±143 at 375, and the stops
   put half strength at ±16% of the width — 205px and 60px. Vertically 120%
   so the ellipse never clips the band itself at any bow. */
.hero__veil::after {
  content: "";
  position: absolute;
  inset: 0;
  background-image: radial-gradient(var(--bow) at var(--bow-at),
    rgb(var(--dk-edge) / 0)    0 70.2%,
    rgb(var(--dk-edge) / 0.30)   71.9%,
    rgb(var(--dk-edge) / 0)      73.8%);
  -webkit-mask-image: radial-gradient(38% 120% at var(--tape-l) 50%,
    #000 0%, rgb(0 0 0 / 0.62) 34%, rgb(0 0 0 / 0.18) 68%, transparent 100%);
          mask-image: radial-gradient(38% 120% at var(--tape-l) 50%,
    #000 0%, rgb(0 0 0 / 0.62) 34%, rgb(0 0 0 / 0.18) 68%, transparent 100%);
}
/* the grain, and only where the edge is. The tile is the light site's own —
   turbulence, desaturated, contrast-stretched — with one change: alpha
   carries the noise instead of being forced flat, so the layer is a warm
   grey speckle at varying alpha rather than a flat grey haze. That matters
   because the boundary has white on one side of it and black on the other
   and the grain has to break up both: a warm grey darkens the lit half and
   lightens the dark half, from one layer, with no blend mode (this element
   has a transform and is therefore its own stacking context, so a
   mix-blend-mode here would blend against the veil and not against the
   photograph — which is the bug that makes film-grain overlays quietly do
   nothing).
   The mask is the same radial again, opened into a band 12.5% of radius
   wide — about 96px at 1280×800 — centred on the boundary, so the grain
   travels with the front and there is none of it anywhere else. */
.hero__veil::before {
  content: "";
  position: absolute;
  inset: 0;
  opacity: 0.62;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='128' height='128'%3E%3Cfilter id='g' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0.56 0 0 0 0 0.53 0 0 0 0 0.47 1 0 0 0 0'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='linear' slope='1.5' intercept='-0.45'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='128' height='128' filter='url(%23g)'/%3E%3C/svg%3E");
  background-size: 128px 128px;
  -webkit-mask-image: radial-gradient(var(--bow) at var(--bow-at),
    transparent 0 68.6%, #000 72.4%, #000 76.0%, transparent 81.1%);
          mask-image: radial-gradient(var(--bow) at var(--bow-at),
    transparent 0 68.6%, #000 72.4%, #000 76.0%, transparent 81.1%);
}
.hero {
  /* --amber is the page's own light, and the warm edge is the last of the
     room's light being pushed out ahead of the dark: the same idea, and the
     same colour, as the lit edge on the shutter blades in section 5. */
  --dk-edge: 255 181 74;
  --dk-travel: 1.55;
  --dk-lead: 1.465;
  --dk-span: 0.78;
  /* The one shape all three layers of the front are cut from. Written once
     and referenced three times so the dark, the warm edge and the grain
     cannot come apart; if the bow is ever retuned it is retuned here.
     175% × 62% of the box, centred at 62% / −16% of it. */
  --bow:    175% 62%;
  --bow-at: 62% -16%;
  /* the bow, restated as a number main.js can solve the ink flip against.
     The front's height at horizontal position u (0 at the left edge, 1 at
     the right), relative to the reference column at u = 0.5, is

       lead(u) = --dk-bow · ((u − 0.62)² − 0.0144)   screen fractions, and
                                                     positive means earlier

     which is the ellipse's own geometry to three decimal places over the
     whole width — checked against the exact solution at u = 0, 0.18, 0.5,
     0.62 and 1.0, worst case 4% of the offset. It matters because the ink
     flip is centred on the instant the front reaches an element, and the
     copy sits in the bottom-left corner where the front now arrives 0.039
     of a screen early: without this the flip would land two thirds of a
     swing late, on the one part of the screen the bow makes the front
     earliest at. */
  --dk-bow: 0.2254;
  /* how steep the ink flip is, in units of the pin's own progress. The dark
     ramp is 0.0131 of the pin wide in these units; 150 puts the flip's whole
     10%→90% swing inside 0.029 of it — about 2.2× the ramp. Faster than that
     and the flip is a visible snap on a ground that has barely started to
     move; slower and the ink is still mid-way when the ground has finished,
     which is the stretch of illegible copy this curve exists to avoid. */
  --flip-k: 150;
}

/* ── what is left where the tape was ─────────────────────────────────────
   The tape's column is the last thing the front covers, because the front
   arrives at the top edge last and the tape leaves through the top edge.
   What is left there is a warm afterimage, and it is the join to the
   constellation: that figure's own origin point is just off top-centre and
   it branches downward from there, so the page's first star lights inside
   the glow the tape left. Centred on --tape-l, the measured column, and
   given enough spread to cover the few per cent between that column and the
   figure's origin at every screen size.

   Sized and placed off the figure it has to hand over to, measured rather
   than eyeballed: the constellation's origin star sits at 49%/10% of a box
   that is centred at 44%/50% of the screen, which comes out at (556,179) on
   1280×800, (836,178) on 1920×1080 and (184,187) on 375×812. The glow's own
   visible footprint — the gradient is spent by 72% of its radius — has to
   contain all three, which is why it is this wide and hung this low rather
   than sitting tight on the top edge. The horizontal offset between the tape
   column and the figure's origin is 5.9% of the screen at 1280 and 5.8% at
   1920; the glow is wide enough to cover it, and reporting it is honest —
   the figure's own x is not adjustable without moving a composition this
   brief says not to touch. */
.hero__after {
  position: absolute;
  z-index: 2;
  top: 0;
  left: var(--tape-l);
  /* taller than it is wide, and an ellipse rather than a circle. A circular
     bloom at the top of a black screen reads as a lens artefact; what was
     there was a vertical thread of light leaving through the top edge, so its
     residue is taller than it is wide. Still wide enough to contain the
     figure's origin — the numbers are in the comment above. */
  width: clamp(15rem, 36svh, 25rem);
  height: clamp(19rem, 46svh, 32rem);
  translate: -50% -34%;
  /* the tail runs to 94% rather than 78%, which does not change the core at
     all and is the difference between the figure's origin star landing inside
     this glow and landing just outside it. At 1280×800 that star sits at
     normalised radius 0.786 of this gradient — measured — so a 78% stop put
     it on flat ground by 6 thousandths of a radius. The core is unchanged;
     what is extended is the part that is nearly nothing anyway, which is also
     the part that has to reach. */
  background: radial-gradient(ellipse 50% 56% at 50% 50%,
    rgb(255 226 178 / 0.22) 0%,
    rgb(255 205 130 / 0.085) 40%,
    rgb(255 205 130 / 0.03) 66%,
    transparent 94%);
  opacity: 0;
  pointer-events: none;
}

/* ── the copy ───────────────────────────────────────────────────────────
   Bottom left, three sizes and nothing between them: the scale's hairlines,
   the kicker at 10–11px, the catch at 32–48px. The mark in the masthead is
   the fourth and it is up in the corner, so no two of them are ever adjacent
   at similar size. */
.hero__copy {
  position: absolute;
  z-index: 3;
  left: var(--gutter);
  right: var(--gutter);
  bottom: clamp(1.75rem, 7svh, 3.5rem);
}
/* four rules at four lengths and, under .js, four speeds. They are the near
   end of the same idea the constellation finishes with — that figure carries
   five register ticks along its foot at 0/25/50/75/100 instead of a ruled
   grid, and these are the same marks stated first, as horizontals under the
   words rather than verticals under a chart. */
.hero__scale {
  display: block;
  margin-bottom: clamp(0.9rem, 2.6svh, 1.5rem);
  color: var(--paper-dim);
}
.hero__scale i {
  display: block;
  width: var(--w);
  max-width: 26rem;
  height: 1px;
  margin-bottom: 0.42rem;
  background: currentColor;
  opacity: 0.5;
  transform-origin: 0 50%;
}
.hero__scale i:last-child { margin-bottom: 0; }
/* four ticks read as "another leftover line" once the room around them was
   quieted elsewhere (tape, SCROLL rail, dark front, grain, afterimage all
   already on this screen) — cut to the two extremes, longest + shortest, so
   the mark reads as a measuring edge's ends rather than a ruled quarter-scale.
   The middle two <i> stay in the markup (display: none only) so the ladder
   can be restored by deleting this block. Gap widened to read as two marks,
   not a doubled underline, now that only one gap remains instead of three;
   0.5/0.62 keeps the same mobile:desktop ratio the original 0.34/0.42 had. */
.hero__scale i:nth-child(2),
.hero__scale i:nth-child(3) { display: none; }
.hero__scale i:nth-child(1) { margin-bottom: 0.62rem; }
.js .hero__scale i:nth-child(4) {
  /* was --dl: .58s (the 4th-of-4 stagger); reusing the original 2nd-of-4
     delay since this tick is now the second visible mark, not the fourth */
  animation-delay: 0.26s;
}
.hero__kicker {
  margin: 0 0 0.7em;
  font-size: var(--text-kicker);
  font-weight: 300;
  line-height: 1.6;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: var(--paper-ink);
}
/* Zen Kaku at 500 over 300, not the page's Cormorant: this is the setting the
   client saw and approved on the white-room exploration, and the two lines are
   a statement rather than a display headline — Cormorant 300 at this size
   reads as a title card, which is the register the old WE CONNECT was in and
   the one that was replaced. */
/* 0.82 of --text-hero, and the gap between the two lines opened. The copy is
   「WE MAKE CONNECTIONS WORK. / つながりを、機能させる。」 as of 2026-08-01, which
   is twice the length of the line it replaced — at the full size it filled the
   lower left corner and read as a headline crowding its own frame rather than as
   a statement placed in a room. Smaller and further apart, the two lines read as
   two statements instead of one block.

   Scaled here rather than in --text-hero because that token also carries the
   mobile floor at style.css:607, which exists to stop the English wrapping to
   three lines on a phone. */
.hero__catch {
  font-weight: 500;
  font-size: calc(var(--text-hero) * 0.82);
  line-height: 1.2;
  letter-spacing: 0.005em;
  color: var(--paper-ink);
}
.hero__catch-en,
.hero__catch-jp { display: block; }
/* the two lines are one sentence in two languages, not a heading and a
   subheading, so the space between them is wider than the leading inside either */
.hero__catch-jp { font-weight: 300; margin-top: 0.34em; }

/* ── the scroll indicator ────────────────────────────────────────────────
   Bottom right, opposite the copy, and set to be read down the page rather
   than across it: the word is rotated a quarter turn so it points the way it
   is asking the reader to go, and the rule under it is the thing the eye
   actually follows. 10px, --paper-dim (6.36:1 on the room), tracked wide
   enough that at this size it reads as a label and not as a word.

   It is deliberately on the opposite corner from .hero__scale, which is the
   other set of hairlines on this screen: the scale is horizontal, at the
   foot of the copy, and this is vertical, at the outer edge. Two marks of the
   same weight in the same corner would have read as one broken thing.

   The animation is in the motion layer, and so is the gate that stops it. */
.hero__scroll {
  position: absolute;
  z-index: 3;
  right: var(--gutter);
  bottom: clamp(1.75rem, 7svh, 3.5rem);
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0.85rem;
  color: var(--paper-dim);
}
.hero__scroll-t {
  writing-mode: vertical-rl;
  font-size: 0.625rem;
  line-height: 1;
  letter-spacing: 0.42em;
  /* vertical-rl puts the tracking after the last glyph too, which hangs the
     column 0.42em low against the rule below it */
  text-indent: 0;
  padding-bottom: 0.42em;
  margin-bottom: -0.42em;
  text-transform: uppercase;
}
.hero__scroll-l {
  position: relative;
  display: block;
  width: 1px;
  height: clamp(2.4rem, 6svh, 3.6rem);
  overflow: clip;
  /* the rail is always there and always still; only the segment inside it
     travels, and only while the reader has not yet scrolled.

     This used to be `opacity: 0.34` on the element itself, which sits above
     the ::after in the same stacking context: opacity multiplies through to
     children, so the travelling segment's own colour was silently cut to
     0.34 x currentColor on top of this same 0.34 x currentColor rail —
     both layers landed on the same painted value and the light became
     unreadable against the rail it was supposed to stand out from (measured:
     rgb(181,175,169) end to end, no luminance step anywhere in the column).
     Moving the dimming into the background's own alpha keeps the rail dim
     without touching what paints on top of it. */
  background: color-mix(in srgb, currentColor 34%, transparent);
}

@media (max-width: 30rem) {
  .hero__scroll { gap: 0.6rem; }
  .hero__scroll-t { letter-spacing: 0.3em; }
}

@media (max-width: 30rem) {
  .hero__kicker { letter-spacing: 0.16em; }
  .hero__scale i { margin-bottom: 0.34rem; }
  .hero__scale i:nth-child(1) { margin-bottom: 0.5rem; }
}
/* --text-hero's 2rem floor is a floor for a reason on the English line, and
   too big for the Japanese one below about 400px: measured at 375, その日を、
   成立させる。 sets 352px wide against a 337px measure and breaks after させ,
   leaving る。 alone on a third line. Both lines drop together rather than the
   Japanese one alone — they are one statement at one size, which is the
   setting the client approved, and two sizes here would read as a hierarchy
   that is not there. */
@media (max-width: 26rem) {
  .hero { --text-hero: clamp(1.5rem, 7.4vw, 2rem); }
}

/* ═══ 2 · connection ════════════════════════════════════════════════════
   Nine points and nine lines. The stars, the words and the SVG all live in
   one box and are placed in the same 0–100 space, so the two coordinate
   systems cannot drift.

   The box is square, and that is a decision the drawing forces rather than a
   preference. A line is drawn here by paying out stroke-dashoffset against
   pathLength="1", which is the only way to normalise nine lines of nine
   different lengths to one 0→1 progress. That normalisation is done in user
   units — so it is silently discarded by vector-effect: non-scaling-stroke,
   which moves the dash pattern into screen space and turns every line into a
   1px dotted rule. Without the vector-effect, a stretched viewBox
   (preserveAspectRatio="none") scales the stroke anisotropically and each
   line comes out a different thickness — measured at 7.1× to 10.9× on one
   1280×800 screen, and inverted on a phone.

   A square viewBox in a square box has neither problem: the scale is
   isotropic, the stroke is uniform, and pathLength does what it says. What
   it costs is that the figure no longer fills a wide screen edge to edge —
   which in a room this dark is not a cost. */
/* Every child of this section is positioned — the figure, the statement and
   the plate label are all absolute, because they have to share one 0–100
   coordinate space. So the section has nothing in flow to give it a height,
   and without .js (where .pin supplies one) it collapses to zero and the
   constellation is drawn over the hero and over ABOUT. The height belongs
   here, in the static layer, and the pin only repeats it. */
.connect {
  position: relative;
  min-height: 100svh;
  /* The same clip .js .pin already puts on this element, moved down into the
     resting layer so both layers behave the same. Under .js nothing here
     changes at all. Without it, the three Japanese hub labels — placed off
     fixed percentages, so at 375px 経験と可能性。 lands 149px past the right
     edge — contributed to the document's scrollable width: measured 478/567/
     914px against 320/375/768 viewports under prefers-reduced-motion, on this
     build and on the one before it alike. The viewport's own propagated
     overflow-x was already clipping them visually and already blocking the
     user from panning to them, so this is a containment fix and not a visual
     change — the labels are cut at exactly the same edge either way. That they
     are cut at all on a small screen is a real problem, but it belongs to this
     section's own placement rather than to a clip, and it is in the report
     rather than fixed here. */
  overflow: clip;
}
/* The box is bigger than it was — 704px against 624 at 1280×800, 1000 against
   904 at 1920 — and moved to the middle of the screen rather than 44% of it.
   Both follow from the field: the drawing is no longer the whole figure, it
   is the bright part of a field that runs off every edge, so there is no
   longer a right-hand margin to keep clear for the vertical statement (the
   texture is meant to run under and past it) and the square can take more of
   the screen. The floor it is held off is the plate and legend row, which is
   why the svh term is 6rem and not 0. */
/* THE CAMERA holds the box now, and the two layers inside it fill it. They were
   siblings with identical geometry before, which worked only for as long as
   nothing had to move them together. Now one thing does: the retreat. Putting
   the box here means the 0–100 space, the origin, and the retreat are all
   stated once, and nothing inside has to know about any of them. */
/* ONE FIGURE, ONE SET OF METRICS, AT EVERY WIDTH — and this is the change that
   made the generated solution provable rather than only checked at 1440.
   The generator solves ninety-seven phrase rectangles against each other in the
   figure's own 0-100 space, off two measured constants: 4.538 units per
   character and 5.47 of line box. Those constants are only constants if the type
   is a fixed fraction of the box. It was not: --text-node clamped to 1.9rem in
   absolute pixels while the box ranged 254px to 984px, so one line of Japanese
   was 5.47 units wide at 1440, 6.25 at 1280, 4.47 at 1920 and 9.43 at 320 —
   and at 320 and 375 the same solution came back with 45 and 33 real ink
   collisions. Measured, both of them, before this.
   So the box is a variable now and the type is a fraction of it. 0.0378 is
   30.4px / 804px, which is exactly what 1440 was rendering and what the two
   constants were measured on. Every breakpoint now renders 5.47. */
.cam {
  position: absolute;
  top: 50%;
  left: 48%;
  translate: -50% -50%;
  height: var(--box);
  aspect-ratio: 1;
  /* three layers, stated here so none of them is left on auto: the network,
     then the scrim that clears it off the copy, then the copy. Leaving the
     copy on auto put it under the scrim the moment the scrim came up from 0. */
  z-index: 0;
}
.field-label,
.constel__legend { z-index: 2; }
.constel,
.field {
  position: absolute;
  inset: 0;
}
/* the same box, registered to the same 0–100 space. There is no ground here:
   .field is not a backdrop for .constel, the two are one network cut in half
   only so the nine named points can carry a rotation the rest does not. */
.field { z-index: 0; }
.field__svg {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}
/* A reveal band. Ninety-seven phrases and a hundred and fourteen edges, grouped
   by distance from the origin into eighteen of these, so eighteen elements read
   --q per frame instead of three hundred. Everything inside a band is the same
   material as the nine named points — .star, .node, .zone, ln--m1/2/3 — because
   it is the same figure continued and not a second layer behind it. */
.field .nb { position: absolute; inset: 0; }
/* same grain as .constel__lines above — this IS that figure continued, so a
   7px dash here against a 2px one there would split one drawing in two. */
.field .nb path { stroke-dasharray: 0.28 0.36; stroke-dashoffset: 0; }
/* --ns is the phrase's own magnitude, written per node by the generator: 1.0 for
   the twelve that define the company, 0.78 and 0.58 for the rings past them.
   Size, not tone — the colour stays --ink everywhere, because a phrase that goes
   grey with distance is scenery and that is the mistake this section keeps
   making. The camera is what makes the far ones small; this only sets how big
   they were to begin with. */
/* .node.nw-node and not .nw-node: .node sets font-size further down this file at
   the same specificity, so a single class here loses the cascade and every
   phrase renders at full size. It did, and the generator had already reserved
   the smaller boxes for them — which is where the overlapping phrases in the
   first render came from, not from the width estimate. That was right: 30.4px
   with 9.728px of tracking is 40.1px per character, 4.99 user units, against
   CH_W = 5.0. */
/* The phrases have to sit in front of the edges, and once there are a hundred of
   them the edges cross a lot of type. The client's note was 「線と文字が重なって
   いるのがちょっとごちゃっとしている」.

   A halo on the glyphs, not a plate behind the box. A rectangle reads as type
   sitting on a panel, which is the cheap version of this; a blurred copy of the
   text's own shapes reads as the line passing behind the words, which is what is
   actually happening. Three stacked shadows rather than one wide one: one wide
   pass at this alpha is a grey cloud with a visible edge, three build density
   toward the glyph and fade out with no boundary of their own.

   Sized in em, so it holds at every --ns without a second set of numbers, and it
   scales with the camera along with the type it belongs to. */
.node,
.zone {
  /* A stroke, not three blurred copies. The blurred version worked and cost too
     much: a hundred phrases each re-rastering three blur passes inside a box
     whose transform changes every frame is the one thing this section cannot
     afford. Measured at 1440x900, three sweeps each:

       blur x3   117/123/125 frames   27/13/8 over 20ms
       none      130/129/127          4/4/3
       blur x1   129/129/128          11/3/2
       stroke .14em  129/129/127      8/4/3
       stroke .09em  130/129/127      4/4/5    <- this

     paint-order puts the stroke under the fill, so the knockout does not eat
     into the letterform the way a centred stroke would; 0.09em is 2.7px on the
     twelve at full size and 1.6px on the sixty smallest. */
  paint-order: stroke fill;
  -webkit-text-stroke: 0.09em var(--void);
}
/* The three hub phrases are absolute too, as of this build. They used to be the
   only three .node in normal flow and the generator carried nine hand-measured
   rectangles (OCCUPIED) to say where their ink had actually landed after the
   flow stacked them. Now their boxes are derived from --x/--y like every other
   phrase's, which is one fewer measured constant to go stale. */
.node.cn-node { position: absolute; }
.node.nw-node {
  /* position, and this is the one that mattered. .node resolves to
     position: relative — the rule further down this file overrides the
     `.zone, .node { position: absolute }` above it — so a .node is a block in
     normal flow that --y then nudges. With three of them the accumulated
     displacement is a couple of units and the original placement absorbs it;
     with ninety-seven it is four and a half units per predecessor, which is
     what was still pushing phrases into each other after the collision maths
     were correct. Measured: ink top ran 4.34, 8.88, 13.41, 17.95 for four
     nodes that should all have sat at the same offset from their own --y. */
  position: absolute;
  font-size: calc(var(--fs-node) * var(--ns, 1));
}
.constel { z-index: 1; }
.constel__lines {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}
/* THE DASH IS FINE NOW, AND IT WAS NEVER MEANT TO BE A DASH AT ALL.
   The comment above says a line is drawn by paying out stroke-dashoffset
   against pathLength="1" — but pathLength is not in the markup. Searched
   case-insensitively across index.html: zero occurrences. So `1` here has
   always been one USER UNIT, not the whole path, and at the camera's own
   scale (measured getScreenCTM 6.978 at --q 0.45, 1440x900) that is a 7px
   dash on a 7px gap. Every line in the figure has been broken that way,
   not only the register named ln-dash, and the draw never drew: offsetting
   by one unit against a one-unit period just slides the pattern along by
   one dash.

   The client asked for the texture to stay and the grain to come down
   (「この残ってのいいねんけど…破線の部分が大きいな」), so the period is stated
   directly instead of being an accident of a missing attribute: 0.28 on
   0.36 is 1.95px on 2.51px at that same scale, and 0.75 on 0.96 when the
   camera has pulled back. Fine enough to read as a line with a grain in
   it rather than as a row of ticks.

   Restoring pathLength would make the lines solid AND make the draw work,
   which is the figure as designed — but that is a change to the section's
   motion, not to its grain, and it is not going in the week of the show. */
.constel__lines path {
  fill: none;
  stroke-dasharray: 0.28 0.36;
  stroke-dashoffset: 0;
}
/* Two passes over the same nine paths. The wide one is the glow — a blur
   here would cost a filter pass on every frame of the turn, and a wide
   translucent stroke is indistinguishable from one at this opacity.
   0.17 user units is 1.06px on the 624px box a 1280×800 screen gives it,
   and 0.7px on the 410px box at 768. Below 48rem the box is small enough
   that the same number would be half a pixel, so it is raised there.
   Colour now comes from each path's own linearGradient (stroke="url(#lf…)"
   in the HTML) rather than a flat stroke here — that gradient is what fades
   a line's own ends to nothing, so this layer only sets width and the
   overall envelope opacity, and both are keyed to the magnitude of the star
   the line arrives at: a line landing on a 1st-mag hub (.ln--m1) runs
   thick and bright, one landing on a 3rd-mag leaf (.ln--m3) thin and dim.
   .ln--m2 keeps the figure's previous baseline numbers unchanged. */
/* Edge weights dropped about a quarter at this pass. The client's note was that
   lines crossing the type read as clutter, and the glyph halo above only answers
   half of it: at 0.32em of tracking most of a phrase's box is the gap between
   characters, where nothing is knocked out and the line shows through at full
   strength. Measured inside the phrases' own ink boxes at --q 0.6, 1440x900:
   1.04% amber before, 0.89% with the halo alone, and the rest of the distance is
   here. The three magnitudes keep their ratios — this is one multiplier, not a
   retune, so the figure still reads the way it was approved. */
/* fill: none, and it is not defensive. SVG fills an OPEN path by closing it with
   the chord from its last point to its first, and until this build every edge in
   the field WAS that chord — a straight M/L, zero area, nothing to fill. Curved,
   the same paths enclose real area and the default fill is black, so the first
   render of the curves came back with big dark facets lying across the picture:
   the section's own oldest failure mode, 「最初から背景が出てます」, reintroduced
   as a literal black background by a one-word default. .constel__lines has
   carried this all along; the field never needed it before. */
.ln-glow path,
.ln-core path { fill: none; }
.ln-glow path { stroke-width: 0.85; opacity: 0.065; }
/* SCALING, LIKE EVERYTHING ELSE IN THE FIGURE. `--cam` (transform:
   scale(var(--cam))) takes the whole figure from ~8.05x viewBox-to-viewport at
   rest down to ~0.28x of that by the end, so a stroke-width stated in the
   figure's own user units THINS as the camera pulls back — 0.17 renders at
   about 0.82px early and 0.28px late at 1440×900. That is the same figure
   .ln-glow below has always been drawn in, and the client's own note on the
   reference was that this thinning-with-distance read as intentional, not as
   a bug.
   vector-effect: non-scaling-stroke was tried here to hold the on-screen width
   constant and reverted: this register is drawn with pathLength="1" and
   stroke-dasharray/-dashoffset to pay it out, and non-scaling-stroke moves
   that dasharray into screen-pixel space too — a `1` meant as "one dash the
   length of the whole path" is read as a literal 1px on/off pattern instead,
   which turned the line into a dotted rule. See the quiet register below,
   which has no dasharray of its own and inherited exactly that. */
.ln-core path {
  stroke-width: 0.17;
  opacity: 0.46;
}
/* ── the second register ──────────────────────────────────────────────────
   Two kinds of edge, which is what the reference actually shows and what the
   last build did not have: a bright amber curve carrying the structure, and a
   thin, quiet one beside it carrying the lateral connection. The split is NOT
   radial. It is drawn from what an edge MEANS — every cross-link is quiet, and
   a seeded share of the leaf edges inside every band is too — because a split
   by distance is a falloff by distance, and material that thins as it goes out
   is the exact thing three rejected builds did (「背景が広がってるだけや」).
   Both registers are present at every distance, at full colour.

   NO LONGER A DASH. The client asked for a solid line, thin — 「点線、実線に
   してほしい。細く」 — so this register is a hairline now, not a broken one. The
   class stays `ln-dash` (HTML is generated by tools/gen_network.py and
   renaming the class means a regeneration this pass does not run): read it as
   "the quiet register", not literally dashed.
   stroke-dasharray: none is explicit and load-bearing, not a tidy-up: every
   path in .constel__lines inherits stroke-dasharray: 1 / stroke-dashoffset: 0
   from further up this file (the main register's own draw-in), and without an
   override here this register inherits it too and renders as a 1px dotted
   line instead of a solid one. stroke-dashoffset and stroke-linecap are left
   out rather than set, since dasharray: none makes them no-ops.

   0.105 and not the main register's 0.17: the two must stay visibly distinct
   at every point on the camera's pull-back. Both scale with --cam, so the
   ratio between them holds throughout rather than only at rest. No glow pass
   on this register, by design: it is the quiet one. */
.ln-core path.ln-dash {
  stroke-width: 0.105;
  stroke-dasharray: none;
  opacity: 0.42;
}
.ln-core path.ln-dash.ln--m1 { stroke-width: 0.125; opacity: 0.5; }
.ln-core path.ln-dash.ln--m3 { stroke-width: 0.09;   opacity: 0.34; }
/* the same lead/speed logic as .star--m1/m3 above, applied to the line's own
   draw: a line landing on a hub is drawn a touch sooner and a touch faster
   than the baseline, one landing on a leaf a touch later and slower */
.ln--m1 { --mag-speed-ln: 13; --mag-lead-ln: -0.01; }
.ln--m3 { --mag-speed-ln: 7; --mag-lead-ln: 0.01; }
.ln-glow path.ln--m1 { stroke-width: 1.15; opacity: 0.10; }
.ln-core path.ln--m1 { stroke-width: 0.24; opacity: 0.60; }
.ln-glow path.ln--m3 { stroke-width: 0.6; opacity: 0.045; }
.ln-core path.ln--m3 { stroke-width: 0.11; opacity: 0.32; }


/* ── the texture ─────────────────────────────────────────────────────────
   Not a background. It is the same reveal as the drawing above, continued:
   the nine-point figure lands, and the movement that drew it keeps going
   outward — next points, next lines, next words — past every edge of the
   screen. The client's words are the spec, and they rule out the reading this
   layer had in its first build:

     「今広がっていくやつをどんどん広げるっていう話やのに、
       最初から背景が出てます。全く意図したものではありません」

   So there is no "始めから" here at all: at --q 0 every band below is at
   opacity 0 by its own schedule, not by a container fade, and the first of
   them does not start until --q 0.30 — by which point the figure is 77%
   drawn. See the reveal in the motion layer.

     before    9 stars ·  9 lines ·  6 words · x 21–86% of a 624px box
     then    134 stars · 44 lines · 20 words · x −42…157
     now     260 stars · 90 lines · 34 words · x −92…191  y −95…195

   The extents are measured rather than chosen. The box is square and centred,
   so the field has to reach:
     1280×800   box 704px   x −38…166   y −11…111
     1920×1080  box 984px   x −44…152   y  −5…105
     2560×1080  box 984px   x −75…185   y  −5…105   ← the widest we serve
     375×812    box 343px   x  −5…105   y −45…192   ← the tallest
   and it is generated over x −92…192, y −95…196, which contains all four with
   margin at every edge. Nothing here is placed at a screen percentage: it is
   all in the figure's own 0–100 space, registered to the drawing exactly.

   HOW IT IS BUILT, AND WHY IT IS NOT 384 ELEMENTS DOING ARITHMETIC. One
   element per shape, but the *reveal* is one calc per band: sixteen <g>s hold
   the stars and lines together, sixteen <div>s hold the words, and it is the
   band that reads --q. Thirty-two elements do the work of 384, and the shapes
   inside them are static SVG the rasteriser treats as one path list. The
   markup is generated — site-dark/tools/gen_field.py, seed 20260731 — because
   the ordering is the behaviour and no hand edit can keep 384 shapes sorted.

   WEIGHT IS DISTANCE, and that is what replaced the m4/m5/m6 tiers. A fixed
   set of texture magnitudes is what made this read as a separate, darker layer
   behind the figure: there was a step at the boundary. Now --tone falls with
   the band index — 0.72 at the innermost band against the drawing's own
   0.62–0.8 line cores, 0.22 at the outermost — and so do the stroke width and
   the radii the generator writes into the markup. There is no boundary to see:
   one movement, thinning as it goes out. The three per-shape tiers below are
   local variation inside a band, so a band is a band and not a ring. */
.tb {
  --tone: calc(0.72 - var(--i) * 0.02);
  opacity: var(--tone);
  /* on the group, so it is one declaration per band rather than per path;
     circles carry no stroke, so nothing else sees it */
  stroke-width: calc(0.135 - var(--i) * 0.005);
}
.tx circle { fill: var(--amber); }
.tx--t2 { opacity: 0.74; }
.tx--t3 { opacity: 0.52; }
/* 0.5 against the star's 1.0, which lands the innermost band's lines at 0.36
   under the drawing's own 0.62 core — near enough that the join does not show,
   far enough that 90 of them do not compete with the nine */
.tx path {
  fill: none;
  stroke: var(--amber);
  opacity: 0.5;
  /* and the same grain again, for the same reason */
  stroke-dasharray: 0.28 0.36;
  stroke-dashoffset: 0;
}

/* the outer words. Set on the point rather than beside it — the six named
   satellites are labels and are offset off their star, these are not labels
   of anything and sit in the field the way the stars do. Two tones, both
   over AA on --void: --dim at 6.29:1 for the seventeen on the inner eight
   bands, which carry the same weight as the named satellites, --faint at
   5.03:1 for the seventeen on the outer eight. */
.txw { position: absolute; inset: 0; }
.txw__w {
  position: absolute;
  left: var(--x);
  top: var(--y);
  translate: -50% -50%;
  /* the words' own falloff outward, and it is size and not tone on purpose:
     the dots and lines thin by opacity, but a word dimmed the same way would
     drop under the page's contrast floor, and these still have to clear AA
     even though they are aria-hidden decoration. 1.0 → 0.80 across the
     sixteen bands, so the outermost words read as far off without being any
     fainter than --faint's own 5.03:1. */
  font-size: calc(var(--text-txw) * (1 - var(--i, 0) * 0.0133));
  font-weight: 300;
  line-height: 1;
  letter-spacing: 0.34em;
  white-space: nowrap;
}
.txw__w--a { color: var(--dim); }
.txw__w--b { color: var(--faint); }

/* Three magnitudes. Size, core opacity, glow spread and the tightness of the
   inner core all move together per tier — a star that is only bigger reads
   as a bigger dot, not a brighter one. m1 is the three Japanese hubs; m2/m3
   split the six English satellites by how many lines actually meet there
   (SPACE / FLOW / FORM pass one through and are m2; PEOPLE / IDEAS / TIME
   just end one and are the dimmest, m3). */
.star {
  position: absolute;
  left: var(--x);
  top: var(--y);
  border-radius: 50%;
}
/* --mag-lead/--mag-speed are read only under .js, in the reveal formulas
   below — a bright hub snaps to full strength fast and a touch ahead of the
   line that reaches it (decisive, the way the eye finds a bright star first);
   a dim leaf star eases in slowly and a touch behind (the way a faint one
   takes longer to resolve). This is the "order and speed carry the life"
   requirement standing in for the twinkle the brief rules out. */
.star--m1 {
  --star-op: 1;
  --mag-speed: 12;
  --mag-lead: -0.0075;
  width: 12px;
  height: 12px;
  margin: -6px 0 0 -6px;
  background: radial-gradient(circle, var(--amber-2) 0 16%, rgb(255 181 74 / 0.85) 45%, rgb(255 181 74 / 0) 78%);
}
.star--m2 {
  --star-op: 0.85;
  width: 8px;
  height: 8px;
  margin: -4px 0 0 -4px;
  background: radial-gradient(circle, var(--amber-2) 0 22%, rgb(255 181 74 / 0.55) 46%, rgb(255 181 74 / 0) 70%);
}
.star--m3 {
  --star-op: 0.65;
  --mag-speed: 5.2;
  --mag-lead: 0.01;
  width: 5px;
  height: 5px;
  margin: -2.5px 0 0 -2.5px;
  background: radial-gradient(circle, var(--amber-2) 0 30%, rgb(255 181 74 / 0.4) 42%, rgb(255 181 74 / 0) 58%);
}

.zone, .node {
  position: absolute;
  left: var(--x);
  top: var(--y);
}
/* ── the English, and it is not a label any more ──────────────────────────
   These used to be satellites: one word hung off one star, 0.85rem to its
   right, so the word named the point. The reference names something else with
   them. PEOPLE / INFORMATION / PROCESS / TIME / ROLE / EXPERIENCE / SITE stand
   in the OPEN SPACE BETWEEN nodes, small and grey, one per region — they are
   the names of districts of the network, not captions on its dots.

   Which is why this is centred on its own coordinate (translate -50% -50%)
   rather than offset off one, why the generator solves for a gap instead of
   reading a node's position, and why there is no leader mark of any kind. One
   tone throughout, --dim, the page's own floor at 6.29:1: a region name that
   dimmed with distance would be the falloff this section keeps being rejected
   for. Tracking is opened past --track-plate — a name spread over a district
   reads as belonging to the space, where a tight one reads as a tag. */
.zone {
  translate: -50% -50%;
  font-size: var(--fs-zone);
  font-weight: 300;
  letter-spacing: 0.42em;
  color: var(--dim);
  white-space: nowrap;
}
/* the three Japanese phrases are the hubs — larger, in the JP display face
   (not --font-body's gothic; see --font-jp-display in tokens.css for why),
   in --ink, and set above their point so the lines below them stay clear.
   Letter-spacing opened well past the satellites' own --track-plate — a
   star atlas's captions run loose, and it is JP glyphs' own even, square
   rhythm that this reads best on. */
.node {
  position: relative;
  /* em and not rem, for the same reason the size is a fraction of the box: the
     generator reserves the ink's rectangle in user units, and an offset stated
     in rem is a different number of user units at every width. 0.105em is the
     3.2px this was at 1440 and 0.29em is the 8.8px. */
  translate: -0.105em calc(-100% - 0.29em);
  font-family: var(--font-jp-display);
  font-size: var(--fs-node);
  font-weight: 500;
  line-height: 1.2;
  letter-spacing: 0.32em;
  color: var(--ink);
  white-space: nowrap;
}
/* the closing 。, de-weighted so it reads as punctuation and not as a fourth
   point in the figure */
.node__dot {
  font-size: 0.6em;
  letter-spacing: 0;
  opacity: 0.55;
}
/* the leader line a star atlas draws from a caption back to its star. Static
   by default — this is a convention of the figure, not a scroll gesture —
   and the .js rule below only keeps its length in sync while the reveal
   below still has the text lifted off its landed position. Anchored with
   left/bottom rather than repeating .node's own translate, since a
   pseudo-element's box is the parent's post-translate box. */
.node::after {
  content: "";
  position: absolute;
  left: 0.105em;
  bottom: -0.29em;
  width: 1px;
  height: 0.29em;
  background: var(--dim);
  opacity: 0.4;
}

/* ═══ the brief ═════════════════════════════════════════════════════════
   The vertical CONNECTION IS OUR LANGUAGE. that used to stand at the right edge
   is gone, and so is the FIG. plate that stood alone at the foot. Both are
   folded into one block in the bottom-left corner, which is what the reference
   shows and what the client approved: a display line, four lines of Japanese, a
   warm rule, and the figure's number under it.

   THE COMPOSITION ARGUMENT for moving it, since it is the third time this
   corner has been rebuilt: the network now retreats toward its own origin at
   (49,10) — up and to the right of centre. Everything the camera does empties
   the bottom-left. Copy standing in the emptying corner is copy the drawing
   moves away from, which is the reading the client asked for in as many words
   — 「ネットワークはこのテキストブロックの背後で広がる」.

   Fixed to the screen and outside .cam, so nothing here scales. The generator
   reserves the rectangle (RESERVED in tools/gen_network.py) so no phrase is
   placed under it, and the scrim below thins the drawing out over it — belt and
   braces, on purpose: the keep-out holds the phrases off, the scrim handles the
   curves, which are not placed and cannot be reserved against. */
/* THE FIGURE LABEL. 六's direction, 2026-08-01: CONCEPT is a bridge between HERO
   and ABOUT rather than a section that explains itself, so the display copy comes
   out and one small chapter mark goes in. Everything the old block had — the two
   display lines, the two supporting lines, the amber rule, FIG. 02, and the .brief
   wrapper — is deleted rather than hidden.

   ON THE PAGE'S OWN LEFT GUIDE, not on the numbers 六 suggested. He asked for
   left: clamp(28px, 4.2vw, 64px) and bottom: clamp(36px, 7vh, 72px) and then said to
   use the site's existing variables instead if there are any. There are:
   --gutter is clamp(1.25rem, 5vw, 5.5rem) and every other section on this page is set
   to it, HERO and ABOUT included, and --plate-bottom is the pair the vertical legend
   already sits on. Hardcoding his numbers would have put this label 8px off the guide
   at 1440 and 24px off at 1920.

   --brief-in and --brief-up are deliberately NOT read here any more. They moved the
   old copy block inward and upward at the client's direction, and they are still read
   by .field-scrim::before — so the figure's darkness stays exactly where it is, which
   the brief for this change required. The label sits at the gutter, which is the
   darkest corner of that pool, so it gets the best contrast available without the
   pool being touched. */
.field-label {
  position: absolute;
  left: var(--gutter);
  bottom: var(--plate-bottom);
  margin: 0;
  white-space: nowrap;
  /* 9 / 10.5 / 11px across phone, tablet and desktop — 六 asked for 9–10 / 10 / 11 and
     --text-plate is a fixed 11px, so this is that token's value as the cap with a
     ramp under it rather than a second scale. */
  font-size: clamp(0.5625rem, 0.78vw + 0.28rem, 0.6875rem);
  font-weight: 400;
  line-height: 1;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  -webkit-font-smoothing: antialiased;
  /* --dim IS the warm white at the presence 六 asked for, which is why no new colour
     is introduced: --ink is #f4f2ea, and at the 0.53 alpha he specified over --void it
     resolves to rgb(134 133 129). --dim is #97917f. Same value, and it is already the
     token every small label on this page uses. 6.29:1. */
  color: var(--dim);
}
/* The amber at 0.72 of itself, inside 六's 0.65–0.75. opacity on the span rather than
   an alpha on the colour, so the token is used unmodified and the two multiply
   correctly with the parent's reveal. */
.field-label__n {
  margin-right: 0.5em;
  color: var(--amber);
  opacity: 0.72;
}

.connect {
  /* shared by the brief and the legend, so the two stay locked together at
     every breakpoint without repeating the clamp() */
  --plate-bottom: clamp(1.5rem, 4svh, 2.5rem);
  /* HOW FAR THE BRIEF COMES IN OFF THE GUTTER. The client: 「左下のテキストブロクもう
     少し中心寄りにしてもいいんじゃないかな ちょっと被るぐらいでも全然問題ない」. Measured
     first, because the answer differed by width more than it looked: --box is
     centred, so at 1920 it runs 468..1452 while the brief ended at 375 — it was
     not overlapping the figure at all there, which is exactly why it read as
     stranded in a corner. At 1440 the box starts at 318 and the brief already
     crossed it by 234px. One number could not describe both.

     So the inset grows with the viewport, which is where the extra room is. It is
     zero below 48rem: the phone has no corner pool, the brief takes the whole
     measure, and the drawing is mostly off-frame there anyway. */
  --brief-in: clamp(0rem, 13vw, 19rem);
  /* and up off the bottom, second pass: 「もう少し上右上にしてもいいよ」. Measured
     first — at 1280 the phrase 優先と保留。 sat 4px above the brief's top edge and
     先回りと確認。 already overlapped it vertically, so unlike the sideways move
     this one cannot be made inside the existing reservation. The generator
     re-solves against a new RESERVED; see the note there. */
  --brief-up: clamp(0rem, 9svh, 7rem);
  /* THE SCRIM'S BOX, and only the scrim's now. These were measured off the old copy
     block's render so the pool below could not drift off the copy it existed for;
     that block is gone and the pool is all that reads them. They are left at the
     measured values on purpose — shrinking them to fit a one-line label would lift
     the darkness off part of the figure, and the figure is what this change was told
     not to touch. Oversized here means a slightly larger dark corner and nothing else.
       768   301 x 273      1440  408 x 331
       1280  408 x 324      1920  408 x 331
     27rem is 432 and 21.5rem is 344, which contains all four. */
  --copy-w: min(27rem, calc(100vw - 2 * var(--gutter)));
  --copy-h: 21.5rem;
  /* the figure's box, stated once. .cam takes its height from it and the type
     inside takes its size from it, which is what keeps one generated solution
     valid at every width — see the note on .cam. */
  --box: min(calc(100svh - 6rem), calc(92vw - 2 * var(--gutter)));
  /* 30.4px / 804px and 10.0px / 804px, the two sizes the generator's constants
     were measured at. --text-node and --text-sat in tokens.css are the numbers
     these replace; the tokens are left alone because they are shared.
     CAPPED, and the cap is one-sided on purpose. The generator reserves a
     rectangle per phrase at 5.47 units of line box; type BELOW the ratio makes
     every reserved rectangle roomier than it needs to be and cannot introduce a
     collision, type above it can. So a max is free and a min would not be.
     The cap is there because 1920 is the one width where the ratio asks for type
     bigger than this page has ever set — 37.2px against the 30.4 every previous
     build used — and 1.5x the glyph area of a hundred phrases, each re-rastered
     with a 0.09em knockout stroke every frame, is real: measured interleaved
     against the straight-line build in one session at 1920x1080, uncapped came
     back median 35 over-20ms frames against its 23, and this is what closes it.
     At 1440 the cap is not reached (30.39 against 30.4) so nothing there moves. */
  --fs-node: min(calc(var(--box) * 0.0378), 1.9rem);
  --fs-zone: min(calc(var(--box) * 0.01244), 0.625rem);
}
/* The scrim. Gradients in the page's own ground, each opaque where type is and
   transparent everywhere else, so the drawing is simply not there behind a line
   of copy. Every one is a fade and never a cut — the network has to thin out
   where type is, not stop at an edge, and a hard edge here is a hairline
   waiting to leak on the compositor.

     · the top strip, for the fixed masthead nav. 8rem, which on the figure's
       own scale is everything above y = 1.6 at 1920: a sliver, and the network
       is running off that edge anyway.

     · the pool over the brief, which is NOT AN ELLIPSE and that is the point.
       The copy is a rectangle 408 x 331, and no ellipse covers a rectangle
       without reaching about 100px past its far side: centred on the block, an
       ellipse has to be 0.71 x the block's own width in semi-axis, which at 1440
       runs to x 578 — and 経験と可能性。, one of the three phrases the client
       approved, has its own point at x 586. Measured: the first build of this
       corner ate the 経 clean off it.

       So it is a soft-cornered RECTANGLE instead, and it is built from one
       background gradient and one mask rather than from mask-composite: the
       background carries the vertical fade, the mask carries the horizontal one,
       and multiplying an alpha by an alpha is the intersection. Nothing exotic,
       supported everywhere, and it covers 480 x 367 of copy with a 92px and an
       81px fade instead of a 789 x 609 pool. See .field-scrim::before.

     · a small pool on the left edge at mid height, for the vertical legend.
       Deliberately a local ellipse and not a full-height left ramp: a ramp down
       the whole edge takes a column of the drawing out along its entire length,
       and a column of missing network reads as a wall. The legend is 19 x 101 at
       1440, and 5.5rem x 7.5rem with the stop at 62% puts its own far corner at
       0.92 alpha and stops 88px short of the nearest region name. */
.field-scrim {
  position: absolute;
  inset: 0;
  /* above .cam, not between its two layers — see the note in the HTML. It has
     to stay screen-locked while the network retreats. Below the copy, which is
     the whole point of it. */
  z-index: 1;
  pointer-events: none;
  background:
    linear-gradient(to bottom, var(--void) 0, transparent 8rem),
    radial-gradient(5.5rem 7.5rem at 0% 50%, var(--void) 0 62%, transparent 100%);
}
/* the brief's own pool. The box is the copy's reach plus a 7rem fade band on the
   two sides that face the drawing; the other two are the screen's own edges,
   where the scrim is fully opaque and meets a --void ground on both sides of the
   section's clip — the same-colour seal this page needs at every fixed cut. The
   two stops are where the copy actually ends: 77% of 624 is 480 against the
   brief's 480, and 73% of 500 is 365 against its 367.

   7rem and not 4.5, and the number came off a render rather than a preference.
   The keep-out only holds the phrases off at --cam 1; past that the camera walks
   the outer phrases INTO this corner, and they have to leave through the fade.
   A 22px line of Japanese crossing a 72px ramp loses 30% of its alpha between
   its top and its bottom, which reads as a phrase with its feet rubbed out. Over
   112px it loses 20%, which reads as a phrase going into the dark. */
.field-scrim::before {
  content: "";
  position: absolute;
  /* Reaches the left edge and grows to follow the copy, but its right-hand ramp
     is a FIXED LENGTH IN PIXELS rather than a percentage of that width. Both
     other arrangements were tried and both broke:
       · width including --brief-in with a 23% ramp pushed the ramp 187px right at
         1440 and dropped it onto 経験と可能性。 — a 170px phrase inside a 142px
         ramp is a phrase with half of itself erased, which is the exact failure
         the ramp length was tuned against.
       · translating the whole pool right by --brief-in fixed that and created the
         mirror of it at the other end: 順番と余白。 sits at x 145 at 1440, outside
         the pool's new left edge, so half of it appeared and the half inside the
         pool did not.
     A ramp in px is invariant to the width, which is what lets the pool grow to
     cover a moved brief without re-tuning the falloff. 190px because a phrase at
     this camera position runs up to about 170px, and a phrase should cross the
     whole ramp rather than end inside it. */
  left: 0;
  bottom: 0;
  width: calc(var(--gutter) + var(--brief-in) + var(--copy-w) + 7rem);
  height: calc(var(--plate-bottom) + var(--brief-up) + var(--copy-h) + 7rem);
  background: linear-gradient(to top, var(--void) 0 73%, transparent 100%);
  -webkit-mask-image: linear-gradient(to right, #000 0 calc(100% - 190px), transparent 100%);
          mask-image: linear-gradient(to right, #000 0 calc(100% - 190px), transparent 100%);
}
/* The legend, now vertical at the left edge — three dots in a column with the
   label reading down beside them, which is where the reference puts it. It is
   the star atlas's own apparatus and it is kept to this one mark, so the figure
   stays a figure and does not turn into a dashboard.

   flex-direction: row and NOT column. In writing-mode: vertical-rl the inline
   axis runs down the page and the block axis runs right to left, so `row` is
   what stacks the three dots and the label vertically; `column` would lay them
   out across the screen. align-items centres them on the horizontal. */
.constel__legend {
  position: absolute;
  left: calc(var(--gutter) * 0.34);
  top: 50%;
  translate: 0 -50%;
  display: flex;
  flex-direction: row;
  align-items: center;
  gap: 0.55em;
  writing-mode: vertical-rl;
  font-size: 0.625rem;
  letter-spacing: var(--track-plate);
  color: var(--dim);
}
.constel__legend-dot {
  flex: none;
  border-radius: 50%;
  background: var(--amber-2);
}
.constel__legend-dot--1 { width: 6px; height: 6px; opacity: 1; }
.constel__legend-dot--2 { width: 4px; height: 4px; opacity: 0.85; }
.constel__legend-dot--3 { width: 2.5px; height: 2.5px; opacity: 0.65; }
.constel__legend-label { margin-top: 0.15em; }

/* ═══ 3 · about · 位相のずれた銀 ════════════════════════════════════════
   A dark room again, not a grey intermission: the full 120svh is --void.
   One silver mesh occupies three fixed cuts. The cuts are vertical and
   slightly oblique beside the desktop copy, then turn into horizontal cuts
   above it on a phone so the supplied 3:4 and 16:9 grades each keep their own
   composition.

   The critical construction is the 3px --void gasket between every outer cut
   and the bright image inside it. clip-path antialiasing is therefore sampling
   --void on BOTH sides of every shared boundary, never a silver pixel on one
   side and transparency on the other. This is the same-colour seal needed for
   a compositor-only half-sample leak; a screenshot is not being trusted to
   prove its absence. contain: paint is deliberately absent. */
/* ═══ 3 · about ═════════════════════════════════════════════════════════
   Static form, which is also the last frame of the sequence: the filament plate
   at its final width on the right, the type set and readable on the left. .js
   supplies the beginning of the gesture, not the end of it, so
   prefers-reduced-motion and scripting-off both land here and stop.

   The plate is 104vw wide and clipped from the left rather than 54vw wide. Same
   result at rest — the visible band is the right 54vw — but it means the whole
   gesture is one clip-path value moving from 0 to 48%, with the photograph never
   resizing and never reflowing. A width animation would relayout the image on
   every frame; a clip does not touch layout at all. */
/* overflow: clip unconditionally, not only under .js. The pin clips itself in the
   motion layer, and without it the resting state had nothing holding the plate's
   104vw or the sweep's start position — measured 547px of horizontal overflow at
   1440 and 730 at 1920 with the .js class removed, which is the reduced-motion path
   as well as the scripting-off one. */
.about-pin {
  position: relative;
  overflow: clip;
  background: var(--void);
  isolation: isolate;
}
.about__silver {
  position: absolute;
  right: 0;
  top: 0;
  /* 100vw, not 104. The extra 4% was overscan against a seam at the screen's left
     edge, and the left edge is where the clip-path hides the plate anyway — so all
     it bought was 4% more upscale on a 1672px source. */
  width: 100vw;
  height: 100%;
  background: var(--void);
  clip-path: inset(0 0 0 48%);
  /* the plate's own edges are still softened into the page, as before — a hard
     104vw rectangle would put a vertical seam down the middle of the screen at
     the start of the sequence */
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 7%, #000 93%, transparent 100%);
          mask-image: linear-gradient(to bottom, transparent 0, #000 7%, #000 93%, transparent 100%);
}
.about__frame {
  position: absolute;
  inset: 0;
  display: block;
  background: var(--void);
}
.about__frame img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
/* THE FOUR REGIONS OF THE REVEAL.
   Each is the same photograph, in the same box, behind a different soft mask. They
   are invisible at rest — the picture above them is the whole image and needs no
   help — and .js is what gives them their opacity ramps.

   Why the same picture five times instead of one picture and a black veil eaten
   away: identical sources, normal-blended, compose to 1−Π(1−αᵢ). That can never
   exceed the picture itself, so no overlap glows and no boundary can appear, and
   the only property animating is opacity, which stays on the compositor. A veil
   would have wanted its mask re-rasterised every frame instead.

   background-size: cover + 50% 50% against the picture's object-fit: cover + 50%
   50% in a box of identical size is the same arithmetic, so the layers are in
   register to the pixel. Verified by diffing a lit frame against the picture alone.

   THE ORIGINS ARE MEASURED, NOT PLACED, AND THEY RUN RIGHT TO LEFT. The luminance
   of about-web-16x9 was mapped over the whole frame (the reveal is full bleed now —
   the plate is only cropped to 52% afterwards, by the panel):

       x 76 y 27   a thin filament, the brightest interior run       → wave 1
       x 91 y 63   the brightest point on the frame, down and right, → wave 2
                   bridged back to wave 1 through the x 85 y 48 ridge
       x 47 y 51   the long central ridge, a jump across the frame   → wave 3
       x 19 y 44   the far left, and the upper-left structure at     → wave 4
                   x 11 y 26

   Right to left because the black then comes back left to right. The light crosses
   the frame one way and the panel takes that territory back the other, which is the
   whole gesture of the section in two moves.

   Each wave is two overlapping ellipses rather than one, so no wave is a circle,
   and the four together are not symmetrical about anything. */
.about__wave {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  /* The plain url() first, and it is not redundant. image-set() with type() is
     Safari 17, so on iOS 16 and earlier the image-set declaration is invalid and
     gets dropped — and with nothing before it, that left this element with no
     background at all: the ABOUT section as a black panel with text and no
     photograph, on every iPhone that cannot go past iOS 16 (an 8 or an X, which
     is not a small population for a site meant to last).
     A browser that does not understand the second declaration keeps the first;
     one that does, overrides it and takes the webp. The jpg is never fetched by
     the browsers that can read image-set, so this costs nothing but the line. */
  background-image: url("../img/about-web-16x9.jpg");
  background-image: image-set(url("../img/about-web-16x9.webp") type("image/webp"),
                              url("../img/about-web-16x9.jpg") type("image/jpeg"));
  background-size: cover;
  background-position: 50% 50%;
  background-repeat: no-repeat;
}
/* 1 · the thin filament. The smallest region on the frame, and the only one that is
   alone on the screen while it happens. */
.about__wave--1 {
  -webkit-mask-image:
    radial-gradient(ellipse 12% 15% at 76% 27%, #000 0%, rgb(0 0 0 / 0.88) 32%, rgb(0 0 0 / 0.40) 64%, transparent 88%),
    radial-gradient(ellipse 9% 12% at 80% 34%, #000 0%, rgb(0 0 0 / 0.70) 36%, transparent 84%);
          mask-image:
    radial-gradient(ellipse 12% 15% at 76% 27%, #000 0%, rgb(0 0 0 / 0.88) 32%, rgb(0 0 0 / 0.40) 64%, transparent 88%),
    radial-gradient(ellipse 9% 12% at 80% 34%, #000 0%, rgb(0 0 0 / 0.70) 36%, transparent 84%);
}
/* 2 · down and right, onto the brightest point there is, with the ridge between them
   carrying it there rather than a jump */
.about__wave--2 {
  -webkit-mask-image:
    radial-gradient(ellipse 20% 24% at 91% 63%, #000 0%, rgb(0 0 0 / 0.86) 28%, rgb(0 0 0 / 0.40) 62%, transparent 88%),
    radial-gradient(ellipse 16% 13% at 85% 48%, #000 0%, rgb(0 0 0 / 0.72) 32%, transparent 84%);
          mask-image:
    radial-gradient(ellipse 20% 24% at 91% 63%, #000 0%, rgb(0 0 0 / 0.86) 28%, rgb(0 0 0 / 0.40) 62%, transparent 88%),
    radial-gradient(ellipse 16% 13% at 85% 48%, #000 0%, rgb(0 0 0 / 0.72) 32%, transparent 84%);
}
/* 3 · across the frame to the long central ridge. The one real jump in the sequence,
   and the reason the light reads as travelling rather than swelling. */
.about__wave--3 {
  -webkit-mask-image:
    radial-gradient(ellipse 25% 18% at 47% 51%, #000 0%, rgb(0 0 0 / 0.85) 26%, rgb(0 0 0 / 0.38) 60%, transparent 87%),
    radial-gradient(ellipse 17% 16% at 62% 40%, #000 0%, rgb(0 0 0 / 0.68) 34%, transparent 84%);
          mask-image:
    radial-gradient(ellipse 25% 18% at 47% 51%, #000 0%, rgb(0 0 0 / 0.85) 26%, rgb(0 0 0 / 0.38) 60%, transparent 87%),
    radial-gradient(ellipse 17% 16% at 62% 40%, #000 0%, rgb(0 0 0 / 0.68) 34%, transparent 84%);
}
/* 4 · the far left, which is the half the panel is about to take back. Largest, and
   the one that makes the frame continuous. */
.about__wave--4 {
  -webkit-mask-image:
    radial-gradient(ellipse 26% 22% at 19% 44%, #000 0%, rgb(0 0 0 / 0.84) 24%, rgb(0 0 0 / 0.36) 58%, transparent 88%),
    radial-gradient(ellipse 16% 19% at 11% 26%, #000 0%, rgb(0 0 0 / 0.62) 32%, transparent 85%);
          mask-image:
    radial-gradient(ellipse 26% 22% at 19% 44%, #000 0%, rgb(0 0 0 / 0.84) 24%, rgb(0 0 0 / 0.36) 58%, transparent 88%),
    radial-gradient(ellipse 16% 19% at 11% 26%, #000 0%, rgb(0 0 0 / 0.62) 32%, transparent 85%);
}
.about__col {
  position: relative;
  z-index: 1;
  height: 100%;
  display: grid;
  align-content: center;
  justify-items: start;
  /* the plate's visible edge is at 45.9vw, so the column takes the room to its
     left less one gutter of air */
  width: min(100%, 42vw);
  /* LEFT PADDING OFF --gutter, ON PURPOSE: the client asked for +50–65px over
     the current position at 1440 / 1835 / 1920, and --gutter's own 5.5rem cap
     (hit at 1835 already) can only ever add +40px there, short of the ask at
     every width above it. A second clamp, sized for this column alone: 9vw
     puts 1440 at 129.6px (+57.6px) before the cap, and the cap itself sits at
     9rem/144px, which both 1835 and 1920 hit (+56px each) — one shape, inside
     the requested range at all three. Border-box keeps width at 42vw
     regardless, so the column's own right edge does not move; only where the
     text starts, and how narrow its measure runs, changes. */
  padding: clamp(4rem, 11svh, 7rem) 0 clamp(4rem, 11svh, 7rem) clamp(5rem, 9vw, 9rem);
}
.about__col .plate { margin-bottom: var(--space-2xs); }
/* room for the amber rule under ABOUT plus air: at --space-lg the rule sat on the
   heading's first line. */
.about__col .section-h { margin-bottom: clamp(2.25rem, 5svh, 3rem); position: relative; }
.about__col .section-h::after {
  content: "";
  position: absolute;
  left: 0;
  bottom: -0.55em;
  width: 3.6rem;
  height: 1px;
  background: linear-gradient(to right, var(--amber), rgb(255 181 74 / 0));
}
/* D · the peak, and the size is solved rather than chosen. 六 asked for clamp(42px,
   4vw, 64px) and for the lines to break naturally at 8–11 characters. At 4vw the
   longest line — 作用し合うように。, nine glyphs — comes to 539px against the 533px the
   column actually has at 1440, so it wrapped and put 「に。」 alone on a third line.
   That breaks the device as well as the setting: each line is masked by its own
   overflow box, so a line that wraps inside its box is two lines in one mask.

   Solved from the character count instead: nine glyphs at 1.04em of tracking need
   9.36em of measure, and the column gives 533px at 1440, so the face can be 53px
   there. clamp(2.4rem, 3.7vw, 3.6rem) is 53.3px at 1440 and 57.6 at 1920, both
   inside 六's 42–64 range, and both leave the nine glyphs on one line. */
.about__lead {
  margin-bottom: clamp(2rem, 4svh, 3rem);
  font-family: var(--font-jp-display);
  font-weight: 500;
  font-size: clamp(2.4rem, 3.7vw, 3.6rem);
  line-height: 1.3;
  letter-spacing: 0.04em;
  color: var(--ink);
}
/* one box per line, so each can be masked on its own. No overflow: hidden any
   more — that was there to hide a full line-height rise, and the rise is gone. */
.about__line { display: block; }
.about__line > span { display: block; }
.about__rule {
  display: block;
  margin-top: clamp(1.5rem, 4svh, 2.5rem);
  width: min(100%, 22rem);
  height: 1px;
  background: linear-gradient(to right, var(--amber), rgb(255 181 74 / 0));
  transform-origin: left center;
}

@media (max-width: 47.99rem), (pointer: coarse) and (max-width: 63.99rem) {
  /* THE PHONE DOES NOT DO THE PULL. 六: 「右へ大きく引く演出は行わない」. The plate is
     a band across the top from the start and the type is under it, so there is no
     corner to open and nothing to draw sideways. The sequence that remains is the
     mask on the heading and the fade on the body, which is what he specified. */
  .about-pin { overflow: clip; }
  /* Full bleed and behind everything, not a band at the top. A band would have to
     fit above the type, and the type is a label, a two-line 34px heading and five
     paragraphs — about 700px of an 844 screen. There is no room for an image band,
     and the first attempt at one put the label and the opening paragraph straight
     onto the brightest filaments. So the plate takes the whole pin and a scrim
     carries the text, which is 六's own fallback: 「画像と文字が重なる場合は黒いグラ
     デーションで可読性を確保」. Continuity with the network section survives either
     way — it is the same photograph arriving full-screen. */
  .about__silver {
    right: auto;
    left: 0;
    width: 100%;
    height: 100%;
    clip-path: none;
    -webkit-mask-image: none;
            mask-image: none;
  }
  .about__silver::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(to bottom,
      rgb(10 10 10 / 0.58) 0%,
      rgb(10 10 10 / 0.84) 26%,
      rgb(10 10 10 / 0.93) 100%);
  }
  .about__col {
    width: 100%;
    height: 100%;
    align-content: center;
    padding: clamp(1.5rem, 4svh, 2.5rem) var(--gutter);
  }
  .about__lead { font-size: clamp(1.6rem, 7.4vw, 2.2rem); }
}

/* ═══ 4 · what we do ════════════════════════════════════════════════════
   Static form: four blocks in a column, each with its own composition, each
   readable on its own. The conveyor in the motion layer does not change any
   of these compositions — it only sends them sideways. */
/* minmax(0, 1fr) and not 1fr: a bare 1fr is minmax(auto, 1fr), and the auto
   floor is the track's min-content height — four panels' worth of numeral
   and prose, which is half again the screen. The row would then be taller
   than the pin holding it and every photograph inside would be measured
   against the wrong box. */
/* THE BEAT BEFORE IT. ABOUT and this section were flush: ABOUT's plate ends at its
   own bottom edge and FIG. 04 sat 99px under it, so two completely different
   compositions shared a screen with nothing between them and the joint read as
   crowded. This is ordinary margin, so it is there without script as well. */
.stage--work { margin-top: clamp(6rem, 22svh, 13rem); }
.work-pin {
  position: relative;
  display: grid;
  /* the column is bounded for the same reason the row is: the single auto
     column would otherwise be sized by the 400%-wide track's min-content and
     the pin would lay out a screen that is not the screen */
  grid-template-columns: minmax(0, 1fr);
  grid-template-rows: auto minmax(0, 1fr) auto;
  gap: var(--space-md);
  padding: clamp(3rem, 9svh, 5.5rem) 0 clamp(1.5rem, 4svh, 2.5rem);
}
.work-head { padding-inline: var(--gutter); }
/* 六: 「01の内容と競合しない小さなイントロ」、大きな見出しにはしないこと。 So it is set at
   the note's own size, not at a heading's, and it is a <p> — the section already has
   its h2 and each card has an h3, and a third level here would be a heading that says
   nothing the h2 does not. */
/* Its SIZE is now the run's note size and not a size of its own. The client's note
   is 「左の導入エリアと各スライド本文のタイポ設計を統一」, and the intro was the one block
   in this section typed off --text-sm while the four cards were typed off four other
   numbers. One step, one leading: --wd-note at 1.9, the same pair all four notes
   take. */
.work-head__lede {
  margin-top: var(--space-lg);
  max-width: 26em;
  font-size: var(--wd-note);
  line-height: 1.9;
  color: var(--quiet);
}
/* THE SIZE IS UNTOUCHED. This heading is the section's benchmark and the client is
   explicit that it must not grow. What changes is the air under the plate label: 4px
   was the section's tightest step spent on its widest jump, 11px to 41.6px, so the
   two read as one lockup and the heading had no landing. 32px is the card's own
   label-to-statement step (12 + rule + 36 = 49) taken at the ratio the two statements
   sit at — 41.6 / 55.3 = 0.75, so 37, rounded onto the 4pt scale. The same 刻み,
   scaled by the thing it separates. Above 48rem the row-gap states it instead. */
.work-head__h {
  margin-top: calc(var(--space-md) * 2);
  font-family: var(--font-display);
  font-weight: 300;
  font-size: clamp(1.5rem, 3.4vw, 2.6rem);
  line-height: 1;
  letter-spacing: 0.03em;
}
/* --quiet and not --dim, which is the token this size of label normally takes.
   The counter is the one plate label on the page that does not stand on --void:
   under .js and above 48rem it is an overlay on the corner (see the bottom-edge
   block in the motion layer), and on 03 that corner is the foot of a paper
   photograph. Measured on --void --dim is 6.29:1 and fine; measured on 03's
   scrimmed foot it was 1.53:1, which is not a label, it is a watermark.

   This is the same move tokens.css already records for the plate label in the
   light band — 「--dim 3.34:1 ← the one that does not clear it, so the plate
   label in this band is lifted to --quiet」 — so it is the file's own rule for a
   plate label on a ground that is not the room, not a new idea. Nothing about
   the counter's size, tracking or position changes; only which of the two
   label inks it takes. The four grounds are measured in the report. */
.work__count {
  padding-inline: var(--gutter);
  font-size: var(--text-plate);
  letter-spacing: var(--track-plate);
  color: var(--quiet);
}
/* THE LEADING FIGURE ADVANCES, and nothing is written to the DOM to do it.
   Four figures occupy one box: the first is in flow and supplies the box's
   width and baseline, the other three are laid over it. The motion layer
   cross-fades them off --qs, which already steps 0 / ⅓ / ⅔ / 1 with a dwell on
   each — so the figure is constant for the whole of a card's rest and only
   moves during a hand-off. tabular-nums so the four boxes are the same width
   and the em dash after them cannot shift. */
.work__count-n {
  position: relative;
  display: inline-block;
  font-variant-numeric: tabular-nums;
}
.work__count-n > span { display: block; }
.work__count-n > span + span { position: absolute; inset: 0; opacity: 0; }
.work__count-rule {
  display: block;
  margin-top: var(--space-xs);
  width: min(100%, 16rem);
  height: 1px;
  background: var(--amber);
  opacity: 0.45;
  transform-origin: left center;
}

.work { display: grid; gap: clamp(3rem, 8svh, 5rem); }
.work__panel { position: relative; padding-inline: var(--gutter); }

/* ── THE GIANT NUMERAL, ONE RULE FOR FOUR CARDS ─────────────────────────
   Every panel sets its own --no and nothing else about the numeral changes:
   same family, same weight, same lining figures, same 0.74 leading. The form
   of the value is the same on all four as well —

     --no: clamp(<floor>, min(<vw>, <svh>), <cap>)

   vw so it holds its share of a narrow screen, svh so it can never grow taller
   than the frame it is a background mark in, and a rem cap so 1920 does not
   inflate it. The four differ only in the three numbers, which is the
   「カードごとに微調整」 the spec asks for rather than four unrelated sizes. */
.work__no {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: var(--no, var(--text-numeral));
  line-height: 0.74;
  letter-spacing: -0.03em;
  /* Cormorant Garamond's default figures are oldstyle, and its oldstyle "1"
     is a small capital I — both serifs, no flag — so panel 01 read as "0I"
     at 270px. The lining set has the flag. */
  font-variant-numeric: lining-nums;
  color: var(--ink);
  opacity: 0.9;
}
/* THE HIERARCHY INVERTS. The English service name was the display line here —
   clamp(1.35rem, 2.9vw, 2.3rem), up to 37px — and 六's spec puts it at 14–16px with
   a Japanese lead of 24–30px under it. That is the right way round for this site:
   every other section states itself in Japanese at display size and keeps its
   English as a label. WHAT WE DO was the one place doing the opposite.

   So the English becomes a name, the Japanese lead becomes the line, and the giant
   numeral is untouched — it was always the panel's display element. */
.work__en {
  font-family: var(--font-display);
  font-weight: 400;
  font-size: var(--wd-en);
  line-height: 1.3;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
/* The Japanese service name in amber, which is a change: it was --quiet at 13px.
   Amber is the page's only accent and it has meant "the light is on this" all the
   way down — on a card whose display line is now the lead beneath it, the service
   name is what identifies the card, so it is what the light is on. */
.work__jp {
  margin-top: var(--space-2xs);
  font-size: var(--wd-jp);
  /* the body's 1.9 put 11px of leading under a 12px label that is 4px below the
     name it belongs to, so the pair did not read as a pair. At 1 the two lines are
     one lockup and the rule below has something to close.

     --wd-jp now equals --wd-en (see tokens.css) instead of 2/3 of it, and the
     4px above stays as it was rather than growing to match: 4px was 33% of the
     old 12px line and is 20–22% of the new 18/20px one, which only pulls the
     pair closer — the right direction for a lockup, not the wrong one. Growing
     the gap to hold the old ratio would have loosened it just as the type
     itself got heavier. The lockup is 6px taller at 1440 and 7px taller at
     1835/1920 from the line-box alone (12→18, 13→20); the rule and the lead
     below inherit that shift, nothing above margin-top moves. */
  line-height: 1;
  letter-spacing: 0.16em;
  color: var(--amber);
}
/* The line each card is actually about. --font-jp-display is the face ABOUT's lead
   and the light band already use, so this is the third instance of one voice rather
   than a new one. */
.work__lead {
  margin-bottom: calc(var(--space-md) * 2);
  font-family: var(--font-jp-display);
  font-weight: 500;
  font-size: var(--wd-lead);
  /* 1.6 was a reading leading on a 30px line. At 55px it is 88px between two lines
     that are meant to be seen as one mark, and a display line set at body leading is
     precisely the 「テンプレっぽい」 the client is pointing at. 1.38 closes the pair into
     a block and does not touch the 0.04em the width arithmetic depends on. */
  line-height: 1.38;
  letter-spacing: 0.04em;
  color: var(--ink);
}
/* 3.5rem — 56px, the middle of the 40–70px the spec asks of the gold rule. It was
   72px, which at 26–32px of lead above it read as a second underline rather than a
   mark.

   THE TWO MARGINS ARE NO LONGER EQUAL, and that is the client's 「今は金線が両者を
   分けているが、間隔が均等すぎる」. 16/16 puts the rule at the midpoint of the gap,
   and a midpoint has no direction — the eye cannot tell whether the line closes
   the name above it or opens the statement below it. 12 above and 36 below, 1:3,
   makes it the name's own underline and hands the air to the 55px line, which is
   where a three-fold jump in size needs it. Total name-to-lead air is 49px against
   the 33px it was, on type that grew 1.8×.

   UNCHANGED when --wd-jp grew to match --wd-en. The 12px above is set from
   .work__jp's own baseline in flow, not from its font-size, so a taller jp
   line pushes the rule down with it but does not tighten the gap the rule
   sits in — it is still 12px clear above and 36px clear below. */
.work__rule {
  display: block;
  margin: var(--space-sm) 0 calc(var(--space-sm) * 3);
  width: 3.5rem;
  height: 1px;
  background: var(--amber);
  transform-origin: left center;
}
/* 26em, and the cap does real work now that it did not do at 13px. An em measure
   holds the same number of characters per line as the type scales, so 26em is 26
   Japanese glyphs at any size — the middle of the 25–30 a Japanese line is read
   comfortably at. At 33em and 16px it would be 528px and 33 glyphs, which is a line
   the eye loses its place on, and it would also be wider than three of the four text
   columns and so not a measure at all, just a column.

   416px at 1440 on all four cards. The four columns are 526 / 526 / 674 / 636 now,
   so the note is the narrower of the two measures on every card and the lead is the
   wider: the display line takes the full column, the reading copy is inset inside
   it. That difference is the density the client asked for — one block, two measures,
   rather than one block at one width. */
.work__note {
  max-width: 26em;
  font-size: var(--wd-note);
  line-height: 1.9;
  color: var(--quiet);
}
/* 02's five lines. Sizing and colour live here so the phone and the desktop share
   them; only the box the figure is drawn into changes between the two. The stroke
   is 1px in screen pixels regardless of how far the viewBox is stretched
   (vector-effect, declared on the paths below), which keeps them 極細 at any
   width. */
.wp2__wire {
  display: block;
  width: 100%;
  /* the viewBox's own ratio, so on a phone the box scales isotropically and the
     five node dots stay round. The desktop box is the panel's right flank and is
     stretched — 6.84 against 6.73 at 1440×900, which is a 2% difference and
     invisible; a phone forced into a 10rem band would be 2.9 against 1.6, and the
     dots would be flat 1px slivers. */
  aspect-ratio: 120 / 100;
  margin-bottom: var(--space-lg);
  color: var(--amber);
  pointer-events: none;
}
/* vector-effect is NOT an inherited property, so the attribute on the <g> does
   not reach the paths — it has to be declared on them. Without it the viewBox is
   stretched ~7× horizontally and ~6.6× vertically and a 1-unit stroke comes out a
   7px band that is also thicker on the near-horizontal runs than on the steep
   ones. With it every curve is exactly one screen pixel at every width. */
.wp2__wire-l path { vector-effect: non-scaling-stroke; }
.wp2__wire-l { stroke-width: 1px; opacity: 0.34; }
/* the two that are further back. 0.34 × 0.58 ≈ 0.20 against --void — present,
   and under the lead's own contrast by an order of magnitude */
.wp2__wire-l .wp2__wire--x { opacity: 0.58; }
.wp2__wire-n { opacity: 0.5; }
/* THE 04 BUG, and it was never about 04. `picture` was missing from this
   list, so `height: 100%` on the <img> was resolving against a <picture> that
   is an inline box with no height of its own — which makes it compute to
   auto, and every photograph in this section was therefore laid out at
   width:100% × its own 4:3 aspect instead of covering its box. Measured at
   1280×800 before the fix:

     01  plate 563×563  ·  img 563×423   (short by 140px, hidden by scale 1.1)
     03  plate 1280×563 ·  img 1280×960  (over by 397px, cropped from the top)
     04  plate 333×563  ·  img 333×250   (short by 313px — 56% of the box)

   01 and 03 got away with it because their boxes are wide and the overflow is
   clipped; 04's plate is a 26%-wide vertical column, so the same rule left it
   a small 4:3 photograph pinned to the top of a tall empty box, which is
   exactly the "the photo is tiny and there is a dead corner" the client saw.
   One selector. object-fit then does what it was always supposed to. */
.wp1__plate, .wp4__plate, .wp3__plate { position: relative; overflow: clip; }
.wp1__plate picture, .wp4__plate picture, .wp3__plate picture,
.wp1__plate img, .wp4__plate img, .wp3__plate img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ═══ 5 · the light band ════════════════════════════════════════════════
   Static form: the room, lit, with the message in it. The shutter does not
   exist without JS — there is nothing to open. */
/* The room is the whole band and not a card inside it — a lit rectangle with
   a dark margin round it would be a slide projected on a wall, and the point
   is that the room's own lights have come up. So the padding lives on the
   room, never on the band. */
.light {
  position: relative;
  display: grid;
  min-height: 78svh;
  padding: 0;
  background: var(--paper);
  color: var(--paper-ink);
  text-align: center;
  overflow: clip;
}
.light__room {
  position: relative;
  z-index: var(--z-plate);
  display: grid;
  justify-items: center;
  align-content: center;
  gap: clamp(1.2rem, 3.4svh, 2.2rem);
  padding: clamp(4rem, 12svh, 7rem) var(--gutter);
  background: var(--paper);
  width: 100%;
  height: 100%;
}
.light__plate { color: var(--paper-dim); }
/* The one lit screen on the page, and the Japanese is now the statement on
   it. The client asked for 日本語を綺麗にシンプルにバシッと and the setting is
   the whole of that instruction and nothing else: one line, 92px at 1280,
   Shippori Mincho 500 — the same face the constellation's three hubs are
   set in, so the two places on this page where Japanese is meant to be *read*
   share a voice — tracked at 0.06em, which is open enough that the glyphs are
   not touching and closed enough that it is a sentence and not a row of
   characters. Nothing else: no rule, no blur, no second colour, no entrance
   beyond the one the room already has.
     --paper-ink on --paper: 16.28:1 */
.light__h {
  font-family: var(--font-jp-display);
  font-weight: 500;
  font-size: var(--text-light);
  line-height: 1.26;
  letter-spacing: 0.06em;
  color: var(--paper-ink);
}
/* The English is a running head under the Japanese, at the plate's own size — which
   is what an English line on a Japanese page is for. It was EVERY CONNECTION IS THE
   BEGINNING., itself demoted from being this band's display line because it was in
   the wrong language for the one screen the page spends its only inversion on. It is
   now THE VISIBLE IS SHAPED BY THE UNSEEN., which says the Japanese again rather than
   sitting beside it, and ties to the plate above (BEFORE THE LIGHTS COME UP). With it
   gone, HERO's WE MAKE CONNECTIONS WORK. is the only CONNECTION left on the page.
     --paper-dim on --paper: 6.36:1 */
/* the one place the tracking closes: at 320 the difference between 0.06em and
   0.02em is 27px on an 11-glyph line against a 280px measure, i.e. it is the
   difference between one row and two */
@media (max-width: 26rem) {
  .light__h { letter-spacing: 0.02em; }
  /* the plate and the running head close too, for the same reason and by the same
     arithmetic. --track-plate is 0.26em at this width, and on the 36 glyphs of
     THE VISIBLE IS SHAPED BY THE UNSEEN. that is about 100px of pure tracking
     against a 280px measure. 0.12em gives half of it back. Both lines wrapped to two
     rows here before this copy as well — measured at 246px and 206px for the previous
     strings — so this is a fix to an existing fault rather than to a new one. */
}
.light__en {
  margin-top: calc(var(--space-xs) * -1);
  font-size: var(--text-plate);
  font-weight: 300;
  letter-spacing: var(--track-plate);
  text-transform: uppercase;
  color: var(--paper-dim);
}
/* The tracking closes on both small lines at 320, for the same reason and by the same
   arithmetic as .light__h above: --track-plate is 0.26em there, and on the 36 glyphs of
   THE VISIBLE IS SHAPED BY THE UNSEEN. that is about 100px of pure tracking against a
   280px measure. Both wrapped to two rows before this copy too — 246px and 206px for
   the previous strings — so this repairs an existing fault rather than a new one.

   AND IT HAS TO LIVE HERE, after .light__en, not up in the media block beside
   .light__h. Written there it had the same specificity as .light__en's own
   letter-spacing and lost to it on source order, which is why the plate started
   fitting on one line and the running head did not: .plate is declared earlier in the
   file, .light__en later. Same rule, two different outcomes, and the only thing that
   differed was where the two selectors happened to sit. */
@media (max-width: 26rem) {
  .light__plate, .light__en { letter-spacing: 0.12em; }
}
.blade { display: none; }

/* ═══ 6 · colophon ══════════════════════════════════════════════════════
   Static form: the plate above, the 奥付 below.

   The client's word for what was here was 一番ダサい, and there were four
   separate reasons, none of them about the copy:

     · all four blocks were centred, on a page where every other section is
       set to a left edge and is deliberately lopsided. Symmetry read as a
       template because on this page it *is* the odd one out.
     · a radial warm glow sat behind them. It was the only ready-made on the
       site — an off-the-shelf lens flare — and it is deleted, not dimmed.
     · there was one line of content: an address.
     · and the bottom half of the screen was empty, so the page stopped
       rather than ended.

   All four are the same fix. The ask, the company facts and the footer are
   not three things stacked, they are one colophon: two columns off one left
   edge, set as ruled rows the way the back of a catalogue carries its own.
   The page has been running FIG. 01 … MAG. 1—3 as publication apparatus the
   whole way down; this is where a publication puts its 奥付, and the density
   of it against the middle of the site is what makes it read as an ending
   rather than as a stop.

   The form is the left column, and it is built out of the imprint's own
   furniture rather than out of form furniture: the same 11px tracked label in
   a fixed left column, the same value in the measure beside it, the same
   hairline under every row, the same 0.62rem vertical rhythm. Nothing here
   has a box around it, a corner radius or a shadow, because nothing else on
   this page does. Read across, the two columns say the same sentence in two
   directions — what is already written down about this company on the right,
   and the space for what you have to say to it on the left.

   Three things carry the form's state, and all three are in-palette:
     · resting   the row rule at 0.14 neutral, the label at --dim
     · focused   the rule to --ink 0.55 and the label to --quiet, on top of
                 the page's own amber :focus-visible ring — two signals,
                 neither of them the browser's default
     · invalid   the rule and the label to --amber, and the message under the
                 row in --amber. Amber is the page's only accent and it means
                 "the light is on this"; an error is exactly that, so no
                 second hue had to be invented for it.                    */
/* Measured at 1440: the send button ended at 437 and the logotype began at 642,
   205px apart — 126 of the band's shared bottom padding plus 81 of the footer's
   top. On a 900px screen that is enough empty room after the last thing a reader
   can act on that the page trails off instead of closing. This band gets its own
   bottom padding for that reason: it is the only one with a footer under it, and
   the two are one movement. About 108px between them now, which reads as a space
   left on purpose. */
.band--contact {
  /* The head of this band comes up sooner than the shared .band padding allows.
     六 asked for the run from the shutter closing to the CONTACT heading to fit in
     8–16vh. It cannot fully — this is an in-flow band, so --p is pinned at 1 until
     the band's own top edge reaches the fold and nothing here can respond before
     that — but the heading's distance from that top edge is ours to set, and 126px
     of it was inherited padding. Measured: closed shutter to readable heading was
     260px at 1440 before this. */
  padding-block-start: clamp(3rem, 8svh, 5rem);
  padding-block-end: clamp(1.5rem, 5svh, 3.25rem);
  display: grid;
  /* was center. The plate still centres itself; the words no longer do. */
  justify-items: stretch;
  gap: clamp(3rem, 9svh, 6rem);
  padding-inline: var(--gutter);
  overflow: clip;
}
/* The line the shutter hands over. 1px, the page's own amber falling to nothing,
   and the same length as the rule ABOUT and the brief carry — 六 asked for the
   existing rule's length and colour to be reused rather than a new mark invented.
   At rest this is all it is: an accent rule at the head of the section. */
.close-line {
  display: block;
  width: clamp(6rem, 12vw, 9.5rem);
  height: 1px;
  margin-bottom: clamp(1.5rem, 4svh, 2.5rem);
  background: linear-gradient(to right, rgb(255 217 160 / 0.9), rgb(255 181 74 / 0));
}
.close-words {
  text-align: left;
  display: grid;
  justify-items: start;
  gap: clamp(1.8rem, 5svh, 3rem);
  width: 100%;
  /* Centred rather than hugging the left gutter. Measured at 1920: the content
     ran x 88..1240 and left 680px of nothing on the right — 「今ブロク全体が左に
     寄っててバランスが悪い」. The aperture behind it is centred on 50.4%, so a
     centred column is also the one that agrees with the photograph. Capped so
     the measure does not stretch past reading width on a wide screen. */
  max-width: 84rem;
  margin-inline: auto;
  /* how far this centred measure sits inside the page's own gutters. The colophon
     borrows it back to reach the right gutter; nothing else does. */
  --bleed: max(0px, calc((100vw - 2 * var(--gutter) - 84rem) / 2));
}
.close-lede {
  display: grid;
  justify-items: start;
  gap: var(--space-md);
  max-width: 36rem;
}
/* The client: 「フォームと会社概要のバランスというかレイアウトというか距離がまず近いのと
   なんかあんま工夫がないなと思う」. Both halves of that were measurable, and the second
   one is the real finding: at 1440 the form ran -66..536 and the record -65..389.
   Their tops did not align, their bottoms did not align, and their heads were 67px
   apart. Nothing in the composition was related to anything else — two blocks of
   the same label-and-value furniture, at arbitrary offsets, 88px apart. That is
   what reads as unconsidered, more than the tightness did.

   Three changes, and they are one change:

   · The pair now spreads to the full measure rather than stopping at 72rem, which
     also removes the void that was left on the right of it.
   · The gap roughly doubles. Two dense blocks 88px apart read as one crowded
     block; the record has to be far enough away to be a separate thing.
   · The record's head is set level with the form's FIRST FIELD, which is the
     stagger stated as arithmetic instead of guessed at with an svh: it is exactly
     the height of the form's own head block. The record begins where the form's
     first field begins. That is a relationship, and it is the one thing the two
     columns did not have.

   The form still takes the wider track: it is the only thing here a reader can
   act on, and the one with a 4-line textarea in it. */
.colophon__cols {
  display: grid;
  grid-template-columns: minmax(0, 1.08fr) minmax(0, 0.92fr);
  gap: clamp(2.5rem, 9vw, 10.5rem);
  align-items: start;
  width: 100%;
  max-width: 84rem;
}
.close__h {
  font-family: var(--font-display);
  font-weight: 300;
  font-size: var(--text-section);
  line-height: 1;
  letter-spacing: 0.02em;
}
/* Shared with contact.php's result page and privacy.html, where it carries a
   whole paragraph of prose — which is why it is set as body copy and not as a
   display line. It briefly was one, for a Japanese statement under LET'S
   CONNECT.; both are gone and this has to go back with them. */
.close__jp { color: var(--quiet); font-size: var(--text-md); }

/* THE ELEVEN LINES ARE GONE. The client, on the whole close: 「あまりにも工夫が
   なさすぎる」「デザインが全くされてない」「これ最後締める部分なので」.

   The block this replaces was designed on a real idea — a publication's 奥付,
   set as ruled rows the way the back of a catalogue carries its own — and the
   idea is defensible in prose and wrong on the screen. Five field rules, six
   imprint rules and two column-head rules is eleven near-identical hairlines
   stacked in two columns, on a page that spent an entire day removing horizontal
   lines. It reads as a spreadsheet, and a spreadsheet is what 「デザインが全く
   されてない」 describes.

   The deeper problem: this section had furniture where every other section has a
   gesture. The hero darkens, the network propagates while the camera retreats,
   WHAT WE DO sends four panels through a held frame, section 5 inverts the page.
   The close had a heading and a table.

   So: not one rule anywhere in it. The gesture the section already owned — the
   aperture opening, the same aperture the page opened with — is allowed to stay
   on screen instead of dissolving to nothing, and the composition sits in its
   light. What a line under each row was doing is done by the ground under each
   field instead: the fields are the one thing here a reader can touch, so they
   are the one thing allowed to be tangible. 6.29:1. */
.close-colhead {
  width: 100%;
  /* was var(--space-md); stepped up one rung alongside the field gap below so
     the heading still reads as separate from the field rhythm it now
     approaches in size (space-lg > the new space-md field gap) */
  margin: 0 0 var(--space-lg);
  font-size: var(--text-plate);
  font-weight: 300;
  line-height: 1.6;
  letter-spacing: var(--track-plate);
  color: var(--dim);
}

/* ── the form ─────────────────────────────────────────────────────────── */
.close-form {
  position: relative;   /* contains the honeypot */
  width: 100%;
}
/* --labw is the label column, and it is one number in two places: the grid
   track, and the indent the error message under a row lines up to. 6.9rem is
   お問い合わせ内容 — eight glyphs at 11px and 0.14em, which palt brings in to
   about 98px — with a little air after it. */
.close-form { --labw: 6.9rem; }

/* Label above 必須, both in the fixed left column; the field spans both rows of
   the pair in the second. The tag used to sit at the far right end of the rule,
   which put a 必須 or a 任意 at the outer edge of the composition five times
   over, each one disconnected from the field it governs. */
/* The grounds need air between them or the five of them butt into a single
   slab — which is what the row rules used to be preventing, and the reason a
   field ground cannot simply be dropped in where a rule came out. */
.close-field { width: 100%; }
/* was var(--space-sm) (0.75rem) — the brief calls this the one place reading
   as under-designed; one rung up the space scale, not a doubling, keeps it
   inside the token ladder while still visibly loosening the five-field run. */
.close-field + .close-field { margin-top: var(--space-md); }
.close-field__row {
  display: grid;
  grid-template-columns: var(--labw) minmax(0, 1fr);
  grid-template-areas: "lab field" "tag field";
  /* the field spans both rows, and with both rows auto its height is shared out
     between them — which pushed 必須 to the middle of the textarea. The label
     row takes its own height and the second row takes the rest. */
  grid-template-rows: min-content minmax(0, 1fr);
  gap: 0 var(--space-md);
  align-items: start;
}
/* the same 11px / 2.2 / 0.14em / --dim / 0.62rem the imprint's <dt> is set in.
   They are the same row in two columns and they have to measure the same. */
.close-field__label {
  grid-area: lab;
  /* carries the label's baseline down across the ground's own top padding so it
     sits on the field's first line */
  padding: 0.86rem 0 0;
  font-size: var(--text-plate);
  line-height: 2.2;
  letter-spacing: 0.14em;
  color: var(--dim);
  transition: color var(--dur-micro) var(--ease-out);
}
/* The field is a ground, not a line. 0.038 of the ink over --void is the
   faintest lift that still reads as a surface at arm's length on a monitor,
   which is what this page is being shown on on 8/4. No radius and no shadow,
   because nothing else on the page has either — it is a slightly lit rectangle
   in a dark room, which is the site's one idea. */
.close-field__in {
  grid-area: field;
  align-self: start;
  width: 100%;
  padding: 0.82rem 0.95rem;
  font-family: inherit;
  font-weight: 300;
  font-size: var(--text-sm);
  line-height: 1.9;
  letter-spacing: 0.03em;
  color: var(--ink);
  background: rgb(244 242 234 / 0.038);
  border: 0;
  /* transparent at rest, so the focus line costs no reflow when it arrives */
  border-bottom: 1px solid transparent;
  border-radius: 0;
  appearance: none;
  caret-color: var(--amber);
  /* capped, now that the pair spreads to the whole measure. An 11-character name
     does not need 560px of ground, and a field wider than its own content reads
     as a slab rather than as something to fill in. The textarea overrides it. */
  max-width: 32rem;
  transition: background-color var(--dur-micro) var(--ease-out),
              border-color var(--dur-micro) var(--ease-out);
}
textarea.close-field__in { resize: vertical; min-height: 6.5rem; max-width: 40rem; }
/* Chrome paints autofilled inputs on a white-blue ground and there is no
   property that turns it off — an inset shadow the size of the field is the
   only way to put the room back. */
.close-field__in:-webkit-autofill,
.close-field__in:-webkit-autofill:hover,
.close-field__in:-webkit-autofill:focus {
  -webkit-text-fill-color: var(--ink);
  -webkit-box-shadow: 0 0 0 100px var(--void) inset;
  caret-color: var(--amber);
  transition: background-color 6000s;
}
/* One amber line on the page at a time, and it is under the field you are in.
   That is the same sentence the rest of the site says with amber — the light is
   on this — and it is why an error still needs no second hue. */
.close-field__row:focus-within .close-field__in {
  background: rgb(244 242 234 / 0.068);
  border-bottom-color: var(--amber);
}
.close-field__row:focus-within .close-field__label { color: var(--quiet); }

.close-field__tag {
  grid-area: tag;
  font-size: 0.625rem;
  line-height: 1.5;
  letter-spacing: 0.2em;
  white-space: nowrap;
  color: var(--amber);        /* 11.28:1 */
}
/* 任意 is the absence of 必須, and marking both puts a tag on all five fields.
   Left in the DOM so the markup still states it and form.js is untouched. */
.close-field__tag:not(.close-field__tag--req) { display: none; }

.close-field__err {
  display: none;
  margin-left: calc(var(--labw) + var(--space-md));
  padding: 0.45rem 0 0.15rem;
  font-size: var(--text-sm);
  line-height: 1.7;
  color: var(--amber);
}
.close-field.is-bad .close-field__err { display: block; }
.close-field.is-bad .close-field__in {
  background: rgb(255 181 74 / 0.055);
  border-bottom-color: var(--amber);
}
.close-field.is-bad .close-field__label { color: var(--amber); }

/* not display:none — a bot that skips hidden fields is a bot this does not
   catch. aria-hidden and tabindex="-1" are on the markup. */
.close-trap {
  position: absolute;
  left: -9999px;
  top: 0;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* Indented to the fields' own left edge — it belongs to the form, and starting
   it at the column edge while every field starts 6.9rem in read as a stray line
   under the block. --quiet rather than --dim: 「文字が暗すぎて見にくい」 was said about
   this section once already. */
.close-privacy {
  margin-left: calc(var(--labw) + var(--space-md));
  padding-top: var(--space-lg);
  font-size: var(--text-sm);
  line-height: 1.9;
  color: var(--quiet);
}
/* On its own line. In the form's measure the sentence ran out mid-link and broke
   between 個 and 人情報 — a break inside a word, the one thing Japanese setting
   does not do. */
.close-privacy a {
  display: inline-block;
  margin-top: 0.5em;
  color: var(--quiet);
  border-bottom: 1px solid rgb(151 145 127 / 0.4);
  transition: color var(--dur-micro) var(--ease-out),
              border-color var(--dur-micro) var(--ease-out);
}
.close-privacy a:hover { color: var(--ink); border-bottom-color: var(--ink); }

/* The one button on the site, and it has to look like one. It was a full-width
   bar between an amber rule and a dimmer amber rule, which on a page of ruled
   rows is indistinguishable from another row — two more of the eleven lines, on
   the only thing here a reader is meant to press.

   Now it is bounded: sized to its own words, indented to the fields' left edge
   so the form reads as one object, outlined rather than filled at rest. The fill
   still sweeps from the left because every rule on this page is drawn from one
   end, and it goes to full amber rather than a tenth of it — on hover the button
   is the lit thing in the room and its words go to --void. */
.close-send {
  position: relative;
  display: inline-flex;
  align-items: baseline;
  gap: var(--space-md);
  margin: var(--space-lg) 0 0 calc(var(--labw) + var(--space-md));
  padding: 1.05rem 2.9rem;
  background: none;
  border: 1px solid rgb(255 181 74 / 0.55);
  border-radius: 0;
  color: var(--ink);
  font-family: var(--font-body);
  font-weight: 300;
  cursor: pointer;
  overflow: clip;
  transition: color var(--dur-micro) var(--ease-out),
              border-color var(--dur-micro) var(--ease-out);
}
.close-send::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--amber);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform calc(var(--dur-micro) * 3) var(--ease-out);
}
.close-send:hover::before,
.close-send:focus-visible::before { transform: none; }
.close-send:hover,
.close-send:focus-visible { border-color: var(--amber); color: var(--void); }
.close-send:hover .close-send__jp,
.close-send:focus-visible .close-send__jp { color: rgb(10 10 10 / 0.7); }
.close-send:active { transform: scale(0.994); }
.close-send__en,
.close-send__jp { position: relative; }
.close-send__en {
  font-size: var(--text-plate);
  letter-spacing: 0.42em;
  text-indent: 0.42em;   /* the trailing track, given back to the centring */
}
.close-send__jp { font-size: var(--text-sm); letter-spacing: 0.14em; color: var(--quiet); }
.close-send[disabled] {
  cursor: default;
  color: var(--dim);
  border-color: rgb(255 181 74 / 0.22);
}
.close-send[disabled]::before { transform: scaleX(0); }
.close-send[disabled] .close-send__jp { color: var(--dim); }

/* the live region. Never display:none — a region a screen reader has stopped
   watching announces nothing when it fills. It is empty and unpadded instead,
   and only the em-rule marker is taken away. */
.close-status {
  position: relative;
  padding: var(--space-md) 0 0 1.6rem;
  font-size: var(--text-sm);
  line-height: 1.9;
  color: var(--quiet);
}
.close-status::before {
  content: "";
  position: absolute;
  left: 0;
  top: calc(var(--space-md) + 0.92em);
  width: 1rem;
  height: 1px;
  background: currentColor;
}
.close-status:empty { padding: 0; }
.close-status:empty::before { content: none; }
.close-status[data-state="busy"] { color: var(--dim); }
.close-status[data-state="ok"]   { color: var(--amber); }
.close-status[data-state="ng"]   { color: var(--amber); }

/* The imprint, with its six rules taken out and the space they were dividing
   given to the rows. Six short facts do not read as six unrelated things because
   they are ruled apart; they read as one block because the label column is a
   column and the leading is even.

   It sits lower than the form now. The two heads used to start on the same line,
   which made the ask and the record symmetric peers — and symmetry is the thing
   this page never does anywhere else. The record is the quieter of the two, so it
   arrives under the form's first field. */
.colophon__imprint {
  width: 100%;
  /* the height of the form's head block — one 11px line at 1.6 plus its own
     margin — so 会社概要 sits on the top edge of the お名前 field */
  padding-top: calc(1.6 * var(--text-plate) + var(--space-md));
}
/* --space-lg. Two reasons that agree: six facts with no rules between them need
   the leading to do the separating, and this is the shorter of the two columns, so
   air closes part of the height it ends above the form. It went to --space-md while
   the colophon lived in that space and had to be paid for out of one screen; the
   colophon has its own screen now, so both reasons are back. */
.colophon__dl {
  display: grid;
  gap: var(--space-lg);
  width: 100%;
  margin: 0;
}
.colophon__row {
  display: grid;
  grid-template-columns: 7.5em minmax(0, 1fr);
  gap: 0 var(--space-md);
}
.colophon__row dt {
  font-size: var(--text-plate);
  line-height: 1.9;
  letter-spacing: 0.14em;
  color: var(--dim);
}
.colophon__row dd {
  margin: 0;
  font-size: var(--text-sm);
  line-height: 1.9;
  color: var(--quiet);
}
.colophon__row--name dd { font-size: var(--text-md); color: var(--ink); }
.colophon__en {
  display: block;
  font-family: var(--font-display);
  font-size: var(--text-md);
  letter-spacing: 0.05em;
  color: var(--dim);
}
.mail {
  display: inline-block;
  font-family: var(--font-display);
  font-size: var(--text-mail);
  letter-spacing: 0.05em;
  color: var(--amber);
  border-bottom: 1px solid rgb(255 181 74 / 0.45);
  padding-bottom: 0.1em;
  transition: border-color var(--dur-micro) var(--ease-out), transform var(--dur-micro) var(--ease-out);
}
.mail:hover { border-bottom-color: var(--amber); }
.mail:active { transform: scale(0.985); }

/* ── the two pages off the side of this one ─────────────────────────────
   contact.php's rendered result (the scripting-off path) and privacy.html.
   Both are the colophon's own furniture on the colophon's own ground, with
   no masthead, no motion layer and nothing above them — they are the back
   matter, and the back matter of a book is set quieter than the book. */
.close-result {
  min-height: 100svh;
  align-content: center;
}
.close-result .close-words { max-width: 46rem; }
.close-result__errs {
  margin: 0;
  padding-left: 1.1rem;
  font-size: var(--text-sm);
  line-height: 1.9;
  color: var(--amber);
}
.close-result__back { padding-top: var(--space-md); }

.close-doc {
  display: grid;
  gap: var(--space-xl);
  max-width: var(--measure);
  border-top: 1px solid rgb(151 145 127 / 0.2);
  padding-top: var(--space-xl);
}
/* The plate treatment this used to have was right for the page it was written
   for: five articles, each a heading and one paragraph, where a small dim label
   was enough to mark a turn. The real policy is 3,090px of six articles with
   lists, sub-headings inside two of them, and a signature — and measured on that
   page the hierarchy was upside down:

     article heading  h2   11px / 300 / --dim    ← the weakest text on the page
     body                  13px / 300 / --quiet
     sub-heading      h3   13px / 500 / --quiet  ← stronger than its own parent

   A child heading louder than its parent is not a preference, it is a document
   you cannot scan, and this is the one page on the site a reader actually has to
   scan rather than be carried through. So the article heading takes the ink and
   a step of size, and the tracking comes back to earth: --track-plate is 0.32em,
   which is a label's spacing at 11px and a sprawl at 15. 0.1em keeps the
   lettered character without breaking the words apart.

   Sizes stay inside the existing scale — the step used is --text-md, the same
   one CONTACT's own labels sit on — so no new type step enters the page. */
.close-doc h2 {
  margin: 0 0 var(--space-sm);
  font-size: var(--text-md);
  font-weight: 500;
  line-height: 1.6;
  letter-spacing: 0.1em;
  color: var(--ink);
}
.close-doc p { color: var(--quiet); font-size: var(--text-sm); line-height: 2; }
.close-doc p + p { margin-top: var(--space-md); }

/* privacy.html grew from 5 placeholder articles to 6 articles with nested
   lists, sub-headings inside article 2 and 6, and a signature line closing
   article 1. These sit inside .close-doc, which contact.php's result page
   does not use, so extending it here does not touch that page. */
.close-doc__list,
.close-doc__subhead,
.close-doc__sign,
.close-doc__links {
  margin-top: var(--space-md);
}
.close-doc h2 + .close-doc__list,
.close-doc h2 + .close-doc__subhead {
  margin-top: 0;
}
.close-doc__subhead + .close-doc__list,
.close-doc__subhead + p {
  margin-top: var(--space-xs);
}
.close-doc__list,
.close-doc__links {
  padding-left: 1.1rem;
  font-size: var(--text-sm);
  line-height: 2;
}
.close-doc__list { color: var(--quiet); }
.close-doc__list li + li,
.close-doc__links li + li {
  margin-top: 0.4em;
}
.close-doc__subhead {
  font-size: var(--text-sm);
  font-weight: 500;
  color: var(--quiet);
}
.close-doc__sign {
  margin-top: var(--space-lg);
  padding-top: var(--space-sm);
  border-top: 1px solid rgb(151 145 127 / 0.2);
  color: var(--dim);
  font-size: var(--text-sm);
  line-height: 1.8;
}
.close-doc a {
  color: var(--quiet);
  border-bottom: 1px solid rgb(201 195 180 / 0.4);
  transition: color var(--dur-micro) var(--ease-out), border-color var(--dur-micro) var(--ease-out);
}
.close-doc a:hover { color: var(--amber); border-bottom-color: var(--amber); }

/* ═══ 7 · the foot of the colophon ══════════════════════
   A SCREEN OF ITS OWN, and the mark can finally be big enough to be itself.

   Five shapes came before this one and the sequence is the argument: a centred
   stack (one of the four centred blocks the client called 一番ダサい); a left-huddled
   row under a full-width divider (the divider was the worst line on a page that
   spent a day removing them); space-between across the page (a barbell with 900px
   of nothing between its ends); the lower-right cell of the close (which worked,
   but capped the mark at 98px); and dead-centre on a screen of its own, which the
   client then loosened — 「ドセンターでなくてもいいわ とにかくこのロゴと…フッターに
   含めるものを入れて1画面使っちゃおう それで印象的な最後にしよう」.

   The 98px cap is why a screen was needed rather than a better arrangement. The
   mark's one fine feature — the channel between the counter and the wedge — is 5.3
   of 333 units and needs about 2.4px to read, so it needs 150px of width. At 260
   the channel is 4.1px: the original mark, crisp, with nothing redrawn and no
   optical variant. Three commits argued about that measurement; this ends it.

   TWO REGISTERS, so the screen is not symmetrical twice over. The card — mark,
   rule, name — is centred, because one mark alone on an empty screen is an end
   card and an end card is centred. The furniture is split to the two gutters on
   the bottom edge, where furniture belongs, and that asymmetry is what keeps the
   screen from reading as a poster template. */
.colophon {
  min-height: 100svh;
  display: grid;
  grid-template-rows: minmax(0, 1fr) auto;
  align-content: stretch;
  gap: clamp(2rem, 6svh, 3.5rem);
  padding: clamp(3rem, 9svh, 6rem) var(--gutter) clamp(1.8rem, 5svh, 3rem);
}
.colophon__card {
  display: grid;
  justify-items: center;
  align-content: center;
  gap: clamp(1.1rem, 3.2svh, 2rem);
  text-align: center;
}
/* Smaller, at the client's ask — 260 → 210 at the top and 170 → 160 at the floor.
   THE FLOOR IS NOT A PREFERENCE. The channel between the counter and the wedge is
   5.3 of the mark's 333 units and needs about 2.4px to read as a line rather than a
   grey needle, which is 150px of width. 160 leaves 2.54px, and 14vw puts 1280 at
   2.85 and 1920 at 3.34. Anything under 150 here and the whole reason this screen
   exists is gone — see the note in index.html and the ladder it came from. */
.colophon__logo { display: block; width: clamp(160px, 14vw, 210px); color: var(--ink); }
.colophon__logo svg { display: block; width: 100%; height: auto; fill: currentColor; }
/* A BAR, not a point. The first version read as a dot with a whisker — 「下の黄色の
   金色のバーが消えちゃったよ点になってるよ」 — and there were two reasons, both mine:
   at 6.5rem it was 104px against the brief's 144, and its gradient fell to 0.15
   over that short length, so almost all of its ink was in the first third. On top of
   that the light on its end was a hard 3px circle at full amber, which is a point by
   definition, sitting next to a bar that had faded out. The eye kept the point.

   So: longer, and the left 44% holds full amber before it falls away — the fade is
   the bar's end, not most of the bar. The end light is the brief's own treatment, an
   8px radial glow rather than a disc, so it reads as the light the line was drawn
   from instead of as a bullet. */
/* ABOUT'S TREATMENT AT THE LENGTH THAT WAS RIGHT. Two separate notes from the
   client, and they pull in different directions until you see which part of the bar
   each one is about: 「このAbout Section のこのバーぐらいの感じでいい…今のちょっとなんか画が
   強い」, then 「いや 消えてない？ そして線の長さ的にはさっきので良かったのよ」.

   So the length was never the problem and neither was the thickness. What made it
   画が強い was the light on its end: a 7px radial glow is a shape in its own right,
   it does not sit on a line, it competes with one — and it was the reason the bar had
   been pushed to 2px in the first place, to survive next to it. The pair had become a
   piece of graphic design under the mark rather than a rule.

   So: the glow is gone, ABOUT's gradient exactly (amber falling to nothing, no held
   section), and 160px — the length the client confirmed. The quietness came from
   removing the graphic, not from thinning the line.

   AND 1PX AFTER ALL — 「まだちょっと強いかな」「幅をちょっと狭めてみる?」. This is the third
   attempt at one pixel here and the reason the first two failed is the useful part,
   because neither was about the thickness:

     · 1px at 160px WITH the 7px glow beside it. The glow was the brighter object and
       the line lost to it.
     · 1px at 352px with no glow. The same gradient spread over more than twice the
       run, so it reaches half opacity at 176px instead of 80 and every pixel of it is
       fainter. Measured: 352px carried 280 amber pixels, 160px carries 130. Fewer in
       total, brighter each.

   Short and thin reads; long and thin does not. Rendered against 2px at full amber,
   2px at 0.6 and 1px in the same frame to confirm this is the lightest of them and
   still a line.

   …and then back to 2px, because 1px vanished on the client's screen for the third
   time: 「1ミリやと見えなくなったわまた」. Three observations by the person actually looking
   at it settles it — in this room, on a 1920x1080 panel at DPR 1, a one-pixel amber
   line alone on an empty screen does not register. 2px is the floor here and it is not
   negotiable by argument.

   THE LENGTH IS THE LEVER, and that is what he reached for: 「線の幅じゃなくて160を
   もうちょっと縮もうかと思ってんけど」、then 「132ぐらいにして」. 160 → 112 → 132px, and the
   ink tracks it directly: 254 amber pixels, then 182, now 214. Every one of them as
   bright as it ever was, because a shorter run reaches each step of the same gradient
   sooner rather than dimming anything. That is the one adjustment on this element
   that makes it quieter without making it fainter, and it is why length was the
   usable dial and thickness was not — thinning it cut brightness per pixel, which is
   why 1px vanished rather than receded.

   132px is 0.63 of the mark's width at 1920 and 0.57 at 1440 and 1280, so it stays
   that mark's underline at every width rather than becoming a horizon under it. */
.colophon__rule {
  display: block;
  width: clamp(4rem, 8vw, 8.25rem);
  height: 2px;
  background: linear-gradient(to right, var(--amber), rgb(255 181 74 / 0));
}
.colophon__name {
  font-family: var(--font-display);
  font-size: clamp(1.05rem, 1.8vw, 1.5rem);
  font-weight: 300;
  letter-spacing: 0.08em;
  line-height: 1.5;
  color: var(--quiet);
}
.colophon__name span {
  display: block;
  margin-top: 0.5em;
  font-family: var(--font-jp-display);
  font-size: var(--text-plate);
  letter-spacing: 0.2em;
  color: var(--dim);
}
.colophon__foot {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  flex-wrap: wrap;
  gap: 0.8rem clamp(1.5rem, 4vw, 3rem);
}
.colophon__nav {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem clamp(1.2rem, 3vw, 2.4rem);
}
/* --quiet, one step up from the © line beside it. Both pass AA, but this one can
   be clicked and the other cannot, and at the bottom edge of a very dark screen
   that difference has to be visible without being loud. */
.colophon__nav a {
  font-size: var(--text-plate);
  letter-spacing: var(--track-plate);
  color: var(--quiet);
  transition: color var(--dur-micro) var(--ease-out);
}
/* colour only, same correction as .masthead__nav a:hover above: opening the
   tracking on hover is a layout change (letter-spacing adds after every
   glyph including the last), and on this row it pushed every link to its
   right by ~2px — visible — to buy ~0.44px of extra space per gap at 11px,
   which is not. */
.colophon__nav a:hover { color: var(--ink); }
.colophon__legal {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.4rem clamp(1.2rem, 3vw, 2.2rem);
}
.colophon__line {
  font-size: var(--text-plate);
  letter-spacing: var(--track-plate);
  color: var(--dim);
}
.colophon__privacy {
  font-size: var(--text-plate);
  letter-spacing: var(--track-plate);
  color: var(--quiet);
  border-bottom: 1px solid rgb(151 145 127 / 0.35);
  padding-bottom: 0.15em;
  transition: color var(--dur-micro) var(--ease-out),
              border-color var(--dur-micro) var(--ease-out);
}
.colophon__privacy:hover { color: var(--ink); border-bottom-color: var(--ink); }

/* ═══ static composition · the four panels ══════════════════════════════
   Below 48rem all four are a single column and share one shape, because at
   375px there is no room for four shapes. Above it, and in the conveyor,
   they are four. */
@media (min-width: 64rem), (min-width: 48rem) and (pointer: fine) {
  /* the same containment the conveyor gets from contain:paint. Two of these
     compositions bleed off an edge — 01 to the left, 04 downward — and in a
     column those edges are the next panel rather than the screen. */
  /* ── THE GRID ────────────────────────────────────────────────────────
     Twelve columns, the page's own gutter on both sides, one gap. Every panel
     draws on it and no panel uses all of it the same way, which is the whole
     point: 「雑誌、おしゃれな雑誌のよう、1ページのようなのを4つやりたい」.

     Before this the four were laid out on four unrelated systems — 1fr/44%,
     a single centred cell, 38%/1fr, and a bleed with everything hung off
     --gutter margins. Each read as its own object, which is the 「アートっぽく
     かなり違和感のある」 the client is describing. Measured at 1440, the text
     blocks started at x 72, 991, 619 and 1004: four different left edges, none
     of them related. On the grid below they start at 72, 952, 732 and 842 —
     all four on a column line, and the reader's eye has something to hold.

     What is deliberately NOT unified: which columns each panel uses, where its
     block sits vertically, and which edge it hangs from. Four pages of one
     magazine, not one page four times. */
  .work__panel {
    display: grid;
    padding-inline: var(--gutter);
    overflow: clip;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    column-gap: var(--grid-gap);
  }
  /* Bleeds leave the grid rather than distort it: a plate that runs to the
     screen edge cancels the panel's own padding on that side and keeps its
     inner edge on a column line. */
  .work__panel .bleed-l { margin-left: calc(-1 * var(--gutter)); }
  .work__panel .bleed-r { margin-right: calc(-1 * var(--gutter)); }

  /* ── THE INTRO IS A STACK, NOT A MASTHEAD ──────────────────────────────
     A prior pass put the heading and the lede side by side on one row (label,
     heading+lede sharing a baseline) to save vertical space. The client flagged
     it as looking misaligned: the lede sat in column 6, on neither the heading's
     column nor the cards' text column, and 11px off the heading's own baseline.
     Reverted to the shape the client actually named: label, heading, paragraph,
     stacked, ranged left on the same column.

     Same twelve columns the panels draw on. The plate label holds its own row
     across the full measure; the heading and the lede now share the heading's
     column span (1–5) on consecutive rows instead of splitting the measure
     left/right. row-gap is already 32px here (see below) — the same figure the
     un-gridded stacked rule states via --space-lg — so no extra margin is
     needed between them. */
  .work-head {
    display: grid;
    grid-template-columns: repeat(12, minmax(0, 1fr));
    column-gap: var(--grid-gap);
    row-gap: calc(var(--space-md) * 2);
    align-items: baseline;
  }
  .work-head .plate { grid-column: 1 / -1; grid-row: 1; }
  .work-head__h     { grid-column: 1 / 6;  grid-row: 2; margin-top: 0; }
  .work-head__lede  { grid-column: 1 / 6;  grid-row: 3; margin-top: 0; }

  /* 01 · THE NUMERAL BRIDGES. It used to stand at the top of columns 1–6 with the
     words at the bottom of the same columns — one left-hand stack, and the numeral
     was the loneliest thing on the page: nothing to its right for 300px, then a
     photograph. The spec's note is exactly that, 「01を左端に単独で置き、他要素と
     離れさせない」, and the reference puts the figure between the words and the
     picture where it holds the two together.

     So the left column is now three rows — air, numeral, words — bottom-anchored,
     and the numeral is pushed to the RIGHT edge of its span so its last figure
     stops one gap short of the plate's left edge. Read left to right the panel is:
     words at the foot, the numeral rising out of them, the photograph starting
     where the numeral ends. Nothing is isolated.

     Bottom-anchored and not centred, because the slack has to go somewhere and at
     1920 a centred stack puts 200px of nothing between the figure and the words —
     the exact fault this replaces. At 1440 the top row is ~73px, at 1920 ~218px;
     the numeral-to-words relationship is identical in both. */
  .wp1 {
    grid-template-rows: minmax(0, 1fr) auto auto;
    min-height: 30rem;
    /* the smallest of the four, and measured off the reference rather than
       guessed: 01 is 14.3% of the mock's frame width where 02 is 25% and 04 is
       21.5%. It is also the one figure that has to fit BETWEEN two things — the
       25svh term is what keeps its box (0.74 × --no) plus the words plus the
       counter clearance inside a short laptop screen. At 1440×900 the row above
       it is 78px; at 1440×800, 22px. */
    /* Smaller than it was, and the trade is deliberate: the client's complaint on
       this card is 「画像の黒い構造物の強さに対して、左側テキストが細く小さい」, so the ink
       goes to the words. 17vw/25svh was 225px at 1440×900 against a 30px lead;
       15vw/17svh is 153px against a 55px lead, and the ratio of figure to line goes
       from 7.5:1 to 2.8:1. It is still the smallest figure of the four, which it
       always was — 01 is the one that has to fit BETWEEN two things rather than hold
       a flank, and it is the only one now sharing a column with a 392px text block.

       17svh and not something larger because the binding screen is 1440×800, not
       1440×900: the panel is 588px there, the block is 385, and the counter overlay
       fixes the bottom padding at 72. That leaves 131 for the figure's box; 17svh
       asks for 101 and the remaining 30 is the standing air the composition needs
       above it. 153 / 136 / 161 / 184 at 1440×900 / 1440×800 / 1835×950 / 1920×1080. */
    --no: clamp(4.5rem, min(15vw, 17svh), 13rem);
  }
  .wp1 .work__no {
    grid-column: 2 / 7;
    grid-row: 2;
    justify-self: end;
    align-self: end;
  }
  /* BACK TO 7/-1 — 732 → 1440, 49% — and the reason is the client's, not the old
     spec's. 「画像の黒い構造物の強さに対して、左側テキストが細く小さい」 has two halves and
     the previous pass only answered one of them: the words got bigger, and the
     photograph kept 57% of the screen. The text block needs five columns to carry
     a 55px lead at all (9.36em = 518px against a 526px column), and five columns
     starting one column in from the margin end at 708. So the plate starts at 732,
     one gap later, and the two are as close as the grid allows. */
  .wp1__plate { grid-column: 7 / -1; grid-row: 1 / 4; align-self: stretch; min-height: 22rem; }
  /* THE JOINT COMES IN OFF THE RIGHT WALL. The crossing in this photograph sits at
     48% × 48% of the file, and the plate's box is 1.24:1 against the file's 1.33:1
     — cover crops 62px of width and nothing else, so at object-position 50% the
     joint lands at x 1013 of 1440. That is the far right of the frame.

     transform-origin is what moves it. The conveyor already scales this image
     1.04 → 1.26; taking the origin off centre and out to 78% means the growth
     pushes content leftward, and the joint lands at ~949 (66% of the screen)
     instead. Checked for uncovered corners at both ends of the scale range and at
     the ±24px the parallax adds: left edge −58 → −205, right edge 857 → 904, box
     818. Never uncovered.

     The base transform below is the conveyor's own resting value, so the still
     frame and the .js resting frame are the same picture. */
  /* the origin moves with the box. At 78% on the old 818px plate the joint landed
     at x 949 of 1440; on a 708px plate the same 78% puts it at 1007, which is back
     out at the right wall this number exists to keep it off. 84% brings it to ~996
     and holds the same relationship to the frame. Re-checked for uncovered corners
     over the conveyor's whole 1.04 → 1.26 range with the ±21px of parallax on top:
     painted content spans element-x −129 → 813 at the far end of the scale against
     a 708 box, so there is 129px of cover on the left and 105 on the right where
     21 is needed. Never uncovered. */
  .wp1__plate img {
    object-position: 52% 48%;
    transform-origin: 84% 50%;
    transform: scale(1.26);
  }
  /* the bottom padding is clearance for the section counter, which under .js is an
     overlay on the panel's own bottom-left corner rather than a row of its own — see
     the conveyor. Without .js it just reads as margin. */
  /* 1/5 → 2/7: five columns, and one column in from the page margin. Both halves
     of 「テキストブロックを少し上・少し右へ寄せて、画像との距離感を詰める」 — 110px right,
     and the block's right edge now lands 24px off the photograph instead of 134px.
     The lead's first line is nine glyphs and reaches 700 of that 708, so the words
     and the figure above them share one right edge against the plate, which is the
     「01の数字は視線の起点として良いが、少し関係性を整理したい」.

     The padding-top goes: the 1fr row above the numeral is the air, and 24px on top
     of it was air inside air. The bottom padding is 9svh where it was 11, and that
     is not the lift either.

     THE LIFT IS THE BLOCK'S OWN HEIGHT. The stack is bottom-anchored, so the words'
     top edge is panel − padding − block, and the block went from 277px to 392px: at
     1440×900 its top edge rises from 296 to 206, and the numeral's from 130 to 93.
     90px and 37px, on a card whose instruction was 「少し上」. The padding could not
     rise with it — the counter overlay puts a floor of about 70px under it and the
     panel only has 588px to spend at 1440×800 — so the number came down while the
     thing it positions went up. */
  .wp1 .work__text {
    grid-column: 2 / 7;
    grid-row: 3;
    align-self: start;
    padding-bottom: clamp(3rem, 9svh, 7rem);
  }

  /* 02 · THE ONE CARD WITH NO PHOTOGRAPH, and it is now the mirror of 01 rather
     than a centred numeral with right-set words. Words left of centre, the hollow
     figure on the right, and five drawn lines crossing both. The words were set
     ragged-right in columns 9–12 before; four cards where three are ranged left
     and one is ranged right is a card that fell out of the run, not a card with a
     different composition. Every card in this section now reads from the same left
     edge of its own block. */
  .wp2 {
    position: relative;
    grid-template-rows: minmax(0, 1fr);
    min-height: 30rem;
    --no: clamp(7rem, min(26vw, 44svh), 21rem);
  }
  /* LEVEL WITH 03, WHICH IT WAS NOT. 03's outline was rebuilt last round and this
     one was left where it stood, so the run shipped two hollow figures a stop and a
     half apart — measured on the client's own screen, 02 read 最明 131 / 点灯画素平均
     43.3 against 03's 154 / 99.7. Both halves of that gap have a cause and both are
     fixed here.

     THE WIDTH is why the means differ by 2.3×. -webkit-text-stroke centres on the
     path, so a straight edge is only fully covered, device pixel for device pixel,
     when the half-width clears a whole pixel either side of it — the floor is 2px,
     and at 1.2px most of this figure's ink was partial coverage. 2.4px is 03's own
     number and clears it with margin.

     THE ALPHA is solved for luminance rather than copied, because the two are not
     the same ink. Composited on --void at alpha a, Rec.709:

       03  --ink   #f4f2ea   L = 231.9a + 10   →  153.8 at 0.62
       02  --amber #ffb54a   L = 179.0a + 10   →  153.7 at 0.80

     so 0.80 is where amber carries exactly the light 03's white carries — 7.0:1 on
     --void against 03's 7.02:1, up from 2.19:1.

     The model is trusted over one of the two readings it is fitted to, deliberately.
     It reproduces the 03 measurement to within a level (155 predicted, 154 read),
     and it cannot reproduce the 02 one: a 0.34 amber stroke on #0a0a0a has a ceiling
     of 93 in its own brightest channel and 64 in luminance, so a reading of 131 is
     above what that stroke can physically put on a screen and is instrument-side —
     a wide-gamut screenshot read back as sRGB will do it to saturated orange. If it
     turns out to read hot next to 03 on the real screen, 0.62 is the fall-back: the
     same alpha as 03 rather than the same light, L 121, 4.9:1. Same width, same luminance, same
     contrast; only the hue is different, which is the 「アンバーのままで」.

     0.8 leading rather than the run's 0.74 so the box hugs a figure this large. */
  .wp2__no {
    grid-column: 7 / -1;
    grid-row: 1;
    justify-self: end;
    align-self: center;
    line-height: 0.8;
    color: transparent;
    opacity: 1;
    -webkit-text-stroke: 2.4px rgb(255 181 74 / 0.8);
    pointer-events: none;
  }
  /* from 43% of the panel to its right edge, full height. The words bottom out at
     x ~488 at 1440 (the note is bounded by columns 1–5, not by its 33em cap) and
     the leftmost drawn point lands at 578, so the lines start 90px clear of the
     measure and run under and past the figure. */
  /* 43% → 52%. The text block moves one column right (below), so its right edge
     goes from 598 to 708 while the leftmost drawn point stood at 578 — the five
     curves would have started underneath the lead's own last glyph, and a 1px amber
     hairline crossing 55px of mincho is not a crossing, it is dirt. At 52% the box
     starts at 746 and the leftmost point lands at 711, three pixels clear of the
     measure. The figure still runs the whole right flank and still passes through
     and out the far side of the numeral, which is the part that matters. */
  .wp2__wire {
    position: absolute;
    inset: 0 0 0 52%;
    width: auto;
    height: auto;
    aspect-ratio: auto;
    margin: 0;
  }
  /* 「テキストブロックが左下に沈んで見えるので、もう少し上げて存在感を」. The 7% was a
     percentage margin, so it resolved against the panel's WIDTH — 91px of downward
     push at 1440, on a block that was already centred. It goes, and a bottom margin
     of the same order takes its place, which lifts the block by half of itself:
     measured at 1440×900 the top edge moves from 244 to 108.

     1/6 → 2/7 for the same reason 01 moved: five columns, one column in from the
     margin. The two cards that have no photograph on their left now share a left
     edge at 182, and the figure's own left edge is 285px away instead of 395 —
     the 「テキストとの関係がまだ弱い」. Vertically the two are one band: the words run
     108 → 480 and the figure 190 → 489, so the lines that cross the frame cross
     both of them. That is the 「静かな緊張感」 — one altitude, two weights. */
  .wp2 .work__text {
    position: relative;
    grid-column: 2 / 7;
    grid-row: 1;
    align-self: center;
    margin-bottom: clamp(3rem, 10svh, 6rem);
  }

  /* 03 · the paper plate off the left edge through column 7 — 0 → 818 at 1440,
     57% of the screen and the spec's 55–62%. The words hold the right at mid
     height in 8–11, column 12 stays empty, and the figure falls out of the
     bottom-right corner of the screen. Plate low-left, words high-right, figure
     low-right: the three make a triangle rather than a row, and the empty
     top-right corner is what keeps the card from reading as two halves. */
  .wp3 {
    position: relative;
    grid-template-rows: minmax(0, 1fr) auto;
    min-height: 30rem;
    /* 「数字ちょっと切れ過ぎ、全然見えへん」. The figure was 320px on every screen and a
       third of it stood outside the frame, so what was actually on the page was one
       flank of a 0 and a fragment of a 3. It is smaller and it is whole: 162px at
       1440×900, 144 at 1440×800, 171 at 1835×950, 194 at 1920×1080, and none of them
       cut.

       18svh is set by the short laptop, the same way 01's is. This card stacks the
       information plane on top of the figure, and at 1440×800 that is 429 + 115 + 28
       inside a 588px panel, which leaves 16px between the plane's bottom edge and
       the top of the figure. Anything larger and the plane starts eating it — which
       would fail safely, the plane is the one carrying the z-index, but a figure
       that is whole on one screen and clipped on the next is exactly the 中途半端
       this card is being fixed for. */
    --no: clamp(5.5rem, min(19vw, 18svh), 16rem);
  }
  /* THE FEATHER IS GONE, AND IT SHOULD NEVER HAVE BEEN HERE.

     Three passes lived in this slot — a gradient painted over the paper, then a
     straight alpha mask, then a 15% smoothstep mask — and all three were answers to
     a question the client had not asked. The note the third was written under said
     trimming was 「measured and is not available on this file」 and concluded the mask
     had to stay. The client's words on the built page were 「画像の端のグラデーション
     要らない、格好悪い」 and 「黒地と画像の境界に、硬いライン感・構図的な切れ味が欲しい」. So:
     a hard edge, and the step is answered by the composition rather than by a ramp.

     THE FILE HAS NO DARK SIDE — that half of the old note is true, and is re-measured
     here rather than inherited. paper.jpg is 1448×1086 and its column means run 169
     at the darkest (x 632) to 200 at the brightest (x 1400): there is no vertical
     band anywhere in it that could meet --void at 10. What it does have is a dark
     FOOT. The bottom eighth of the file runs 101–151, and the shadow under the front
     curl at y 75% × x 25–58% bottoms out at 90. So the trim brings the foot up — see
     transform-origin on the img rule — and the composition covers the rest.

     WHAT COVERS THE REST is the information plane on .wp3 .work__text below: a --void
     rectangle with hard edges laid over the plate's top-right corner, which is the
     client's own 「グラデーションでつなぐより、黒い情報面を重ねる」. It stands over 61% of
     this edge's height at 1440×900, and over that 61% the step is not reduced, it is
     absent — there is no edge there, because the photograph's silhouette is an L and
     not a rectangle. Measured over the 39% that is left, 12px either side of the cut:

                                          exposed edge   step vs --void
       naive un-feather, old crop            173.3          163.3
       old crop, plane over the top          125.8          115.8
       THIS: new crop + plane                119.2          109.2

     Weighted over the full height of the right edge — zero where the plane stands,
     109 where it does not — the mean step is 42.9. At 1835×950 the plane covers 63%,
     exposed 115.3, mean 43.0; at 1920×1080 it covers 52%, exposed 125.8, mean 60.4.

     要確認: 109 is still a hard step and it is meant to be one. This is what the edge
     of a lit photograph on a black page looks like with nothing smeared over it. */
  .wp3__plate { grid-column: 1 / 8; grid-row: 1 / 3; align-self: stretch; }
  /* cover crops almost nothing out of this box (1.24:1 against 1.33:1), so the
     「大胆にトリミング」 is done by the scale and the origin, not by
     object-position: origin at 30% means the enlargement pushes the sheet
     rightward, and the front curl's point lands at ~512 of the plate's 818.

     THE TRIM NOW DOES WHAT THE MASK USED TO. The scale is untouched at 1.18 — same
     enlargement, same sharpness, no upscaling bought to win a measurement — and only
     the two origins move. 82% vertically pulls the shadowed foot of the sheet up into
     frame: the visible window goes from file y 84–1002 to y 137–1056, so the darkest
     eighth of the file is on the screen and it is at the bottom of the plate, where
     the counter's scrim below finishes it. 14% horizontally slides the sheet 71 file
     columns right, which puts a darker column at the cut. Together they take the
     exposed edge from 125.8 to 119.2.

     Checked for uncovering, because origins this far off centre are how that happens.
     The conveyor drifts this image ±11px vertically (5% × 0.333 × --amp) and the
     painted content clears the box by 52px left / 199 right / 101 top / 22 bottom at
     1440×900, 27/166/157/67 at 1835×950 and 30/178/125/28 at 1920×1080. The tightest
     of those is 22 against 11. Never uncovered. */
  .wp3__plate img {
    object-position: 30% 54%;
    transform-origin: 14% 82%;
    transform: scale(1.18);
  }
  /* the paper runs to ~0.88 luminance and the counter overlay sits on this
     plate's foot, so the foot is put into shadow. It also grounds the band —
     lit paper standing straight on the screen's bottom edge would float.
     The right-edge fade used to live in this gradient, then in a mask on the plate;
     it does not live anywhere any more and this scrim is the bottom only. Its second
     job now is the foot of the hard right edge: with the sheet's own shadow trimmed
     into frame under it, the bottom fifth of that cut composites to 53 against
     --void's 10, where the unscrimmed part of the edge is 119. */
  /* The 0% and 12% stops are deepened and a stop is added between them; the 32%
     end is untouched, so the plate's own look above the bottom eighth is exactly
     what it was. What changed is the band the 01 — 04 counter actually stands
     on: it sits 38px off the pin's foot, which is 5.6% up a 680px plate, and at
     the old 0.9 → 0.5 slope that band let 70 of 255 of lit paper through. The
     counter is the one thing on the page that has to be read on all four cards
     (see .work__count's own note), so its ground is held under 45. */
  .wp3__plate::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(to top, rgb(10 10 10 / 0.94) 0%, rgb(10 10 10 / 0.78) 9%, rgb(10 10 10 / 0.5) 14%, rgb(10 10 10 / 0) 32%);
  }
  /* ── THE INFORMATION PLANE ─────────────────────────────────────────
     「大きい画像を左、テキストを右に置いただけのテンプレ感」 is an exact description of
     what 8/12 was, and no amount of type size fixes it: a rectangle of photograph
     beside a rectangle of words is two objects sharing a screen, and what the client
     asked for is 「1つの見開きのように感じる構図」 — one spread.

     So the words are not beside the photograph any more, they are ON it. This block
     takes a --void ground and starts at column 6, which is 196px inside the plate's
     818px width, and runs off the right edge of the screen. Where it crosses the
     photograph it removes it: the plate's silhouette stops being a rectangle and
     becomes an L, a corner cut out of it on two hard lines, with the words standing
     in the cut. That is 「黒い情報面を重ねる・または構図として切る」 — both of them — and it
     is the only device on this page that puts type and photograph on one surface.

     It is also what answers the right edge. The brightest part of that edge is its
     top: measured on the composed frame the top quarter reads 221 against 194 for
     the full height, and the plane is top-aligned, so the loudest 61% of the cut is
     the part that no longer exists. The plate's own note carries the numbers.

     The padding is the page's own gutter, which makes the plane a small page set to
     the same margin as the big one; the type inside it starts at 694 and its measure
     ends at 1368, the last column line. z-index because .wp3__plate is positioned,
     and this block is the one thing that has to be over it. */
  .wp3 .work__text {
    grid-column: 6 / -1;
    grid-row: 1;
    align-self: start;
    position: relative;
    z-index: var(--z-plate);
    margin-right: calc(-1 * var(--gutter));
    padding: var(--space-md) var(--gutter) clamp(1rem, 3.5svh, 2rem);
    background: var(--void);
  }
  /* IT COMES BACK INSIDE THE FRAME. The note this replaces was an argument for the
     cut — 「both hangs are roughly doubled and the mark now loses a third of itself
     to the corner, which is a decision the eye can see was made」 — and the client
     saw it and read it as an accident: 「数字ちょっと切れ過ぎ、全然見えへん」. A third of a
     glyph outside the frame is a decision on paper and a fault on a screen, so both
     hangs go and the figure stands whole in the corner of the black.

     9/-1 ranged right puts its right edge on column 12's own line at 1368, which is
     the line the information plane's measure ends on: the words and the figure share
     a right edge down the card, and the figure reads as the foot of that block
     rather than as an object that fell out of it. It stands entirely on --void — its
     left edge is 1167 at 1440×900 and the plate ends at 818 — so the stroke keeps the
     ground it was solved against and its contrast is exactly what it was.

     Nothing about the ink moves. 2.4px and 0.62 were set last round off the client's
     own screen, and they are now the reference 02's amber is levelled to; see the
     luminance solve in .wp2__no. */
  .wp3__no {
    grid-column: 9 / -1;
    grid-row: 2;
    justify-self: end;
    align-self: end;
    margin-bottom: clamp(0.75rem, 3.5svh, 2.5rem);
    line-height: 0.8;
    color: transparent;
    opacity: 1;
    /* ROUND TWO: 1.6px / 0.48 measured correct on paper (4.57:1, zero AA gaps
       on the straights) and still read as かすれ on the client's own screen.
       -webkit-text-stroke centres on the path, so a straight edge is only
       fully covered, device-pixel for device-pixel, when the half-width
       clears a whole pixel on both sides of it — that floor is 2px, not
       1.6px; 1.6px was proven to land ONE full pixel only under a favourable
       sub-pixel alignment, and a numeral this size crosses plenty of
       alignments that are not favourable. So both go up together: 2.4px
       clears the 2px floor with margin on the straights, and 0.62 is set from
       the client's own screen reading of 03 (brightest 121/255, lit-pixel mean
       53.4) rather than from the alpha alone — solved so a fully-covered pixel
       lands at 150+/255 (composite of --ink at a over --void: 244·a+10·(1−a),
       ≈153.6 at 0.62, matching the 121 the old formula predicted and the
       client measured almost to the pixel).

       0.62 puts the stroke at 7.02:1 on --void, up from 4.57:1. 02's amber
       hollow stays untouched at 1.2px / 0.34 (2.19:1) — 03 now reads
       distinctly stronger than 02, which is intended here (02 was not part of
       this round) and worth a second look once both are on the same visit.
       7.02:1 stays well under the two things it must not compete with: the
       solid figures on 01 and 04 are 14.5:1 and body --ink is 17.66:1. At
       320px a 2.4px hairline is 0.75% of the em, still inside Cormorant 300's
       own 3–4px thins — thicker than round one, still a fine outline rather
       than a bold one. */
    -webkit-text-stroke: 2.4px rgb(244 242 234 / 0.62);
  }

  /* 04 · the photograph takes the whole frame and the words stand in the dark
     water at its foot, bottom left. The figure holds the top right, which is the
     direction the stones are walking. Both of those moved: the words were bottom
     RIGHT and ranged right, which put them on top of the receding stones and away
     from the counter's own corner; the vertical Japanese name that used to run
     down column 1 is gone and back in the text block with the other three. */
  .wp4 {
    position: relative;
    grid-template-rows: minmax(0, 1fr);
    min-height: 30rem;
    --no: clamp(6.5rem, min(22vw, 40svh), 19rem);
  }
  /* bleed-r used to close this box's right edge by cancelling the panel's own
     padding, which reaches the screen edge only if the panel's own track is
     exactly the viewport's width — measured, at 1920 it is not, and the box
     came out 2131px wide against a 1920 screen (cover 1.472 where 01 and 03
     are 0.98–1.08). An explicit width in vw is grounded in the real viewport
     instead of the panel's track, so the right edge lands on the screen edge
     regardless. bleed-l is untouched — anchoring the left edge to the panel's
     own padding has measured correct on both 01 and 03. */
  .wp4__plate { grid-column: 1 / -1; grid-row: 1; width: 100vw; }
  /* BACK TO CENTRE, AND THE RECESSION WAS THE FAULT. 50% 0% was chosen to save
     the vanishing point — the box is 2.7:1 against the file's 1.33:1, so cover
     throws away 696px of height, and taking all of it off the bottom keeps the
     stone line running to its own end. It works and it is wrong: what it puts
     on the screen is the entire file, sixteen stones from a metre across down to
     a few pixels, and a repeat that long stops being a recession and becomes a
     pattern. The client's words were こんだけ石が並んでると気持ち悪い and 前の画像の
     方が雰囲気良かった, and the earlier frame he is remembering is this one.

     At 50% 50% the visible band is file rows 301–785 of 1086 (1835×950): six
     stones, near and large, standing in open water, with the tail of the line
     cut at the top edge — the card is about weight now rather than distance.

     The two things the change had to not break, both measured on the composed
     frame with all three scrims applied:

       the 04 numeral   its footprint is file cols 1103–1314 × rows 330–490,
                        which at 50% 0% was lit water and the small stones —
                        ground mean 66, and 3.39:1 at the brightest 5% of the
                        pixels under it, i.e. under AA. At 50% 50% the same
                        footprint is open water: ground mean 27, 8.81:1 mean
                        and 4.62:1 at the brightest 5%. The numeral is not
                        interfered with, it is better off.

       the light exit   the spec's 「次の明転セクションへつながる光の抜け（右奥または
                        上部）」 survives at the upper right: the top-right
                        quadrant of the composed frame reads mean 44, p95 110,
                        max 204 of 255. Dimmer than 50% 0% (mean 72) and still
                        plainly a lit passage — it is now lit ripple rather
                        than lit distance, which is the trade the six stones
                        cost. 50% 40% was measured as the middle option: eight
                        stones and a brighter exit, but the numeral falls back
                        to 3.41:1 on its bright pixels, so it was not taken.

     The 1.11 resting scale and everything about the conveyor are untouched. */
  .wp4__plate img { object-position: 50% 50%; transform: scale(1.11); }
  /* Three scrims, all of them for contrast rather than mood, and all of them
     deliberately off the top-right corner: that corner is the brightest water in
     the file and the spec asks for a light exit there toward the section that
     follows, which is the only lit thing on this page.
       1  a left pool, for the words
       2  a foot, for the words and for the 01 — 04 counter overlaid on it
       3  a soft veil behind the figure, tapered so it stops short of the corner

     Two of the three move, because the words and the figure moved. The left pool
     reaches further — 0 → 58% instead of 0 → 54%, and 0.6 at 30% instead of 0.5 at
     24% — because the lead is 55px now and its first line runs to x 596 where it
     used to stop near 350; the pool has to still be a pool where the line ends.

     The veil goes from 88% 34% to 85% 63%, following the figure down to the foot,
     and that is a straight gain on the spec's 「光の抜け」: the upper right corner is
     the brightest water in the file and the veil was the one thing dimming it. The
     exit is now unshaded. */
  .wp4__plate::after {
    content: "";
    position: absolute;
    inset: 0;
    background:
      linear-gradient(to right, rgb(10 10 10 / 0.88) 0%, rgb(10 10 10 / 0.6) 30%, rgb(10 10 10 / 0) 58%),
      linear-gradient(to top, rgb(10 10 10 / 0.62) 0%, rgb(10 10 10 / 0.2) 22%, rgb(10 10 10 / 0) 42%),
      radial-gradient(50% 44% at 85% 63%, rgb(10 10 10 / 0.42) 0%, rgb(10 10 10 / 0.15) 50%, rgb(10 10 10 / 0) 80%);
  }
  /* 「04の数字は目立つが、少し単独で浮いている」. It was top right and the words were
     bottom left — diagonally opposite corners of a full-bleed photograph, which is
     two marks on one picture and not a composition. They are now on ONE LINE: both
     are align-self: end and both take the same margin-bottom, so their boxes share
     a bottom edge at every viewport by construction rather than by a number that
     has to be kept in step. Read across the foot of the card it is words, dark
     water, figure — a horizontal, on the one card whose photograph is a horizontal.
     That is the 「画像の水平感・石の連なりを活かしつつ、左テキストエリアとの関係を強くする」.

     It also gives the light exit back. See the scrim note above. */
  .wp4__no {
    grid-column: 8 / -1;
    grid-row: 1;
    justify-self: end;
    align-self: end;
    margin-bottom: clamp(5.5rem, 15svh, 9rem);
    opacity: 0.82;
  }
  /* clears the counter overlay: the counter sits 36px off the foot at 900 and is
     ~30px tall. 12svh → 15svh lifts the block 27px further at 1440×900, which is the
     「もう少し上げて」, and the same value is what .wp4__no hangs from so the figure
     rises with it.

     The column is the one thing on this card that does NOT move. 「左側に暗い面積が
     大きい。そこを余白として成立させるなら、テキストの密度をもう少し上げる」 is an instruction
     to keep the dark left field and fill the block, not to slide the block into it,
     so 1/7 stays and the density comes from the type: 30px → 55px on the lead, 13px
     → 16px on the note, and a 26em measure so the note is a block and not a trailing
     line. The block is 372px tall against the 277px it was in the same 636px column. */
  .wp4__text {
    grid-column: 1 / 7;
    grid-row: 1;
    justify-self: start;
    align-self: end;
    margin-bottom: clamp(5.5rem, 15svh, 9rem);
  }
}

/* The 奥付's two columns stack at 62rem and not at 48rem, which is where the
   rest of the page's columns give up. The form is why: at 992px the two
   tracks are 441 and 391, and 441 less the label column and the 必須 tag
   leaves 269px of ruled line — the shortest a Japanese sentence can be typed
   on and still be re-read. Below that the form gets the whole measure. */
@media (max-width: 61.99rem) {
  .colophon__cols { grid-template-columns: minmax(0, 1fr); gap: clamp(2.5rem, 8svh, 4rem); }
}

/* the imprint's own label column narrows: 代表取締役 is five glyphs and 7.5em
   of an 11px label is 82px, which on a 375 screen leaves the value 213px and
   breaks 東京都千代田区… onto four lines */
@media (max-width: 47.99rem), (pointer: coarse) and (max-width: 63.99rem) {
  /* 120 AND THE 150 FLOOR ABOVE BOTH HOLD, because that floor is a DPR-1 number.
     The mark's aperture channel is 5.3 of the glyph's 333 units, so it renders at
     width x 0.0159: 2.39px at 150, 1.91px at 120. The desktop finale exists to
     keep that channel over about 2.4 CSS px on the client's own 1920x1080 screen,
     which is DPR 1 — one CSS px is one device px there and the channel closes.

     A phone is not DPR 1. At DPR 2 the same 120px gives the channel 3.8 device
     pixels and at DPR 3 it gives 5.7, both well clear. What 160 costs on a 390
     screen is 43% of the width and about a third of the height, which is what the
     client saw: 「最後のGが大きすぎる」. So the mark comes down here and only here.

     Previewing the mobile layout in a desktop window will show it slightly soft —
     that is the DPR-1 artefact, not the phone. */
  /* A phone does not need to be told it can scroll — the gesture is the medium.
     The cue is a desktop affordance and the client asked for it off here, text
     and travelling light together. Removing the element takes the SCROLL label,
     the rail and hero-scroll-drop with it in one line. */
  .hero__scroll { display: none; }
  .colophon__logo { width: 120px; }
  .colophon__row { grid-template-columns: 6.2em minmax(0, 1fr); gap: 0 var(--space-sm); }

  /* the form's label comes up out of the left column and sits over its own
     rule, with the 必須 tag on the far end of that same line. At 375 the
     measure is 337px, and a 6.9rem label column would have left 186px of
     input — which is a text field, but お問い合わせ内容 does not fit in the
     label column at all, so the row would have been ragged as well as tight. */
  .close-field__row {
    grid-template-columns: minmax(0, 1fr) auto;
    align-items: baseline;
    padding-top: 0.35rem;
  }
  .close-field__label { grid-area: 1 / 1; padding: 0; }
  .close-field__tag   { grid-area: 1 / 2; padding: 0; }
  /* the ground keeps its own padding on both edges here. The 0.15rem top this
     replaces was written for a field that was a rule rather than a surface — a
     surface with 13px under the text and 2px over it reads as a mistake. */
  .close-field__in    { grid-area: 2 / 1 / 3 / 3; padding: 0.82rem 0.95rem; }
  .close-field__err   { margin-left: 0; }
  /* the desktop indent is to the field column, and at 375 there is no field
     column — the label sits above the input, so both of these start at the
     measure's own left edge. The button would also have overflowed: 6.9rem of
     indent out of a 337px measure leaves 211px for a button that needs 220. */
  .close-privacy { margin-left: 0; }
  .close-send { margin-left: 0; width: 100%; justify-content: center; }
}

/* ── the four panels on a phone ──────────────────────────────────────────
   The column stays a column; what changes is that each card keeps its own crop
   and its own plate proportion instead of the one 4:3 box all three shared. The
   spec's requirement is 「PCと同じトリミングを無理に再現せず、各カード専用の
   object-position」 — a 4:3 box against a 4:3 file crops nothing at all, so the
   old rule was three uncropped photographs in a row. */
@media (max-width: 47.99rem), (pointer: coarse) and (max-width: 63.99rem) {
  /* THE FOUR CARDS BECOME ONE SHAPE HERE. Stacked, a phone never has two panels
     on screen together, so the desktop's whole point — four different
     compositions, each with its own numeral position — stops reading as
     variety and starts reading as disorder: the client's own words were
     「どれとどれが紐付いてるのかが全くわからない」, and 03's numeral is the proof —
     it sat in DOM order after 03's own body copy and before 04's photograph,
     so a reader scrolling down met a number that belonged to the card above
     the one it looked attached to.

     display:flex + order fixes this without touching a single element in the
     HTML: the three DOM children each panel already has — the numeral, the
     photograph-or-wire, and .work__text — are simply told where to stand.
     Numeral first (order 1) on all four, so it reads as the card's own
     heading rather than a mark buried partway down. .work__text straight
     after it (order 2), so nothing — no photograph, no wire — sits between
     the numeral and the service name it labels. The photograph/wire moves to
     the close (order 3), now a supporting image under the reading copy
     rather than the thing that used to separate the numeral from its name. */
  .work__panel { display: flex; flex-direction: column; }
  .work__text { order: 2; }
  .wp2__wire { order: 3; margin-top: var(--space-lg); }
  .wp1__plate, .wp4__plate, .wp3__plate { order: 3; margin-top: var(--space-lg); margin-bottom: var(--space-lg); }
  /* 5:4 crops 11% off the sides of a 4:3 file and object-position centres what is
     left on the crossing at 48% × 48% */
  .wp1__plate { aspect-ratio: 5 / 4; }
  .wp1__plate img { object-position: 52% 46%; }
  /* 4:3 is the file's own shape, so the framing here is object-position's alone:
     down and left, onto the front curl and the shadow under it */
  .wp3__plate { aspect-ratio: 4 / 3; }
  .wp3__plate img { object-position: 46% 52%; }
  /* the one portrait plate on the phone, and the only card that gets one. 4:5
     against a 4:3 file shows 47% of the width, so the stone line runs top to
     bottom of the box instead of corner to corner.

     "The recession survives the narrow screen" is what this note used to say
     next, and that is exactly what had to go — the phone was carrying the
     desktop's own fault twice over. A portrait box against a landscape file
     crops the WIDTH only: every one of the file's 1086 rows is on the screen at
     any object-position, vanishing point included, so the 12% in the pair below
     was inert and what the phone showed was all eighteen stones. The desktop
     card is six now (see .wp4__plate img above); leaving the phone at eighteen
     would ship the client's complaint on the smaller screen.

     There is no object-position that fixes it and no portrait aspect-ratio that
     fixes it either — nothing narrower than 4:3 crops a single row. A scale is
     the only crop available on this axis, and it is the device this section
     already uses for exactly this job: 03's own 「大胆にトリミング」 is a scale and
     an origin rather than an object-position, for the same reason. Origin at
     the foot so the enlargement throws away the top of the file, which is where
     the tail of the line is, and at 66% across so the horizontal framing stays
     where it was. 1.35 leaves file rows 282–1086 × cols 531–1174: seven stones,
     the far tail gone, and the lit water still standing in the upper right.
     Resting only — the conveyor is off below this breakpoint. */
  .wp4__plate { aspect-ratio: 4 / 5; }
  .wp4__plate img {
    object-position: 66% 50%;
    transform-origin: 66% 100%;
    transform: scale(1.35);
  }
  /* 六's spec: 「04の文字には必要最小限の暗いグラデーションを追加」. It was `content:
     none` — no scrim at all. The words are under the plate rather than on it at
     this width, so this is not a legibility scrim; it is the minimum needed to
     stop a lit photograph ending on a hard horizontal line directly above 13px of
     reading copy. */
  .wp4__plate::after {
    content: "";
    position: absolute;
    inset: 0;
    background: linear-gradient(to top, rgb(10 10 10 / 0.72) 0%, rgb(10 10 10 / 0.18) 22%, rgb(10 10 10 / 0) 44%);
  }
  /* every numeral is in normal flow here and has a margin under it, so none of
     them can reach the words: the overlap the spec warns about is a positioned-
     numeral problem and there are no positioned numerals below 48rem.

     THE SIZE UNIFIES TOO, and stops being the numeral this run built for a
     screen where you see one card at a time. 94px (01/03/04) against 164px
     (02) was two sizes doing the same job three different ways once the four
     are a column instead of a held frame — the client's own read. Small and
     first is the one lockup that cannot be misread scrolling past it: 48–56px
     puts it at label weight, order 1 puts it at the head of every card, and
     --space-2xs under it (the same 4px .work__jp already sits off .work__en
     with) binds it to the name directly below as one unit — "01" then
     "SECRETARIAT OPERATIONS" with nothing of another card's between them. */
  .work__no { order: 1; font-size: clamp(3rem, 8vw, 3.5rem); line-height: 1; margin-bottom: var(--space-2xs); }
  /* the two hollow figures stay hollow at this width — it is the run's own rule
     that 02 and 03 are outlines and 01 and 04 are solid, and a phone that fills
     them in would be a fifth composition */
  /* the same levelling the desktop gets, solved at the phone's own pair. 03 goes to
     1.2px so the two are drawn with one width, and its 0.38 is then the reference:
     L = 231.9 × 0.38 + 10 = 98.1, and amber reaches the same light at
     (98.1 − 10) / 179.0 = 0.49. The strokes stay hairlines even at the smaller,
     unified size — 1.2px on a 48–56px figure reads no bolder than it did on the
     old 94px one, only smaller — so both keep the phone's 1.2. Size and
     line-height now come from .work__no above; only the fill and the stroke
     are this pair's own. */
  .wp2__no {
    color: transparent;
    opacity: 1;
    -webkit-text-stroke: 1.2px rgb(255 181 74 / 0.49);
  }
  .wp3__no {
    color: transparent;
    opacity: 1;
    -webkit-text-stroke: 1.2px rgb(244 242 234 / 0.38);
  }
  /* three lines instead of five — the two furthest back go. At 390 the drawn box
     is 351 × 292 and five curves in it read as a tangle rather than as a mark.
     What is left is the two long sweeps and the chord between the junctions, so
     the figure still crosses and still has a node to cross at. */
  .wp2__wire--x { display: none; }
  /* 16px, and the cap is what goes rather than the size. --wd-note's own floor is
     15px and that is the brief's floor too, but a phone is the one place the note is
     the only thing being read — there is no photograph beside it and no figure over
     it — so it takes the full 1rem the desktop takes. The measure is the screen's:
     26em at 16px is 416px and a 390 screen has 350, so the cap would never bind and
     stating it would only be a number that does nothing. */
  .work__note { max-width: none; font-size: 1rem; line-height: 1.9; }
}

/* the constellation is a fixed figure in percentages, and below 48rem those
   percentages put nine labels into 375px. It is re-spaced rather than
   re-flowed: the same nine points, pushed apart vertically and pulled in
   from the right, so it is still a constellation and still not a row. */
@media (max-width: 47.99rem), (pointer: coarse) and (max-width: 63.99rem) {
  /* On a phone the statement comes out of the right margin and is set
     horizontally under the figure instead. Kept vertical it would have taken
     24vw of a 375px screen and left the star chart a 197px square — nine
     labels in a box the size of a coaster. Given the full width the square
     is 343px, which is the whole point of the section surviving the phone. */
  /* .constel and .field used to be re-positioned and re-sized here, from before
     .cam existed and held the box. Left in, it de-registered the figure from its
     own network on every phone: measured at 375x812, .cam came out 305 x 305 and
     these two came out 160 x 361 and 153 x 343, because `inset: 0` was still
     supplying right and bottom while this supplied left, top and height. A
     square viewBox in a 160 x 361 box letterboxes, and the phrases are placed
     off `left: var(--x)` as a percentage of that box, so the lines and the words
     they join stopped agreeing with each other. The box belongs to .cam; the
     phone only changes how big it is and where it sits.
     Bigger than the viewport on purpose. A phone gets a CROP of the figure, not
     a shrunk whole: 150vw is 480 at 320 and 562 at 375, which puts the Japanese
     at 18.1px and 21.2px instead of the 10.6px and 12.7px a box that fitted the
     screen would give it. The client's requirement for the resting state is that
     the Japanese is readable, and 10.6px is not. */
  .connect { --box: min(calc(100svh - 8rem), 150vw); }
  .cam { top: 40%; }
  /* .ln-glow still scales with the box, so it still needs its own bump here.
     .ln-core used to carry three bumps of its own in this block, removed when
     this register went to vector-effect: non-scaling-stroke on the reasoning
     that a fixed on-screen width made them redundant. That vector-effect has
     since been reverted (see .ln-core path above — it broke stroke-dasharray
     on the quiet register), which puts .ln-core back to scaling with the box
     here too, same as .ln-glow. The three original bump values were not
     recorded anywhere this pass can recover them from, so they are not
     restored here — flagged in the report rather than guessed at.
     RECOVERED. git show 6cd3088 has them: 0.28 / 0.4 / 0.18, the same three
     bumps this block carried before the vector-effect went in. Restored below,
     so the phone's core lines are back to the weight they were drawn at. */
  .ln-glow path { stroke-width: 1.4; }
  .ln-glow path.ln--m1 { stroke-width: 1.9; }
  .ln-glow path.ln--m3 { stroke-width: 1; }
  .ln-core path { stroke-width: 0.28; }
  .ln-core path.ln--m1 { stroke-width: 0.4; }
  .ln-core path.ln--m3 { stroke-width: 0.18; }
  .connect { --plate-bottom: clamp(1.2rem, 3.4svh, 2rem); --brief-in: 0rem; --brief-up: 0rem; }
  /* On a phone the client's four line breaks are dropped rather than kept. The
     longest of the four lines is 26 characters; at 320 minus two 1.25rem
     gutters that is 280px, which needs a 10.7px body to hold on one line, and
     10.7px of Japanese is not a reading size. Wrapped as an ordinary paragraph
     at 13px it comes out six lines and every one of them is readable, which is
     the criterion. The composition is the thing given up, and it is the right
     one to give up at this width. */
  /* On a phone the copy runs the full width of the screen — the brief is 280 of
     320 — so there is no corner to solve and the soft-cornered box and the left
     pool are both replaced by one bottom band. Measured: the block's top edge is
     268px off the bottom at 320×640 and 254px at 375×812; --plate-bottom + 18rem
     gives 307 and 320. */
  /* THE BOTTOM BAND SHRINKS, BECAUSE WHAT IT WAS PROTECTING IS GONE. It was
     solid --void to --plate-bottom + 18rem and only clear by + 24rem, which on
     an 844 screen is opaque for 317px and still dimming at 413 — measured by
     hiding the scrim and diffing, the figure changed from y=460 down, so 45% of
     the network was under it. That band was sized for the CONCEPT copy block,
     and 六's own instruction deleted that block: the section now carries only
     「02 / RELATIONAL FIELD」 and 「MAG. 1—3」, both inside 95px of the bottom.
     A band for two corner labels needs to be a band for two corner labels.

     The client read the moving edge as a line rising over the figure, which is
     what it is: the scrim is fixed to the pin while the figure pans down by
     --a2 * 23.6%, so relative to the network the edge travels up.

     Same reasoning applies to the desktop scrim (it still covers from y=480)
     but desktop is signed off for the show and is left alone — recorded. */
  /* ONE RAMP, at the top, and the bottom band is gone rather than shortened.
     Shortening it was the wrong fix and the client saw straight through it: a
     band with a ramp has an edge wherever you put the ramp, so moving the ramp
     moves the line.

     What the measurement actually says, at 390x844 with the scrim hidden:

       --q      .field box        the band's ramp
       0        45 .. 630         692 .. 765
       0.3      49 .. 592
       0.7     135 .. 486
       1       225 .. 389

     The figure's own box bottoms out at 630 at the very start of the pin and
     only climbs from there. The band began at 692. It never touched the figure
     at any scroll position — 62px clear at the closest — so it was darkening
     nothing, and .js .field-scrim holds the whole layer at opacity 0 until --q
     0.05 so there is no earlier position to worry about either.

     What it did do was band. A translucent --void layer over a --void ground
     comes back one level dark: measured 9 against 10 across the full width for
     73 rows, in 8px stripes. That is 8-bit rounding in the compositor and not
     something a colour fixes — terminating the gradient at rgba(10,10,10,0)
     instead of the transparent keyword changes nothing (checked, identical to
     two decimals), and lifting the colour to rgb(11,11,11) makes it worse
     (9..11 instead of 9..10). The only way to not have the artefact is to not
     paint the layer.

     The two corner labels do not need it. On bare --void they measure 5.80:1
     and 6.49:1 — the band was never what made them readable.

     The top ramp stays: .field's top is 45 at --q 0 against a masthead that
     occupies 0..92, so that one is load-bearing. Its own tail still flips ±1 at
     y104..108, four rows under the masthead, and that is recorded rather than
     chased — same 8-bit floor, a fifth of the reach, and not over the figure. */
  .field-scrim {
    background: linear-gradient(to bottom, var(--void) 0, transparent 7rem);
  }
  .field-scrim::before { display: none; }
  /* On a phone the legend crosses to the other corner of the brief's own row.
     At the left edge it has no scrim to stand on — the phone scrim is one bottom
     band, not a left pool — and the figure's left flank is cut off there, so it
     sat among the tails of 「現場と全体。」and 「依頼と成果。」. Measured at 320
     and 375 both. In the bottom band it has the same clear ground the copy has,
     it stays vertical, and it stays diagonally opposite the FIG. line the way it
     is diagonally opposite it on a desktop. */
  .constel__legend {
    top: auto;
    left: auto;
    right: var(--gutter);
    bottom: var(--plate-bottom);
    translate: none;
  }
}

/* ─────────────────────────────────────────────────────────────────────────
   ═══ THE MOTION LAYER ═══════════════════════════════════════════════════
   Everything below is scoped to .js, which is only ever added when motion is
   going to run. Five published numbers and nothing else:

     --p   on every [data-mo] band   ·  +1 below the fold → 0 centred → −1 above
     --q   on every [data-stage]     ·   0 → 1 across the scroll its pin steals
     --k   on [data-ring]            ·  a damped impulse, and only there
     --dk  on :root                  ·   0 → 1 as the hero's front crosses
     --tc  on every [data-flip]      ·  that element's own ink flip, 0 → 1
     --lit on :root                  ·  how lit the room in section 5 is, so
                                        the fixed masthead can get out of the
                                        way of the one band it cannot be read on

   --dk and --lit are on the root rather than on their own stage because the
   masthead is fixed and is not inside either of them.

   The allocation of emphasis, which is the thing the client asked for and
   the thing a page like this most often gets wrong by making everything
   equally loud. The hero is now the loudest thing on the page by a clear
   margin, which is the client's own direction — 大胆に、わかりやすく — and
   the reason the rest of this list is unchanged: the way to make one gesture
   read as bold is to leave the others alone.

     hero        【強】  a front crossing 130% of the screen, ink flipping
                         behind it, 1.048 pull-in, a 1.9s blurred entrance
     connection  【中】  rotation + depth   ±9° turn, 0.88→1.05 scale, nine draws
     about       【静】  vertical, 0.9rem   deliberately the stillest band
     what we do  【強】  horizontal         a 300% sideways run, ±62% numeral lag
     light band  【強】  luminance          a full inversion, and the only one
     contact     【中】  depth              a 2.28× zoom through the aperture
     colophon    【静】  vertical, 0.6rem
   ───────────────────────────────────────────────────────────────────────── */

/* ── the pins · four, and there is no fifth ──────────────────────────── */
.js .stage { height: calc(100svh + var(--pin)); }
.js .stage--hero    { --pin: var(--pin-hero); }
.js .stage--constel { --pin: var(--pin-constel); }
.js .stage--work    { --pin: var(--pin-work); }
.js .stage--light   { --pin: var(--pin-light); }
.js .pin,
.js .stage--hero > .hero {
  position: sticky;
  top: 0;
  height: 100svh;
  overflow: clip;
}
/* The join between the hero and the constellation, and the only thing in this
   file that reaches across two sections.
   Every pin on this page spends its last screen of scroll leaving: the sticky
   child releases, and the next section rises into the gap it leaves. That is
   fine three times over, because what rises is a band with something in it.
   Here what rises is the constellation's own pin at --q 0 — nine stars at
   opacity 0 on a transparent ground over a black page — so the reader would
   have got a screen of scroll with nothing in it at all, between a screen that
   just went dark and a figure that has not started.
   Pulling the constellation's stage up by exactly the height of that exit
   makes its pin engage on the same frame the hero's releases. The figure is
   then drawn over the top of the departing hero, which is by then flat
   #0a0a0a, and the two are indistinguishable. It also gives the page back a
   full screen of height. */
.js .stage--constel { margin-top: -100svh; }

/* ── the generic rise ─────────────────────────────────────────────────── */
.js [data-reveal] .rv-up {
  opacity: 0;
  transform: translate3d(0, calc(0.9rem * var(--amp)), 0);
  transition: opacity var(--dur-rise) var(--ease-out) var(--rv, 0ms),
              transform var(--dur-rise) var(--ease-out) var(--rv, 0ms);
}
.js [data-reveal].is-in .rv-up { opacity: 1; transform: none; }
/* the mail link is both a .rv-up and a thing you press, and the landed state
   above would otherwise have taken the press with it */
.js [data-reveal].is-in .mail { transform: none; }

/* ── 1 · hero 【強】 ───────────────────────────────────────────────────
   The one part of the page the client asked to be unmistakable rather than
   fine. Four gestures, and the discipline that keeps them from reading cheap
   is that all four are large, slow and land without rebound: nothing here is
   under 0.85s, nothing overshoots, and the only thing that repeats is the
   scale's four hairlines.

     · the front of darkness    crosses 130% of the screen over --dk 0 → 1,
                                which is the first 0.78 of the pin
     · the pull-in              the whole room comes 1 → 1.048 as it goes out
     · the ink                  each line flips as the front reaches it
     · the entrance             the catch arrives once, on load, from blurred
                                and oversize — the one filter on the page

   The mark does not move and does not fade. That is the fourth decision and
   the load-bearing one: with a front sweeping the screen, the room sinking,
   four rules growing and a headline resolving out of a blur, a reader needs
   one fixed thing to read all of it against. */

/* the room sinks as it goes out. Squared, so the pull-in is slow while the
   front is still low on the screen and most of it happens as the room
   actually goes — a constant rate of scale reads as slowing down. */
.js .hero__img {
  transform: scale(calc(1 + var(--dk, 0) * var(--dk, 0) * 0.048));
  /* The room does not just get covered, it goes. The client's note was that the
     photograph itself never changed — measured and true: across --dk 0.15 to
     0.75 the still-lit part of the frame moved 0.3 of 255 in luminance, its
     grain not at all, and its warmth not at all. The front was a curtain drawn
     over a still.

     What is applied here is what actually happens to an image as the light
     leaves it, not an effect laid on top: colour drains, contrast lies down,
     and what is left leans warm because the last light in the room is the
     lamp's. Restrained on purpose — this is the whole first screen, and the
     darkness front is still the event.

     Measured free: interleaved four rounds against baseline, grain-only, a
     soft-light blend layer and a flat wash, all four sat inside the baseline's
     own noise at 1920x634 — median 2-3 frames over 20ms of about 130 either
     way. filter was expected to cost and does not, so this was chosen on how it
     looks rather than on what it costs. */
  filter: saturate(calc(1 - var(--dk, 0) * 0.70))
          contrast(calc(1 - var(--dk, 0) * 0.27))
          sepia(calc(var(--dk, 0) * 0.40));
}
/* the grain, and it belongs to the photograph rather than to the front. The
   veil's own ::before already carries this tile in a band that travels with the
   boundary; that one is the edge. This one is the image, rising as its light
   falls, and it sits under the veil so it only ever shows in the part of the
   room that is still lit. Same tile, same 128px, no request. */
.js .hero__media::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: calc(var(--dk, 0) * 0.68);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='128' height='128'%3E%3Cfilter id='g' color-interpolation-filters='sRGB'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='matrix' values='0 0 0 0 0.56 0 0 0 0 0.53 0 0 0 0 0.47 1 0 0 0 0'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='linear' slope='1.5' intercept='-0.45'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='128' height='128' filter='url(%23g)'/%3E%3C/svg%3E");
  background-size: 128px 128px;
}
/* the front. --dk-travel is the box's own height in screen heights, so the
   translation that puts its top at 1.0 screen heights is 1/--dk-travel of its
   own height, and subtracting --dk from that walks the top to −0.3. */
.js .hero__veil {
  transform: translate3d(0, calc((1 / var(--dk-travel) - var(--dk, 0)) * 100%), 0);
}
/* the afterimage, up over the last fifth of the crossing — i.e. as the front
   clears the top edge and the tape's column is the last thing to go */
.js .hero__after { opacity: clamp(0, calc((var(--dk, 0) - 0.8) * 5), 1); }
/* and the copy goes with the room it was lit by, over the last eighth of the
   pin. Not decoration: the constellation's pin engages on the frame this one
   releases, and that figure carries a legend in the same bottom-left corner
   the hero's catch occupies — measured, MAG. 1—3 lands inside the catch's own
   line box at 1280×800. One of the two has to leave, and it is the words that
   have been read by then rather than the legend of the figure about to be
   drawn. */
/* On --qr, the uneased twin, and not on --q. The schedule is unchanged — same
   8.4, same last eighth of the pin — but it now lands where the scroll is
   rather than where the eased figure is catching up to. See the note beside
   --qr in main.js for the measurement; the short version is that on --q this
   line is still at opacity 1 on the frame the constellation appears, and its
   four rules are the brightest thing this section has ever put across the
   figure. --qr falls back to --q so that a build where the loop has not written
   it yet degrades to the old behaviour rather than to a copy that never
   leaves. */
.js .hero__copy { opacity: clamp(0, calc((1 - var(--qr, var(--q, 0))) * 8.4), 1); }

/* ── the ink flip ────────────────────────────────────────────────────────
   Kept from the exploration, and for the reason measured there: with the
   ground continuously darkening and the ink fixed dark at one end and light
   at the other, there is a mathematically forced instant where ink and ground
   land on the same lightness. Tying ink colour to the ground's own curve made
   that instant a ~40%-of-the-scroll stretch under 4.5:1, screenshotted and
   confirmed illegible; a dual text-shadow was tried first and did not hold up,
   because at small blur radii the two halves average to the mid-grey the fill
   is already at.

   What the exploration shipped was a single steep logistic on a hand-measured
   centre, --q≈0.70, shared by every element. This build can do better,
   because the darkness is no longer uniform: it is a front at a known place.
   Each [data-flip] element is measured once (and again on resize) against the
   hero's own box, and main.js solves --dk-lead − --dk-travel · dk = that
   element's centre for its own --q0. The flip is then centred exactly where
   the front reaches that element, per element — the kicker flips before the
   catch, the catch before the rules above it, the mark last of all, each as
   the edge passes it. The crossing is not removed, which is impossible with
   both endpoints fixed; it is put where the ground under that line is
   half-transitioned anyway, and made faster than the falloff that causes it.

   Both endpoints are tokens whose contrast is already measured: --paper-ink is
   16.28:1 on --paper (which is what the white room is), --ink is 17.66:1 on
   --void. */
.js [data-flip] {
  color: color-mix(in srgb, var(--ink) calc(var(--tc, 0) * 100%), var(--paper-ink));
}
/* the rules are not type and do not need type's contrast, so they flip
   between the two dim tiers instead of the two ink ones */
.js .hero__scale {
  color: color-mix(in srgb, var(--dim) calc(var(--tc, 0) * 100%), var(--paper-dim));
}

/* ── the entrance ────────────────────────────────────────────────────────
   Once, on load, and the only place `filter` appears in this file. The client
   rejected the previous build's fade-in as 安っぽい and was explicit about
   why: not the softness, the timing — ぼわっと出てくるのは嫌いじゃない、前の
   やつのぼわっと出方がダサかっただけ. So blur alone is exactly what is not
   done here. It arrives blurred *and* oversize *and* low, and resolves all
   three at once over 1.9s on the page's own ease — one long move, no bounce,
   no second stage.

   `animation-fill-mode: backwards` and not `forwards`: the rule below is
   already the landed state, and holding the keyframe's end state would leave
   a filter: blur(0) on the element forever, which is a compositing layer for
   nothing. Backwards holds the *start* until the delay is up and then hands
   the element back to its own rule, filter and all gone. */
@keyframes hero-catch-in {
  from {
    opacity: 0;
    filter: blur(14px);
    transform: translate3d(0, 2.6rem, 0) scale(1.11);
  }
  to {
    opacity: 1;
    filter: blur(0);
    transform: translate3d(0, 0, 0) scale(1);
  }
}
@keyframes hero-rise-in {
  from { opacity: 0; transform: translate3d(0, 1.5rem, 0); }
  to   { opacity: 1; transform: none; }
}
@keyframes hero-rule-in {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}
.js .hero__catch {
  transform-origin: 0 100%;
  animation: hero-catch-in 1.9s var(--ease-out) 0.25s backwards;
}
.js .hero__kicker {
  animation: hero-rise-in 1.2s var(--ease-out) 0.15s backwards;
}
/* four rules, four speeds, and the spread is the point: the longest takes
   2.0s and the shortest 0.85s, so they are travelling at 2.35× different
   rates and arrive over a 1.6s window. Set per element in the HTML, because
   each one's speed goes with its own length. */
.js .hero__scale i {
  animation: hero-rule-in var(--sp) var(--ease-out) var(--dl) backwards;
}

/* ── the scroll indicator, and its gate ──────────────────────────────────
   THE EXCEPTION. The brief on this site is that nothing moves unless the
   reader moves it, and this is the one thing on the page that breaks it. It
   is allowed to, on three conditions, and all three are in these five rules:

     · it only runs at the top. The gate is html.moved, set by main.js on the
       first scroll of the session and never taken off again — so the mark is
       moving only while the reader has not yet done the thing it is asking
       for. Used, and it stops. That is what makes it a control and not an
       ornament.
     · it leaves. --dk takes it out over the first eighth of the crossing, so
       even a reader who never scrolls past the hero loses it as the room
       goes; it only means anything on the white room.
     · it costs nothing. One 1px segment inside a clipped 1px rail, moving on
       transform and opacity only — no layout, no paint, no filter, and it is
       torn down for good the moment the gate closes.

   The rail itself never moves. Only the segment in it travels, top to
   bottom, which is the direction being asked for. 2.6s per pass — but the
   brief's own 0.8-1.5s of stillness between passes was never actually
   built: the fade in/out either side of the travel only measured about
   0.6s of invisibility, because the loop restarted the instant the travel
   finished. The 0%/18%/62%/100% waypoints below are unmoved in real time
   (0.468s / 1.144s / 0.988s, same as before — the travel itself is no
   faster or slower); the only change is a fourth, held waypoint appended
   after the fade-out finishes, at the same position and opacity it ends
   on, so the segment sits gone for 0.4s more before the next pass — 0.6s
   +0.4s ≈ 1s of dark rail between passes. */
@keyframes hero-scroll-drop {
  0%     { transform: translate3d(0, -110%, 0); opacity: 0; }
  15.6%  { opacity: 1; }
  53.73% { opacity: 1; }
  86.67% { transform: translate3d(0, 268%, 0); opacity: 0; }
  100%   { transform: translate3d(0, 268%, 0); opacity: 0; }
}
.js .hero__scroll-l::after {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  width: 1px;
  height: 38%;
  background: currentColor;
  animation: hero-scroll-drop 3s cubic-bezier(0.4, 0, 0.5, 1) 1.2s infinite;
}
/* The loop used to keep spending a frame's worth of layer promotion on a
   mark the reader could not see: --scroll-out below already clamps to 1
   (opacity 0) once the guide has retired for the session's current visit
   to the hero, but nothing stopped hero-scroll-drop itself from running
   underneath that invisible state. main.js toggles .is-parked on this same
   element off the identical --q threshold (0.25) --scroll-out clamps at,
   so the two always agree on when the mark is actually gone — and unlike
   .moved above, it is not one-way: scroll back up past 0.25 and main.js
   takes the class back off, same frame the fade starts reversing. */
.js .hero__scroll-l.is-parked::after {
  animation-play-state: paused;
}
/* --q-scheduled now, not --dk-scheduled and not one-way: the brief calls for
   the mark to retire once the hero's own progress passes 15%, fully gone by
   25%, and to come back if the reader returns to the hero — --dk (the
   darkness front, not scroll position) and .moved (main.js's one-time,
   never-undone gate) both retired it far earlier than that and neither
   reverses, so both are replaced by this one clamp on the stage's own --q. */
.js .hero__scroll {
  --scroll-out: clamp(0, calc((var(--q, 0) - 0.15) * 10), 1);
  opacity: calc(1 - var(--scroll-out));
  transform: translate3d(0, calc(var(--scroll-out) * 4px), 0);
}

/* ── the nav ─────────────────────────────────────────────────────────────
   Used to be held out of the white room entirely and only let up once the
   room had gone dark (--dk 0.86 → 1.0). The brief changed: the nav (and its
   mobile stand-in, MENU) now belongs over the white room too, so both fade
   in once on load instead — quietly, after the copy's own entrance has had
   time to land. One shared entrance for both, since they are the same
   control and must never be seen mid-cross-fade into one another.

   The intro used to land at 0.72, not 1: on client hardware the row read as
   too faint at 11px / 0.32em tracking / weight 300, and measurement showed
   why an opacity floor can't fix that on its own — antialiasing on a stroke
   this thin never actually paints a pixel at the specified colour, so
   quietness bought through opacity compounds with quietness the rasteriser
   was already imposing. The client's call was to land at full opacity and
   carry the quiet through colour, size and tracking instead — see the ink
   comment below.

   What still has to hold across that white-to-dark crossing is the ink: on
   --paper --quiet is 11.26:1 elsewhere but unreadable, so the nav's own ink
   flips with the same front main.js already measures for the hero's copy —
   see .masthead's own [data-flip] below, one measurement standing in for
   both children since neither moves once the row lands. */
@keyframes masthead-nav-in {
  from { opacity: 0;    transform: translate3d(0, 4px, 0); }
  to   { opacity: 1;    transform: none; }
}
.js .masthead__nav,
.js .masthead__menu {
  opacity: 1;
  animation: masthead-nav-in 0.7s var(--ease-out) 1.1s backwards;
}
/* the shared ink: --paper-ink on the white room, --quiet once the front has
   crossed the row. --nav-ink is its own custom property (not inlined into
   .masthead__nav a's color directly) so the mobile overlay below can
   override just this one link without having to repeat the --act mix
   underneath it.

   The white-room endpoint used to be a --paper-dim / --paper-ink mix tuned
   to clear 4.5:1 nominally at the row's old 0.72 intro opacity — but nominal
   was the wrong measure: an 11px / 0.32em-tracked / weight-300 stroke never
   paints a pixel at its specified colour, antialiasing only carries it about
   62% of the way from the paper it sits on. Real-rendered, that tuned mix
   read at 3.48:1. --paper-ink straight (no mix, no opacity discount now the
   intro lands at 1) real-renders at ~5.0:1 even with the same 62% coverage
   discount, so the mix step is gone — the mark's own ink is dark enough on
   its own. --quiet on --void real-renders at ~4.8:1 under the same discount,
   still clear of the floor, so that side is untouched. */
.js .masthead {
  --nav-ink: color-mix(in srgb, var(--quiet) calc(var(--tc, 0) * 100%), var(--paper-ink));
  /* the active tint, flipped the same way as --nav-ink: pure --amber once the
     room is dark (11.28:1 on --void), but --amber measures under 1.2:1 on
     --paper, so on the white room the active link goes to --paper-ink
     instead — the row's own strongest ink, not a new colour. */
  --nav-act-ink: color-mix(in srgb, var(--amber) calc(var(--tc, 0) * 100%), var(--paper-ink));
}
.js .masthead__menu { color: var(--nav-ink); }
/* the active mark: which of the three the reader is currently at, published
   on :root by main.js off the same rects .masthead already can't avoid
   asking for (see --end above it) — a colour lift, not an opacity one. The
   opacity split this used to be (0.62 → 1.0) multiplied against the row's
   own 0.72 intro opacity and measured 0.446 in the white room — under 2:1,
   half what an unstyled link read as before this row existed. Both links
   sit at full opacity now and only the active one's colour moves. */
.js .masthead__nav a[href="#about"]    { --act: var(--nav-about, 0); }
.js .masthead__nav a[href="#work"]     { --act: var(--nav-work, 0); }
.js .masthead__nav a[href="#contact"]  { --act: var(--nav-contact, 0); }
.js .masthead__nav a {
  color: color-mix(in srgb, var(--nav-act-ink) calc(var(--act, 0) * 100%), var(--nav-ink));
  opacity: 1;
}
.js .masthead__nav a:hover {
  color: color-mix(in srgb, var(--ink) calc(var(--tc, 0) * 100%), var(--paper-ink));
}

/* ── the nav, on a phone ──────────────────────────────────────────────────
   Three tracked capitals fit at 390px without wrapping, but the brief asked
   for MENU instead of the desktop row repeated small, so below the
   breakpoint the row itself becomes the panel MENU opens rather than
   sitting in the corner. The button takes the row's own place in the
   masthead's space-between and gets the same entrance as the row did — see
   masthead-nav-in above, shared by both selectors already.

   The panel is a plain fade, not a slide or a wipe: the brief's own words
   were "派手なメニューを新設せず" (nothing showy), and a full-bleed rectangle
   crossing the screen would have been exactly that. It is always on --void,
   never the ink-flip pairing the fixed row needs, because it sits over
   itself rather than over scrolling content — see --nav-ink's override
   below, the one place this file undoes a token pairing instead of using
   it. .masthead's own z-index is lifted only while the panel is open, so it
   can clear .stage--light's shutter (--z-shutter) if the reader happens to
   open it there; closed, it drops back and never fights that scene for the
   stacking order it does not need. */
@media (max-width: 30rem) {
  .js .masthead__menu { display: inline-flex; }
  .js .masthead__nav {
    animation: none;
    position: fixed;
    inset: 0;
    flex-direction: column;
    justify-content: center;
    align-items: center;
    gap: var(--space-lg);
    padding: 0;
    margin: 0;
    background: var(--void);
    font-size: var(--text-md);
    opacity: 0;
    pointer-events: none;
    transform: none;
    transition: opacity var(--dur-rise) var(--ease-out);
  }
  .js .masthead__nav.is-open { opacity: 1; pointer-events: auto; }
  .js .masthead:has(.masthead__nav.is-open) { z-index: var(--z-shutter); }
  /* the panel's own ground is always --void, never the paper it can sit over
     while closed, so its ink is the flat dark-room pair and not the flip */
  .js .masthead__nav a { --nav-ink: var(--quiet); --nav-act-ink: var(--amber); }
  .js .masthead__nav a:hover,
  .js .masthead__nav a:focus-visible { color: var(--ink); }
}
html.nav-open body { overflow: hidden; }

/* Section 5 is the one screen on the page a light-ink mark cannot be read on,
   and it is also the one screen that is not allowed to be touched. So the mark
   gets out of its way instead: --lit is the lamp's own level, published on the
   root by main.js off the same --q that band already runs on, and the masthead
   is faded by it. It comes back as the blades close. */
/* Two things retire the masthead and they multiply: section 5's lit room, and
   the end of the page. --end is why the nav used to sit on top of 会社概要 — the
   masthead is fixed 42px from the top of the viewport, and once the closing
   band's own top edge has gone past that, everything under the nav is the
   composition rather than the space the band reserves for it. The chrome's job
   is finished by then anyway. */
.js .masthead { opacity: calc((1 - var(--lit, 0)) * (1 - var(--end, 0))); }

/* ── the ground sense ────────────────────────────────────────────────────
   What the light band taught the masthead, generalised. A fixed bar crosses
   six different grounds, and two of the things that pass beneath it cannot
   be solved by any choice of ink: a photograph with bright pixels in it
   (measured on the ABOUT mesh: 1.65:1 at the worst pixel, against an AA
   floor of 4.5), and the page's own type arriving at the same gutter the
   mark sits on (the ABOUT headline crosses the mark by 34px — same left
   edge, by design, on both sides). No scrim fixes the second, and a scrim
   heavy enough to fix the first would put a band across every photograph
   on the site.

   So the masthead yields, exactly as it already does for the lit room:
   js/chrome.js watches the actual ground under the mark's box and the nav's
   box — passing line boxes measured from Ranges, photograph pixels sampled
   into max-luminance grids — and puts .is-off on whichever of the two is
   standing on something it cannot hold. No scroll positions anywhere in
   that file: the sections are being re-joined by another hand this week,
   and a mechanism that looks at the ground does not care where the ground
   ends up.

   The gesture is a fade with no travel — the mark is the page's one still
   thing, and a mark that dodges sideways reads as chrome. Out fast, since
   the ground arrives at scroll speed; back slow, after chrome.js has seen
   the ground stay clear, so a sweep of departing lines is one bow out and
   one return rather than a flicker per line. */
.js .masthead__mark svg { transition: opacity 640ms var(--ease-out); }
.js .masthead__mark.is-off { pointer-events: none; }
.js .masthead__mark.is-off svg { opacity: 0; transition-duration: 240ms; }
/* two properties, not three — the letter-spacing leg went with the hover rule
   it existed for (see .masthead__nav a:hover above). The .is-off override
   below counts positionally, so it loses its middle value with it. */
.js .masthead__nav a {
  transition: color var(--dur-micro) var(--ease-out),
              opacity 640ms var(--ease-out);
}
.js .masthead__nav.is-off { pointer-events: none; }
.js .masthead__nav.is-off a {
  opacity: 0;
  transition-duration: var(--dur-micro), 240ms;
}
/* a fullscreen mobile overlay is its own ground — always --void — and is
   exempt from the ground-sense read the same way .hero__img is (see the
   file banner in chrome.js). Once open, its box is the whole viewport, so
   it almost always trips the read too (some line of the page's own copy is
   bound to sit inside it) — .is-open has to win outright, on both the
   pointer-events and the opacity .is-off just set, rather than teaching
   chrome.js a third exemption for a box it cannot see is a panel. */
.js .masthead__nav.is-open { pointer-events: auto; }
.js .masthead__nav.is-open a { opacity: 1; }

/* ── 2 · connection 【中】 ────────────────────────────────────────────
   The one gesture on the page that is a rotation. The whole figure — lines,
   stars and words together, which is why they share a box — turns from
   −9° to +4.5° about y and 5.5° to −2° about x while it comes 0.88 → 1.05
   toward the reader. Restrained on purpose: this band is between two loud
   ones and its own event is the drawing, not the turn. */
/* The one change this section needed, and it is a join fix rather than a
   change of gesture. The figure was never actually blank at --q 0: .ln--m1
   carries --mag-lead-ln: -0.02, so the first line is already 13% drawn before
   its pin has stolen a pixel of scroll. That was invisible for as long as this
   stage sat below the fold at --q 0. It does not any more — the stage is
   pulled up a screen so its pin engages as the hero's releases, which puts it
   on screen, at --q 0, over the bottom half of the still-pinned hero. Measured
   at 1920×1080: a 4px amber stub at (840, 974), sitting in the middle of the
   hero's headline. The same applies to the legend, which is deliberately not
   drawn-in and would otherwise appear over the hero's own bottom-left corner
   ~19px of scroll before the pin engages.
   Both are gated on the pin having actually started rather than being
   re-timed: 80 makes it a 1.25%-of-pin fade, and the pin is now 200svh, so it
   is the same 27px of scroll at 1920×1080 it always was (measured: opacity
   reaches 1 at 27px past engage). Doubling the pin without doubling this
   coefficient would have doubled the fade to 54px, which is long enough to
   see. "Present from the start" is still true of the figure, and nothing about
   the draw schedule below moves in physical scroll.
   .field is deliberately NOT in this list any more, and that is the change the
   client asked for. A container fade is what made the texture a background
   that was "最初から出てる": it put 198 shapes on the screen as a layer whose
   own schedule then filled it in. The field has no container fade now — every
   band is at 0 at --q 0 by its own schedule, and the first one does not open
   until 0.30. */
.js .connect { --exit: clamp(0, calc((var(--q, 0) - 0.94) / 0.06), 1); }
.js .constel { opacity: calc(clamp(0, calc(var(--q, 1) * 80), 1) * (1 - var(--exit))); }
.js .constel__legend { opacity: calc(clamp(0, calc(var(--q, 1) * 80), 1) * (1 - var(--exit))); }
/* The turn is compressed by exactly the factor the pin grew by, so it crosses
   the same 1080px of scroll it did before and then holds still for the second
   half while the field spreads. Holding still is the point: the field does not
   move (see .field in the HTML), so a plate still turning under a static field
   would read as two layers sliding, which is the seam this whole change is
   removing. */
/* ── THE CAMERA ──────────────────────────────────────────────────────────
   Two acts, and the boundary between them is 0.42.

   ACT 1 · 0 → 0.42.  The nine points are drawn, at the speed they have always
   been drawn at. The camera barely moves — 1.0 → 0.90 — so this act is the
   plate turning toward the reader and nothing else.

   ACT 2 · 0.42 → 1.0.  The camera pulls back, 0.90 → 0.28, and the network
   that keeps extending outward comes into frame because the frame is widening
   rather than because anything new is lighting up at the edge of it. That
   distinction is the whole section: three earlier builds lit material up in
   place behind the figure and were told three times that it read as a
   background spreading. A retreat cannot read as a background — there is only
   one thing on screen and it is getting further away.

   THE PAN.  The origin is 49% 10% of the box, which is where the hero's tape
   dissolves, so the retreat starts from the point the previous section ended
   on. That point sits above the network's own centre: the material runs y −72
   to 209 in the 0–100 space, centre 68.5, so scaling about y=10 leaves the
   whole thing riding high —

     centre(s) = 10 + (68.5 − 10) · s   →   26.4 at s = 0.28

   and the box centre is 50, so the last 23.6% is pushed back down. Written as
   a term on --a2 rather than solved from --cam so the pan is zero at the start
   (the composition there is the approved one and must not shift) and lands
   exactly on centre at the end. 「全体を収める」 is this number.

   --amp is deliberately NOT applied. It halves the rotation on small screens
   because a turn in perspective is what gets sickening; a scale change is the
   one gesture on this page that carries the meaning, and a phone needs it more
   than a desktop does, not less.

   Not reached under prefers-reduced-motion or with JS off: the .js class is
   added in the head only when motion is allowed, so the resting page is the
   camera at rest — which is the one where the Japanese is still readable. That
   is required, not incidental: the approved reading is that the words go too
   small to read for people who scroll, and it must not happen to anyone who
   has asked for less motion. */
/* The scrim is gated on the pin having engaged, and this is the fix for the note
   asked four times — 「下から上に上がっていく水平線」.

   It was never the hero's warm front and never the register ticks. It is 8-bit
   banding in this layer's own vertical gradient: measured at 1832x938, scrollY
   700, the pixel value steps 10 -> 11 at y=433 for every column from x=0 to
   1520, dead level, which is a linear gradient's contour and not a radial one.
   Hiding .field-scrim removes it (0 of 12 columns); hiding .hero__veil does not
   (11 of 12). It rises because before scrollY 1125 this section's pin has not
   stuck yet, so the whole layer is still travelling up the document with it, and
   it appears to open from the right third because the scrim's third gradient
   covers the statement column and breaks the contour past x 1600.

   Gating it costs nothing: the eighteen bands are all at opacity 0 until --q
   0.26, so there is no texture to clear before then and this layer had no job.
   x 20 puts it at full by --q 0.05, a fifth of the way to the first band.

   Not gated with JS off or under prefers-reduced-motion — there the whole
   network is drawn at rest and the scrim is needed from the first frame. */
/* ── the seal under the leaving hero ──────────────────────────────────────
   The bright line the client asked about four times. Not the warm front, not
   the register ticks, not the scrim's banding — all three of those were chased
   on a guess. 六 found this one from the geometry, and the measurement agrees to
   the pixel.

   Measured at 1832x938:

     scrollY   .hero bottom   .hero__veil bottom   .hero__img bottom
      1314          750               750                772
      1426          638               638                660
      1539          525               525                547

   The veil's bottom edge and the hero's own clip edge land on EXACTLY the same
   row — 155% of the box translated by 1/1.55 of it leaves no overlap at all —
   and the photograph runs 22px past both, clipped by overflow: clip at the same
   row. So one edge, two coincident boundaries, and rgb(244,241,238) directly
   underneath. Half a sample of bilinear error at that row and the lit room comes
   through as a bright hairline the full width of the screen. It rises because
   the hero is leaving: 750 -> 638 -> 525 -> 413 as you scroll.

   It does not reproduce in a CDP screenshot. Page.captureScreenshot can read
   back through a different path than the on-screen compositor, so a seam that
   only exists in compositing is invisible to it — which is why four passes of
   measuring found other things instead. The fix therefore cannot be verified by
   eye here; it is made safe by construction instead.

   The construction: once --dk is 1 the veil is opaque over the whole hero, so
   nothing under it can be seen any more — and from that moment nothing under it
   is allowed to be bright either. The ground goes to --void and the photograph
   goes to zero. Whatever leaks past the edge after that is the same colour as
   the edge. Gated over --dk 0.995..1 so it happens strictly after the front has
   landed, which is about 4px of scroll and changes nothing that is visible.

   Not in the resting layer: with JS off there is no front, the room never goes
   out, and the photograph must stay. */
/* THE COLOUR AND NOT ONLY THE IMAGE, and that distinction is the bug the first
   version of this seal did not close. A background-image over a background-colour
   fills the same box, so on paper it hides it completely — but the two are
   rasterised as separate paint ops, and at the box's fractional bottom edge their
   antialiasing does not agree. A sliver of the room's own tone survives under the
   black.

   It needs the page rendered at a scale BELOW 1 to show, which is why five passes
   at DPR 1, 2 and 3 all came back clean and why the original note here concluded
   the seam "does not reproduce in a CDP screenshot". It does. The client found the
   recipe: Chrome device mode, iPhone 14 Pro Max, browser window at 50%.

   Reproduced at 430x932 with deviceScaleFactor 0.5, .hero bottom 520.8, scanning
   for a full-width level row:

     scale   row            step
     0.5     css520         +9.1     ← every scroll position, on .hero's bottom
     0.75    css521         −4.3       edge, so it rises as the hero leaves
     1       —              none

   +9 of a 234-level difference between #f4f1ee and --void is about 4% coverage,
   which is a sub-pixel sliver and not a blend of two blacks. Everything else
   chased on this section was ±1; this is the one the client has been pointing at
   for five rounds. Ablation agrees: hiding .hero__media, .hero__img, the grain,
   the veil, its edge band and the afterimage leaves the step at +9.1, and
   .hero { background-image: none } exposes 240 — the room, still there.

   color-mix carries the colour on the same --seal the image already uses, so
   there is one gate and not two, and at --seal 0 it resolves to exactly the
   literal below. #f4f1ee is spelled out rather than tokenised because the base
   rule spells it out too — it is the photograph's colour, not the page's, and
   the two have to stay the same value. The image is kept: it is verified, it
   costs nothing, and it covers the interior while this covers the edge. */
.js .hero {
  --seal: clamp(0, calc((var(--dk, 0) - 0.995) * 200), 1);
  background-color: color-mix(in srgb, var(--void) calc(var(--seal) * 100%), #f4f1ee);
  background-image: linear-gradient(rgb(10 10 10 / var(--seal)), rgb(10 10 10 / var(--seal)));
}
/* Not the whole photograph — only the rows that sit on the clip edge. Fading all
   of it also took the tape's afterimage with it: isolated at scrollY 900 and
   1100, the change was 0.29% of the frame in a 210x49 box at (800, 0), which is
   the tape's own column at the top of the screen coming through the hole in the
   veil's mask. That afterimage is the landing beat and has to stay.
   The background seal on its own measured 0.0000% at every position tested, so
   it is free; this mask is the other half, scoped to 14px. */
/* On .hero__media and NOT .hero__img, and that distinction was the whole bug in
   the first attempt at this. The client's own click settled it: clicked y=193
   against .hero bottom=195, .hero__veil bottom=195 — the hero's leaving edge, to
   two pixels — but .hero__img bottom=210. The image carries a scale for its own
   drift, so its box overhangs the hero by 15px, and "the bottom 14px of the
   image" was 196..210, which overflow:clip had already thrown away. The rows
   actually sitting on the edge, 181..195, were untouched.
   .hero__media is inset:0 of .hero with no transform of its own, so its bottom
   14px is the hero's bottom 14px, which is the row that leaks. The image is
   inside it, so the scaled image is masked too. */
/* The ramp reaches zero AT the bottom edge, not before it, and that is not the
   same thing as being off. Measured at 390x844, scrollY 930, .hero bottom
   673.59: row 673 reads 11 against 10 for every column across the full width,
   dead level. Hiding .hero__media takes it to 10; hiding .hero__img does too.
   It is the photograph, arriving through its own mask — 0.59px above the edge
   the ramp still passes 1/14 of it, the room under there measures 68.6, and
   68.6/14 is the one level. The hero's bottom is fractional at every scroll
   position, so there is always exactly one such row, and it rises with the
   hero. That is the line the client has been pointing at.
   Unlike the veil seam above it, this one DOES come back through
   Page.captureScreenshot, which is why it survived four passes that were
   looking for a compositing-only artefact.
   3px of flat transparent before the ramp starts is more than the one partial
   row needs, and the ramp keeps its 13px so the fade itself is unchanged. Both
   numbers are still gated on --seal, so with JS off the mask is inert. The
   tape's afterimage is at the top of the screen and is not in this box. */
.js .hero__media {
  -webkit-mask-image: linear-gradient(to top, transparent 0 calc(var(--seal) * 3px), #000 calc(var(--seal) * 16px));
          mask-image: linear-gradient(to top, transparent 0 calc(var(--seal) * 3px), #000 calc(var(--seal) * 16px));
}
.js .field-scrim { opacity: calc(clamp(0, calc(var(--q, 1) * 20), 1) * (1 - var(--exit))); }
.js .cam {
  --a1: clamp(0, calc(var(--q, 0) / 0.42), 1);
  --a2: clamp(0, calc((var(--q, 0) - 0.42) / 0.58), 1);
  --cam: calc(1 - var(--a1) * 0.10 - var(--a2) * 0.62);
  transform: translate3d(0, calc(var(--a2) * 23.6%), 0) scale(var(--cam));
  transform-origin: 49% 10%;
  opacity: calc(1 - var(--exit));
  /* measured: three sweeps per variant at 1920/1280/375 put the naive
     transform, this, and no transform at all inside each other's noise —
     p50 16.7ms in every one. The hint is kept because it is free and correct.
     contain:paint was tried and rejected: it clipped 6% of the lit figure,
     which is the material this section is about. */
  will-change: transform;
}
.js .constel {
  --t: clamp(0, calc(var(--q, 1) / 0.5), 1);
  transform: perspective(1400px)
             rotateY(calc((-9deg + var(--t) * 13.5deg) * var(--amp)))
             rotateX(calc((5.5deg - var(--t) * 7.5deg) * var(--amp)))
             scale(calc(0.88 + var(--t) * 0.17));
  transform-origin: 42% 50%;
}
/* Nine lines over the first 39% of the pin, each with a window 0.1 wide and
   each starting 0.03625 after the last, so consecutive lines overlap by
   nearly two thirds and the figure is drawn as one continuous movement
   rather than nine separate ones. --i is the line's index, set in the HTML.
   --mag-lead-ln/--mag-speed-ln (0 / 10 where a line doesn't set them, i.e.
   every ln--m2) nudge that shared schedule per magnitude instead of
   replacing it, so the figure still reads as one draw and not nine unrelated
   ones.
   EVERY NUMBER IN THIS BLOCK AND THE NEXT THREE IS HALF WHAT IT WAS — or
   twice, for the speeds — and it buys no change at all. The pin doubled to
   200svh to give the field somewhere to keep spreading; halving the drawing's
   schedule in --q is what keeps the drawing itself at exactly the scroll
   distance and the felt speed it had before. Measured at 1920×1080, px from
   pin engage to 10/25/50/75/90/99% of the draw: 135/270/486/648/729/837 before
   and 135/270/486/648/729/837 after, sampled at the same 27px pitch — the same
   numbers, not near them. The client's instruction was that the existing
   animation is not to change; this is that instruction, in arithmetic. */
.js .constel__lines path {
  --d: clamp(0, calc((var(--q, 1) - var(--i) * 0.03625 - var(--mag-lead-ln, 0)) * var(--mag-speed-ln, 10)), 1);
  /* × 0.64, which is the new dash period (0.28 + 0.36). The offset used to be
     one unit against a one-unit period, so the pattern slid exactly one dash
     across the draw window; scaling it by the period keeps that behaviour
     instead of sliding 1.56 dashes now that the grain is finer. */
  stroke-dashoffset: calc((1 - var(--d)) * 0.64);
}
/* a point lights when the line that reaches it is nearly there. Brief flatly
   rules out twinkle/blink, so the per-magnitude difference lives entirely in
   --mag-lead/--mag-speed: a hub snaps to full strength decisively and a hair
   ahead of schedule, a leaf star eases in slower and a hair behind — no
   looping animation, just a different one-way rise. */
.js .star {
  --d: clamp(0, calc((var(--q, 1) - var(--i) * 0.03625 - var(--mag-lead, 0)) * var(--mag-speed, 8)), 1);
  opacity: calc(var(--d) * var(--star-op, 1));
  scale: calc(0.4 + var(--d) * 0.6);
}
.js .zone, .js .node {
  --d: clamp(0, calc((var(--q, 1) - 0.015 - var(--i) * 0.03625) * 6.8), 1);
  opacity: var(--d);
}

/* ── the continuation ────────────────────────────────────────────────────
   THIS IS THE SECOND HALF OF THE DRAWING, not a second layer. Same shape of
   schedule as the nine lines above — a start offset per band, a window, a
   speed that falls as the band goes out — laid end to end with them:

     the nine lines      --q 0     → 0.39
     band 0              --q 0.30  → 0.40      overlaps the figure's landing
     band 1              --q 0.338 → 0.44
     …                                          step 0.038, window 0.10–0.129
     band 15             --q 0.870 → 0.999     still arriving at the end

   Three things in that table are the whole fix.

   · Band 0 starts at 0.30, which is 77% of the way through the drawing, and
     finishes at 0.40, just after it. Deliberately overlapping and not butted:
     a clean handover at 0.39 would put a seam there, and the reader would see
     two movements. This way the outward spread is already underway as the
     ninth line lands, and there is no instant at which the figure "finishes".
   · Consecutive bands overlap by 62–70%, the same ratio the nine lines use, so
     sixteen bands read as one front and not as sixteen rings.
   · The last band is still coming out at 0.999. Nothing here stops early. The
     client's note was 「どんどんどんどん…星も出ていく」and the failure mode
     it was aimed at is a spread that runs out of field and stands still; the
     schedule is drawn so the reader leaves the section with it still going.

   The bands are numbered outward from (49,10) — the point the hero's tape is
   spent at, and the root the figure itself branches from — and stars, lines
   and words all read the same band index off the same distance scale. That
   shared scale is what makes it a front: everything at the same distance
   arrives together, whatever kind of mark it is.

   The speed falls with --i (10 down to 7.75), which is the same "brighter
   resolves faster" rule the stars use for magnitude. Here magnitude *is*
   distance, so order, speed and weight all follow one number.

   --tl is the same ramp run 45% faster, and only the lines use it: a line that
   draws its geometry in at the same rate its band brightens has a leading tip
   at nearly zero alpha, which loses the one thing that makes a line read as
   travelling. Ahead of the fade, the tip is visible and the band still lands
   on tone together. */
/* Eighteen bands, numbered outward from (49,10) — the point the hero's tape is
   spent at, and the root the nine named points branch from. Band 0 opens at
   0.26 and the last is still arriving at 1.0, so nothing stands still while the
   camera is still pulling back. Consecutive bands overlap by about two thirds,
   the ratio the nine lines already use, so eighteen bands read as one spread
   rather than eighteen rings.

   THERE IS NO --tone HERE, and that absence is the fix. Every previous build
   multiplied this by a weight that fell with distance, and that is what turned
   a continuation into a background: material that dims as it goes is scenery.
   Depth comes from ln--m1/2/3 and star--m1/2/3 — the same three magnitudes the
   nine named points are already built from — and from the camera, which makes
   everything further away smaller because it is further away. Nothing else.

   --nl is the same ramp 45% faster, and only the paths use it: a line whose
   geometry arrives at the same rate as its own fade has a leading tip at almost
   zero alpha, which loses the one thing that makes a line read as travelling. */
/* RE-SPACED AT THIS PASS, and the reason is a measurement rather than a taste.
   The criterion on this pin is that no 0.05 interval of --q may carry less than a
   quarter of the perceptible change the busiest one carries. Measured at 1440x900
   over the whole pin, before: 0.20->0.25 came in at 0.99% against a peak of 8.15%
   at 0.50, which is 0.12 — a lull you can feel. The lull was structural: the nine
   lines are done by 0.357 and the first band did not open until 0.26, so there
   was a stretch with the drawing nearly finished and the spread not started, and
   then everything arrived at once in the middle.
     opens        0.26 -> 0.185      overlaps the nine lines properly now
     step        0.0385 -> 0.0435    the same 18 bands over a longer span
     window      0.085 -> 0.105      so consecutive bands still overlap 59%
   Band 17 now opens at 0.9245 and is at 0.72 of its own ramp when the pin ends,
   so the reader still leaves the section with the spread still spreading — which
   was the point of the old numbers and is kept. 「どんどんどんどん…星も出て
   いく」.
   --nl is the same ramp 45% faster, and only the paths use it: a line whose
   geometry arrives at the same rate as its own fade has a leading tip at almost
   zero alpha, which loses the one thing that makes a line read as travelling. */
.js .field .nb {
  --nr: clamp(0, calc((var(--q, 1) - 0.13 - var(--i) * 0.048) / 0.105), 1);
  --nl: clamp(0, calc((var(--q, 1) - 0.13 - var(--i) * 0.048) / 0.0724), 1);
}
.js .field div.nb { opacity: var(--nr); }
.js .field .nb path { stroke-dashoffset: calc((1 - var(--nl, 1)) * 0.64); }
/* A HARD ZERO AT THE START OF A BAND, and it is a real measurement and not a
   belt. pathLength is 1 and the dash pattern is 1 on 1 off, so at --nl 0 the
   dash occupies [-1, 0] and its trailing edge lands exactly ON the path's first
   point. A straight M/L had nothing there to antialias; a cubic's first
   tessellated segment does, and the curved build's first --q 0 render came back
   with 36 warm pixels at 1440x900 — delta 1 to 4 out of 255, invisible, and not
   zero, and the criterion is zero. Gated on the band's own --nl, so it is one
   inherited declaration per band and it is 1 by --nl 0.0125; the broken register
   sets its own stroke-opacity on the path and is unaffected. */
.js .field .nb { stroke-opacity: clamp(0, calc(var(--nl, 1) * 80), 1); }
/* The broken register cannot be drawn with stroke-dashoffset, because the dash
   pattern IS the drawing here: dasharray and dashoffset are one mechanism and
   the two uses cancel. It could have been solved with a per-edge mask or by
   animating four gradient stops per edge, and both put 400-odd elements back on
   the per-frame arithmetic that this section was rebuilt to get off — rule three
   of the brief. So it is revealed instead, on stroke-opacity, off the band's own
   --nr that is already computed once for the whole band.

   It is deliberately LATE against its own band, which is the part that carries
   meaning: --nr has to reach 0.3 before this starts. The bright structure of a
   band draws, and the lateral connections fill in behind it. Nothing here is a
   fade-in of a layer; it is the second half of one band's arrival.

   stroke-opacity and not opacity, and not a group: opacity on a new <g> would be
   eighteen more elements compositing inside .cam's own per-frame transform,
   which is the exact shape of the 3x cost this section already measured once.
   stroke-opacity on the path re-rasters the same stroke it was already going to
   raster and does not re-tessellate it the way touching dasharray would. */
.js .field .nb path.ln-dash {
  stroke-dashoffset: 0;
  /* the window is 0.45 of the band's own ramp rather than 0.7, which both puts
     the lateral connections further behind the structure that carries them and
     leaves a third fewer of them mid-ramp — and mid-ramp is the only state a
     dashed curve costs anything in */
  stroke-opacity: clamp(0, calc((var(--nr, 1) - 0.55) / 0.45), 1);
}
/* the halo. No pathLength and no dasharray on this pass at all — see the note in
   gen_network.py's build_field. Lagged 0.35 behind the core's own tip so the glow
   builds where the line has already been rather than running ahead of it. */
.js .field .nb path.ln-halo {
  stroke-opacity: clamp(0, calc((var(--nl, 1) - 0.35) / 0.65), 1);
}
/* --d is set per star for the nine; the generated phrases have no star of their
   own to read it off, so it falls back to landed. */
.js .node { translate: -0.105em calc(-100% - 0.29em + (1 - var(--d, 1)) * 0.37em); }
/* the leader line's own length tracks the same lift, so it never reads as
   disconnected while the text is still rising off its landed position */
.js .node::after { height: calc(0.29em + (1 - var(--d, 1)) * 0.37em); }
/* 六's schedule for the label, on the section's own --q and nothing else — no new
   listener, no observer:

     0.00 – 0.26   hidden
     0.26 – 0.36   fades in
     0.36 – 0.84   held
     0.84 – 0.94   fades out
     0.94 – 1.00   hidden

   Two ramps multiplied, which is why --exit is not read here the way the old copy read
   it: --exit runs 0.94 → 1.00 and this is already at zero by 0.94, so a second fade
   would be dead weight. opacity and 4px of translate, no scale, no blur, and the label
   does not follow the camera — it is outside .cam, so it holds its place on the screen
   while the figure retreats behind it.

   Under prefers-reduced-motion the .js class is never added, so none of this applies:
   the label is simply there, at rest, with no transform. That is 六's requirement for
   reduced motion met by the architecture rather than by a second rule. */
.js .field-label {
  --in: clamp(0, calc((var(--q, 1) - 0.26) / 0.10), 1);
  --out: clamp(0, calc((var(--q, 0) - 0.84) / 0.10), 1);
  opacity: calc(var(--in) * (1 - var(--out)));
  translate: 0 calc((1 - var(--in)) * 4px);
}



/* ── 3 · about 【強】 ─────────────────────────────────────────────────
   TWO MOVES. The picture is discovered full bleed, and then the black comes in from
   the left and takes half of it back to make room for the words. Nothing translates
   except that black: the photograph sits at plain cover, no transform, from the first
   frame to the last.

   ONE TIMELINE ACROSS TWO SCROLL EVENTS. A pin cannot publish --q until it sticks,
   which meant this section used to scroll into view over a full screen of dead black
   before anything could begin — 1,000px of it at 1440, measured, and it read as「最初
   から黒が広がっている」. So the pin also carries data-mo, and main.js publishes --p on
   it: 1 with the pin's top at the bottom of the screen, 0 when it engages. --e turns
   that into 0→1 across the entry, and --T concatenates the two into one monotonic
   0→1: the entry is its first 0.30, the pin the remaining 0.70.

   Every window below is in --T, and every one is eased rather than used raw.

     --T            what happens                              scroll at 1440
     0.00 – 0.105   black. The beat after the network.          0 → 35vh of entry
     0.105– 0.175   --s1  a thin filament at x 76 y 27          35 → 58vh of entry
     0.150– 0.240   --s2  down and right, the brightest point
     0.205– 0.305   --s3  across the frame to the central ridge (crosses into the pin)
     0.272– 0.400   --s4  the far left and the upper left
     0.335– 0.470   --s5  the whole picture, under all four
     0.470– 0.500   nothing. The field is lit, full bleed.      ~5vh
     0.500– 0.720   --pn  THE PANEL. 0 → 48% from the left.     ~35vh
     0.660– 0.730   --lab   FIG. 03 and ABOUT, left to right
     0.700– 0.790   --h1    line 1, left to right
     0.755– 0.855   --h2    line 2, trailing it
     0.820– 0.955   --body  five paragraphs, each 0.12 behind the last
     0.955– 1.00    nothing moves, and then a whole screen of it as the stage leaves.

   THE PANEL IS THE ONLY THING ON A SLOWER CURVE. The four regions use smoothstep,
   t²(3−2t). The panel uses smootherstep, t³(t(6t−15)+10), which is flat to the second
   derivative at both ends: it leaves and arrives with no detectable start or stop, and
   over 35vh of travel that is the difference between a wipe and something expensive
   closing. 高級感 is a curve and a duration, not a speed.

   The words follow the black left to right, which is where it came from. Each is
   uncovered by a soft mask over its own box rather than moved, so nothing translates
   into place — 六's original objection to a fade, without the long lift that answered
   it the first time.

   The panel is clip-path on the plate, not a moving div: the picture must not be
   relaid out or resampled while its window closes.

   No will-change anywhere in here. Five opacities, one clip and four small masks;
   the layers are already composited and asking for permanent promotion costs memory
   on every screen for a gain on one. 60fps measured at 1440/1920/390. */
.js .stage--about { --pin: var(--pin-about); }
.js .about-pin {
  /* the entry, 0 → 1. var(--p, -1) and not (--p, 1): with no script there is no --p,
     and the fallback has to mean "long past", which is what the whole file does. */
  --e: clamp(0, calc(1 - var(--p, -1)), 1);
  --T: calc(var(--e) * 0.30 + var(--q, 1) * 0.70);

  --t1: clamp(0, calc((var(--T) - 0.105) / 0.070), 1);
  --t2: clamp(0, calc((var(--T) - 0.150) / 0.090), 1);
  --t3: clamp(0, calc((var(--T) - 0.205) / 0.100), 1);
  --t4: clamp(0, calc((var(--T) - 0.272) / 0.128), 1);
  --t5: clamp(0, calc((var(--T) - 0.335) / 0.135), 1);
  --s1: calc(var(--t1) * var(--t1) * (3 - 2 * var(--t1)));
  --s2: calc(var(--t2) * var(--t2) * (3 - 2 * var(--t2)));
  --s3: calc(var(--t3) * var(--t3) * (3 - 2 * var(--t3)));
  --s4: calc(var(--t4) * var(--t4) * (3 - 2 * var(--t4)));
  --s5: calc(var(--t5) * var(--t5) * (3 - 2 * var(--t5)));

  /* the panel, on smootherstep */
  --tp: clamp(0, calc((var(--T) - 0.500) / 0.220), 1);
  --pn: calc(var(--tp) * var(--tp) * var(--tp) * (var(--tp) * (var(--tp) * 6 - 15) + 10));

  --lab:  clamp(0, calc((var(--T) - 0.660) / 0.070), 1);
  --h1:   clamp(0, calc((var(--T) - 0.700) / 0.090), 1);
  --h2:   clamp(0, calc((var(--T) - 0.755) / 0.100), 1);
  --body: clamp(0, calc((var(--T) - 0.820) / 0.135), 1);
}
.js .about__silver { clip-path: inset(0 0 0 calc(var(--pn) * 48%)); }
.js .about__frame { opacity: var(--s5); }
.js .about__wave--1 { opacity: var(--s1); }
.js .about__wave--2 { opacity: var(--s2); }
.js .about__wave--3 { opacity: var(--s3); }
.js .about__wave--4 { opacity: var(--s4); }
/* one rule for every piece of type that follows the panel. The soft edge is 28% of
   the element's own width and travels from just off its left to just past its right,
   so at --m 0 the box is empty and at 1 it is whole. */
.js .about__label,
.js .about__line > span {
  -webkit-mask-image: linear-gradient(to right,
    #000 calc(var(--m) * 132% - 32%), transparent calc(var(--m) * 132% - 4%));
          mask-image: linear-gradient(to right,
    #000 calc(var(--m) * 132% - 32%), transparent calc(var(--m) * 132% - 4%));
  /* no-clip, because ABOUT's amber rule is a ::after at bottom: -0.55em — outside
     the border box, which is where mask-clip stops by default. The rule vanished
     from the finished layout the first time this mask went on. The gradient tiles
     (mask-repeat is repeat), so the rule gets the same value as the glyphs above
     it and is uncovered with them. */
  -webkit-mask-clip: no-clip;
          mask-clip: no-clip;
}
.js .about__label { --m: var(--lab); }
.js .about__line > span { --m: var(--h1); }
.js .about__line:last-child > span { --m: var(--h2); }
/* --i is on each paragraph, 0..4. One expression staggers all five: each starts
   0.12 of the body window behind the one above it and takes 0.4 of it. */
.js .prose p {
  --pd: clamp(0, calc((var(--body) - var(--i, 0) * 0.12) / 0.4), 1);
  opacity: var(--pd);
  translate: 0 calc((1 - var(--pd)) * 0.4rem);
}
.js .about__rule { transform: scaleX(var(--body)); }

@media (max-width: 47.99rem), (pointer: coarse) and (max-width: 63.99rem) {
  /* THE PHONE HAS NO PANEL, BECAUSE IT HAS NO COLUMN. The picture is full bleed at
     rest there and the type sits on a scrim over it, so the thing that makes room is
     the scrim, and it comes up on the same window and the same curve the panel uses.

     Two regions, not four: cover on a 390×844 box crops a 1672px source to its middle
     26%, so three of the four desktop origins are off the screen. The two that remain
     were read off the same luminance map over the strip the phone actually shows —
     x 57 y 52, the brightest node in it, and x 20 y 46, the ridge running left out of
     it — and waves 3 and 4 are turned off rather than left to fire on nothing.

     The whole schedule is compressed for 85svh of pin instead of 110, and the black
     hold is 0.12 of --T, which is still 40vh of the entry because the entry is a
     screen either way. */
  .js .about-pin {
    --t1: clamp(0, calc((var(--T) - 0.120) / 0.100), 1);
    --t2: clamp(0, calc((var(--T) - 0.180) / 0.130), 1);
    --t5: clamp(0, calc((var(--T) - 0.290) / 0.150), 1);
    --tp: clamp(0, calc((var(--T) - 0.470) / 0.200), 1);
    --lab:  clamp(0, calc((var(--T) - 0.620) / 0.080), 1);
    --h1:   clamp(0, calc((var(--T) - 0.670) / 0.100), 1);
    --h2:   clamp(0, calc((var(--T) - 0.730) / 0.110), 1);
    --body: clamp(0, calc((var(--T) - 0.800) / 0.150), 1);
  }
  .js .about__silver { clip-path: none; }
  .js .about__silver::after { opacity: var(--pn); }
  .about__wave--3,
  .about__wave--4 { display: none; }
  .about__wave--1 {
    -webkit-mask-image:
      radial-gradient(ellipse 26% 15% at 57% 52%, #000 0%, rgb(0 0 0 / 0.86) 30%, rgb(0 0 0 / 0.40) 62%, transparent 88%),
      radial-gradient(ellipse 18% 11% at 67% 43%, #000 0%, rgb(0 0 0 / 0.66) 34%, transparent 84%);
            mask-image:
      radial-gradient(ellipse 26% 15% at 57% 52%, #000 0%, rgb(0 0 0 / 0.86) 30%, rgb(0 0 0 / 0.40) 62%, transparent 88%),
      radial-gradient(ellipse 18% 11% at 67% 43%, #000 0%, rgb(0 0 0 / 0.66) 34%, transparent 84%);
  }
  .about__wave--2 {
    -webkit-mask-image:
      radial-gradient(ellipse 46% 20% at 20% 46%, #000 0%, rgb(0 0 0 / 0.82) 26%, rgb(0 0 0 / 0.36) 60%, transparent 88%),
      radial-gradient(ellipse 24% 14% at 8% 62%, #000 0%, rgb(0 0 0 / 0.60) 32%, transparent 85%);
            mask-image:
      radial-gradient(ellipse 46% 20% at 20% 46%, #000 0%, rgb(0 0 0 / 0.82) 26%, rgb(0 0 0 / 0.36) 60%, transparent 88%),
      radial-gradient(ellipse 24% 14% at 8% 62%, #000 0%, rgb(0 0 0 / 0.60) 32%, transparent 85%);
  }
}

/* ── 4 · what we do 【強】 ────────────────────────────────────────────
   The one place the page changes direction. Four panels are a track four
   screens wide inside the pin, and the screen-and-a-tenth of scroll the pin
   steals is spent translating it right to left: --q 0→1 is −75% of the
   track, exactly four panels of one screen each. This is still one pin; the
   track cannot scroll the page sideways because the pin clips it.

   --d is how far this panel is from the held frame in track steps, and it is
   what the per-panel gestures read. The numeral is given extra travel so it
   arrives and leaves faster than the panel carrying it and reads as nearer;
   the words are given negative travel, so they drag. Four panels, four
   different amounts, because they are four different compositions and the
   numeral is not in the same place in any two of them. */
/* ── THE TRACK HOLDS AT EACH PANEL ───────────────────────────────────────
   The client's note: 「スクロールとそれぞれのブロクの停止位置が曖昧であまりちゃんと
   見れてない」「動きはすごくいい」. The verbs were fine; the schedule never let
   anyone look at them. The track was translate3d(--q * -75%) — dead linear — and
   --near, which is what brings each panel's own movement home, is 1 only at the
   instant --d hits 0. Four instants across a 110svh pin. Nothing ever rested.

   --qs is --q with a hold at each panel: four dwells of 14.5% and three
   transitions of 14% make exactly 1.0, and because the dwell values land on the
   same 0 / 0.3333 / 0.6667 / 1 the panels carry as --c, --d is exactly 0 for the
   whole of each dwell rather than for one frame of it. At 900svh that is about
   144px of scroll per panel with the composition square in the frame and its own
   motion completed.

   Each transition is smoothstepped rather than linear — a linear ramp between two
   holds reads as a mechanism, and this section is the one the client says the
   movement is already good in.

     dwell 1  0     → 0.145      transition 1  0.145 → 0.285
     dwell 2  0.285 → 0.43       transition 2  0.43  → 0.57
     dwell 3  0.57  → 0.715      transition 3  0.715 → 0.855
     dwell 4  0.855 → 1.0

   The 01 — 04 counter RULE below deliberately stays on raw --q: it reports scroll
   progress, not which panel is up, and stepping it would make it look broken. The
   counter's leading FIGURE is the other way round — it reports which panel is up,
   so it reads --qs. Both are in this file and neither is in the script. */
/* ── THREE SCHEDULES, ONE TRACK ──────────────────────────────────────────
   六's spec: 「画像、番号、テキストが同時に動かないよう、わずかな時間差をつける。画像が先に
   少し動き、番号が追従し、最後にテキストが安定する」. The client's separate note is that
   the throw itself is the good part, so not one coefficient below changes — the
   photograph still travels −9%, the numeral still travels 85%, the words still
   drag −20%. What is offset is WHEN each of them spends that travel.

   --qs is the reference and it is what the track itself rides, so the panel edges
   land exactly where they always did. --qa and --qc are the same staircase with
   the transition windows moved a little earlier and a little later:

                    ramp starts              width     first ramp ends
     --qa  image    0.131 0.416 0.701        0.132     0.263
     --qs  numeral  0.145 0.430 0.715        0.140     0.285   ← the track
     --qc  words    0.159 0.444 0.729        0.147     0.306

   All three hold the same four values — 0, ⅓, ⅔, 1 — so at every rest every --d
   is exactly 0 and every amplitude is untouched. Inside a hand-off the image
   starts 0.014 of --q before the numeral and finishes 0.022 before it, and the
   words start 0.014 after and finish 0.021 after. At 1440×900 the pin steals
   110svh = 990px, so a hand-off is 139px of scroll and the spread across it is
   ±14px at the start and ~43px between the image landing and the words landing —
   about 31% of the hand-off, which is a stagger you feel and cannot time.

   The words still finish 21px into a 143px dwell, so the 静止時間 the client asked
   for is 122px of scroll with everything stopped. */
.js .stage--work {
  --t1: clamp(0, calc((var(--q, 0) - 0.145) / 0.14), 1);
  --t2: clamp(0, calc((var(--q, 0) - 0.43) / 0.14), 1);
  --t3: clamp(0, calc((var(--q, 0) - 0.715) / 0.14), 1);
  --s1: calc(var(--t1) * var(--t1) * (3 - 2 * var(--t1)));
  --s2: calc(var(--t2) * var(--t2) * (3 - 2 * var(--t2)));
  --s3: calc(var(--t3) * var(--t3) * (3 - 2 * var(--t3)));
  --qs: calc((var(--s1) + var(--s2) + var(--s3)) / 3);

  --ta1: clamp(0, calc((var(--q, 0) - 0.131) / 0.132), 1);
  --ta2: clamp(0, calc((var(--q, 0) - 0.416) / 0.132), 1);
  --ta3: clamp(0, calc((var(--q, 0) - 0.701) / 0.132), 1);
  --sa1: calc(var(--ta1) * var(--ta1) * (3 - 2 * var(--ta1)));
  --sa2: calc(var(--ta2) * var(--ta2) * (3 - 2 * var(--ta2)));
  --sa3: calc(var(--ta3) * var(--ta3) * (3 - 2 * var(--ta3)));
  --qa: calc((var(--sa1) + var(--sa2) + var(--sa3)) / 3);

  --tc1: clamp(0, calc((var(--q, 0) - 0.159) / 0.147), 1);
  --tc2: clamp(0, calc((var(--q, 0) - 0.444) / 0.147), 1);
  --tc3: clamp(0, calc((var(--q, 0) - 0.729) / 0.147), 1);
  --sc1: calc(var(--tc1) * var(--tc1) * (3 - 2 * var(--tc1)));
  --sc2: calc(var(--tc2) * var(--tc2) * (3 - 2 * var(--tc2)));
  --sc3: calc(var(--tc3) * var(--tc3) * (3 - 2 * var(--tc3)));
  --qc: calc((var(--sc1) + var(--sc2) + var(--sc3)) / 3);
}
.js .work {
  display: flex;
  width: 400%;
  height: 100%;
  min-height: 0;
  gap: 0;
  transform: translate3d(calc(var(--qs, 0) * -75%), 0, 0);
}
.js .work__panel {
  flex: 0 0 25%;
  width: 25%;
  height: 100%;
  /* padding-inline is NOT zeroed here any more. It used to be, because every
     element in every panel hung off its own --gutter margin — four unrelated
     systems, which is what the magazine grid replaced. The grid carries the
     gutter as the panel's own padding now, so zeroing it here put column 1 at
     x 0 and ran the type off the left edge. box-sizing is border-box, so the
     panel is still exactly a quarter of the 400% track. */
  min-height: 0;
  /* flex items default to min-width:auto, and these cannot shrink; one panel
     whose min-content is wider than a screen would widen all four and put
     the fourth off the end of the run */
  min-width: 0;
  /* The track is four screens wide and its transform is rewritten every
     frame. Left as one paint unit, the engine culls it to an interest rect
     computed off an earlier transform, and the trailing ~85px of whichever
     panel is held simply is not painted — measured on the 02 panel, where it
     took the last letter off SECRETARIAT and the whole right edge of the
     plate with it. Containing paint to the panel makes each one its own
     unit, one screen wide, and the cull has nothing left to get wrong.
     Nothing in these compositions bleeds past its own panel — the two that
     bleed, 01 to the left and 04 downward, bleed to a panel edge that is
     also a screen edge — so the containment costs nothing and buys the
     three panels off screen not being painted at all. */
  contain: paint;
  /* every panel is a one-row grid, and that row is the whole panel. Without
     this the row is content-sized and the photographs — which are the only
     things in these compositions asked to stretch — collapse to the height
     of the numeral standing next to them. */
  grid-template-rows: minmax(0, 1fr);
  /* three distances and three landings, one per schedule. --d / --near stay the
     names the numeral uses, because that is the reference and renaming it would
     make every gesture below read as new. */
  --d: calc(var(--c) - var(--qs, 0));
  --ad: max(var(--d), -1 * var(--d));
  --near: clamp(0, calc(1 - var(--ad) * 3.6), 1);
  --da: calc(var(--c) - var(--qa, 0));
  --ada: max(var(--da), -1 * var(--da));
  --neara: clamp(0, calc(1 - var(--ada) * 3.6), 1);
  --dc: calc(var(--c) - var(--qc, 0));
  --adc: max(var(--dc), -1 * var(--dc));
  --nearc: clamp(0, calc(1 - var(--adc) * 3.6), 1);
}
.js .work__panel > * { min-height: 0; }
/* the one-row rule above is right for two of the four; 01 keeps three rows and 03
   keeps two — see the static block */
.js .wp1 { grid-template-rows: minmax(0, 1fr) auto auto; }
.js .wp3 { grid-template-rows: minmax(0, 1fr) auto; }
.js .work__text { transform: translate3d(calc(var(--dc) * -20% * var(--amp)), 0, 0); }
.js .work__rule { transform: scaleX(var(--nearc)); }

/* ── four panels, four kinds of movement ─────────────────────────────────
   The client's note: 拡大するだけじゃなくて、他いろいろな動きで見せる — and a
   first pass at four verbs failed it in a way worth recording. The verbs
   existed in this file but not on the screen, because half of them were
   written against --d, which only reaches ±0.333 while a neighbouring panel
   is anywhere on screen: a rotation declared as 2.4° was 0.8° by the time
   anyone could see it, which is no rotation at all, and the section still
   read as one zoom at four speeds. Every gesture below is now sized by what
   it measures at the moment of hand-off (|--d| = 0.333, --near = 0), not by
   its coefficient:

     01  DEPTH     the photograph comes at the reader, 1.04 → 1.26, while
                   the numeral crosses at ±28% of its own width against it
     02  SHRINK    the numeral arrives over-large and closes on its place,
                   1.62 → 1.00, the five lines join, and the words surface
                   past a hard clip edge
     03  WIPE      the paper plate opens top to bottom on a hard edge, and
                   the numeral rises from below the screen to stand through
                   its bottom edge
     04  ROTATION  the full-bleed plate enters turned a visible 5° and too
                   close, and squares up as it settles — the one photograph
                   on the page allowed off-square

   (Two of those labels used to be the other way round in this file — the 03
   block was over the .wp4 rules and the 04 block over the .wp3 ones. The code
   was always right; only the headings were swapped.)

   All four are reversible, symmetric about the held frame, and driven off the
   panel-local numbers only — six of them now rather than two, three distances
   and three landings, one pair per schedule. Nothing here is a keyframe. */

/* 01 · DEPTH. The numeral has the most travel of the four — 85% of its own
   width each way, so it clears the frame entirely between panels and re-enters
   from the same side the track came from — and the photograph is what carries
   the depth: it is furthest away when the panel is furthest from the frame and
   closest when it arrives. On --da it is also the first of the three to start
   and the first to finish. */
.js .wp1 .work__no  { transform: translate3d(calc(var(--d) * 85% * var(--amp)), 0, 0); }
.js .wp1__plate img {
  transform: translate3d(calc(var(--da) * -9% * var(--amp)), 0, 0)
             scale(calc(1.04 + var(--neara) * 0.22));
}
/* 02 · SHRINK, and it is the other direction from 01 on purpose. The figure
   arrives over-large and closes on its own place instead of opening out of it.
   The words come up from under a hard clip edge — no gradient, no blur:
   inset() has one edge and it is a line.

   transform-origin is at the right edge now that the figure is ranged right
   rather than centred on the panel. Centred, the 1.62 scale threw 98px of
   glyph past the panel's right edge in both directions; anchored, the same
   1.62 → 1.00 closes leftward into the corner it lives in, which is also the
   direction the track is travelling. The amplitude is the same number. */
/* opacity is quadratic in --near, not linear, and the reason is a fragment:
   scaled 1.55×, the "2"'s right edge reaches ~80px into the neighbouring
   held frame, and at a linear 0.36 that sliver read as a stray amber shard
   floating at the screen edge. Squared, the same moment is 0.13 — gone —
   while the landing still runs to full ink. */
.js .wp2__no {
  transform-origin: 100% 50%;
  transform: scale(calc(1.62 - var(--near) * 0.62));
  opacity: calc(0.12 + 0.88 * var(--near) * var(--near));
}
.js .wp2 .work__text {
  clip-path: inset(0 -12% calc((1 - var(--nearc)) * 104%) -12%);
  transform: translate3d(0, calc((1 - var(--nearc)) * 2.2rem * var(--amp)), 0);
}
/* 02 · THE LINES CONNECT, and 六's note is 「派手に描画しない」.

   A soft left-to-right mask, not stroke-dashoffset. Dashing five curves off one
   number needs pathLength="1" to normalise their five different lengths, and
   pathLength's interaction with vector-effect: non-scaling-stroke — which these
   need, because the viewBox is stretched ~7× — is not something to rely on across
   engines. The mask is the same device 4a already uses on the header and on 01's
   plate, it is one composited layer either way, and it reads the same: the lines
   arrive in the order they are drawn, left to right, reaching the junction and
   carrying on past it.

   At --neara 1 the opaque stop is at 100% and the transparent one past the box, so
   the resting frame is an unmasked figure — the same guarantee 4a's masks give.

   It rides --neara and not --near: the drawing IS this card's photograph, so it
   belongs on the image's schedule and lands before the words do.

   The nodes are quadratic in --neara on top of that, so the junctions settle a
   beat after the lines that make them rather than with them. */
.js .wp2__wire {
  -webkit-mask-image: linear-gradient(to right,
    #000 calc(var(--neara, 1) * 128% - 28%), transparent calc(var(--neara, 1) * 128% - 2%));
          mask-image: linear-gradient(to right,
    #000 calc(var(--neara, 1) * 128% - 28%), transparent calc(var(--neara, 1) * 128% - 2%));
}
.js .wp2__wire-n circle { opacity: calc(var(--neara, 1) * var(--neara, 1)); }

/* 04 · ROTATION. The numeral still moves against the plate on the axis the panel
   is read on; the vertical Japanese name that used to move with it is gone, so
   the pair is now the figure and the photograph. 15° of coefficient is 5° at
   hand-off, and 5° on a 1280-wide cover needs scale ≥ cos5° + (w/h)·sin5°
   ≈ 1.17 plus the ~0.03 the sideways drift consumes — the far scale is 1.24
   and the drift is kept to 5% so the corners can never show. */
.js .wp4__no   { transform: translate3d(0, calc(var(--d) * 16% * var(--amp)), 0); }
.js .wp4__plate img {
  transform: translate3d(calc(var(--da) * -5% * var(--amp)), 0, 0)
             rotate(calc(var(--da) * 15deg * var(--amp)))
             scale(calc(1.24 - var(--neara) * 0.13));
}
/* 03 · WIPE. The plate opens downward on a hard edge — the only clip-path on
   a photograph anywhere on the page — and the numeral rises from wholly
   below the screen to its landing, 24% of its own box still under the edge,
   so the cut on it is the screen's own and not a box's. Written into the
   same `translate` property the static rule uses, not into `transform`: the
   two compose. */
.js .wp3__plate { clip-path: inset(0 0 calc((1 - var(--neara)) * 100%) 0); }
/* The resting hang was stated here a second time so that the still frame and the
   conveyor's resting frame would agree. There is no hang any more — the figure
   stands whole inside the corner, see .wp3__no — so the constant goes and only the
   gesture is left. The WIPE is unchanged: 120% still lifts it from wholly below the
   screen, and at --near 1 it lands on 0 instead of on 24%. This is the only line in
   the motion layer this pass touches. */
.js .wp3__no { translate: 0 calc((1 - var(--near)) * 120% * var(--amp)); }
/* 1.18 and not 1.06: the crop is the composition here — see the static rule for
   why the origin is off-centre — and the conveyor has to resolve to the same
   picture the still frame shows. Only the constant changed; the 5% drift is the
   amplitude and it is untouched. */
.js .wp3__plate img { transform: translate3d(0, calc(var(--da) * 5% * var(--amp)), 0) scale(1.18); }

/* THE BREATHING PASS. Client: 画像がずっとじんわり動き続ける感じが欲しい、完全に静止画なのが
   寂しい — a presence just under the threshold of noticing, not a thing to watch. FINALE
   already has the vocabulary for this (.colophon__logo svg, mark-breathe: scale 1 → 1.05,
   8s, infinite alternate); this reuses the same easing and the same device, only slower and
   smaller, because these are large image areas rather than a small mark.

   scale: is the standalone CSS property, not the transform() function, so it composes with
   the scroll-linked transform: already written on each of these elements — individual
   transform properties apply first, then transform, so the parallax survives untouched.

   01/03/04 breathe on the photograph itself. 02 has no photograph — the wire figure
   breathes instead, anchored at its own primary junction (74,44 of the 120×100 viewBox →
   61.7% 44%) so that node does not move at all; the four secondary nodes drift well under
   a couple of screen pixels at this amplitude.

   Amplitude: 04's photograph is already ~1.47× effective scale at 1920 (cover ratio × the
   static 1.11 reframe on .wp4__plate img) because stones.jpg is only 1448px wide — more than +1.5% here
   would push it into visibly softer territory, so +1.5% (→ ~1.49× at the peak) is the cap
   and it is used on all three photographs for one shared number. 02 is vector, so quality is
   not the constraint there; the amplitude is kept close anyway so the four read as one
   gesture, tightened slightly (+1.2%) to keep the wire's node drift as small as possible.

   Durations sit inside the 24–32s window the client asked for after the fps pass below
   (moved from 16–24s: the slower a cycle, the smaller the change any one frame has to
   carry, and it reads more じんわり besides), and negative delays start each card
   partway through its own cycle, so the four are never breathing in phase during the
   horizontal scroll.

   FPS PASS: measured 20ms+ frames up 16–17% → 22–28% across a full-page scroll sweep at
   1440×900 (three passes), against the network register's own stroke-dasharray regression
   fixed above — the two were not isolated from each other, so this pass moves both toward
   safe rather than assigning blame. Amplitude is unchanged (image-quality floor, not a
   cost); only period moves. .wp2__wire stays: it is a `scale` animation on one small
   (~350×292px) SVG root, the same compositor-accelerated path a standard CSS transform/
   scale @keyframes gets without needing will-change, not a per-path repaint — cutting it
   would also break the four cards reading as one gesture, which was the point of this
   pass in the first place. */
@keyframes wp-breathe-photo { from { scale: 1; } to { scale: 1.015; } }
@keyframes wp-breathe-wire  { from { scale: 1; } to { scale: 1.012; } }
.js .wp1__plate img {
  /* breathe stopped; frozen at the mid-breath scale (1 -> 1.015 midpoint), not the 1.0 start */
  scale: 1.0075;
}
.js .wp2__wire {
  transform-origin: 61.7% 44%;
  /* breathe stopped; frozen at the mid-breath scale (1 -> 1.012 midpoint), not the 1.0 start */
  scale: 1.006;
}
.js .wp3__plate img {
  /* breathe stopped; frozen at the mid-breath scale (1 -> 1.015 midpoint), not the 1.0 start */
  scale: 1.0075;
}
.js .wp4__plate img {
  /* breathe stopped; frozen at the mid-breath scale (1 -> 1.015 midpoint), not the 1.0 start */
  scale: 1.0075;
}

.js .work__count-rule { transform: scaleX(clamp(0.02, var(--q, 1), 1)); }
/* THE LEADING FIGURE. Each of the four is 1 when --qs sits on its own dwell value
   and falls linearly to 0 over the ⅓ that separates it from its neighbour, so the
   pair either side of a hand-off cross-fade and sum to 1 throughout — never two
   half-lit figures, never a blank. Because --qs holds its dwell value for 14.5%
   of the pin, the figure is simply constant for the whole of a card's rest. */
.js .work__count-n > span {
  --cd: calc(var(--qs, 0) - var(--v, 0));
  opacity: clamp(0, calc(1 - max(var(--cd), -1 * var(--cd)) * 3), 1);
}
.js .work__count-n > span:nth-child(1) { --v: 0; }
.js .work__count-n > span:nth-child(2) { --v: 0.3333; }
.js .work__count-n > span:nth-child(3) { --v: 0.6667; }
.js .work__count-n > span:nth-child(4) { --v: 1; }

/* ── 4a · the opening assembles ───────────────────────────────────────
   Everything here runs off --p, not --q: a pin cannot publish --q until it
   sticks, and by then the whole section has already ridden up the screen. --p
   is on the pin (data-mo) and runs 1 → 0 across exactly that entry, so --we is
   the entry as 0 → 1 and every window below is inside it. All of them close
   before --we reaches 1, which is the frame the conveyor starts from.

     --we           what arrives
     0.00 – 0.42    nothing. ABOUT is still leaving the screen.
     0.42 – 0.60    FIG. 04 and WHAT WE DO, uncovered left to right
     0.52 – 0.72    the lede, behind them
     0.56 – 0.82    the photograph, wiped open from its own left edge
     0.66 – 0.90    the numeral and the words
     0.80 – 0.95    01 — 04

   Left to right throughout, which is the direction this section is about: the
   conveyor runs the other way, and the frame filling in one way before the track
   pulls the other is the section stating its axis before it uses it.

   The plate is clip-path and not opacity: it already has overflow: clip, so at
   --ep 1 the inset is the box the plate was clipping to anyway and nothing about
   the conveyor changes. Verified by diffing the standing frame. */
.js .work-pin {
  --we:  clamp(0, calc(1 - var(--p, -1)), 1);
  --e1:  clamp(0, calc((var(--we) - 0.42) / 0.18), 1);
  --e2:  clamp(0, calc((var(--we) - 0.52) / 0.20), 1);
  --e3:  clamp(0, calc((var(--we) - 0.56) / 0.26), 1);
  --e4:  clamp(0, calc((var(--we) - 0.66) / 0.24), 1);
  --e5:  clamp(0, calc((var(--we) - 0.80) / 0.15), 1);
  --eh1: calc(var(--e1) * var(--e1) * (3 - 2 * var(--e1)));
  --eh2: calc(var(--e2) * var(--e2) * (3 - 2 * var(--e2)));
  --ep:  calc(var(--e3) * var(--e3) * (3 - 2 * var(--e3)));
  --et:  calc(var(--e4) * var(--e4) * (3 - 2 * var(--e4)));
  --ec:  calc(var(--e5) * var(--e5) * (3 - 2 * var(--e5)));
}
.js .work-head .plate,
.js .work-head__h,
.js .work-head__lede {
  -webkit-mask-image: linear-gradient(to right,
    #000 calc(var(--m) * 132% - 32%), transparent calc(var(--m) * 132% - 4%));
          mask-image: linear-gradient(to right,
    #000 calc(var(--m) * 132% - 32%), transparent calc(var(--m) * 132% - 4%));
  /* no-clip for the same reason ABOUT's labels need it: descenders and any rule
     hung outside the border box are otherwise cut off by mask-clip's default. */
  -webkit-mask-clip: no-clip;
          mask-clip: no-clip;
}
.js .work-head .plate,
.js .work-head__h { --m: var(--eh1); }
.js .work-head__lede { --m: var(--eh2); }
/* A MASK AND NOT A CLIP, because a clip opens at zero width. The first thing a
   hard-edged wipe put on the screen was a 12px column of photograph standing
   alone in the black — at that x the joint picture is a bright grey edge, so it
   read as a stray bar rather than a plate opening. A 20%-wide soft edge means
   what arrives first is a gradient. At --ep 1 the opaque stop is at 100% and the
   box is whole, so the standing frame is untouched. */
.js .wp1__plate {
  -webkit-mask-image: linear-gradient(to right,
    #000 calc(var(--ep) * 122% - 22%), transparent calc(var(--ep) * 122% - 2%));
          mask-image: linear-gradient(to right,
    #000 calc(var(--ep) * 122% - 22%), transparent calc(var(--ep) * 122% - 2%));
}
.js .wp1 .work__no { opacity: var(--et); }
.js .wp1 .work__text {
  opacity: var(--et);
  translate: 0 calc((1 - var(--et)) * 0.4rem);
}
.js .work__count { opacity: var(--ec); }

/* ── the bottom edge, made real ──────────────────────────────────────────
   The 04 numeral is meant to fall off the bottom of the SCREEN, and for a
   build and a half it fell off the bottom of its panel instead — a line 78px
   above the screen's, with the counter and a black band underneath, so the
   glyphs were simply cut mid-stroke with nothing to blame it on. The panel
   row is therefore run all the way to the pin's bottom edge, and the counter
   — which is a HUD for the whole run, not a fourth row of content — becomes
   an overlay on the corner. wp4's foot scrim and wp3's plate scrim already
   hold the counter's ground dark. ≥48rem and .js only: without the conveyor
   the counter stays an ordinary last row under the column. */
@media (min-width: 64rem), (min-width: 48rem) and (pointer: fine) {
  .js .work-pin {
    grid-template-rows: auto minmax(0, 1fr);
    padding-bottom: 0;
  }
  .js .work__count {
    position: absolute;
    left: 0;
    right: 0;
    bottom: clamp(1.5rem, 4svh, 2.5rem);
    z-index: var(--z-raised);
  }
}

/* Below 48rem the conveyor is switched off entirely and the four panels go
   back to being a column. A sideways run needs a screen wide enough that the
   panel leaving and the panel arriving are both legible for a moment, and
   375px is not it — the gesture would be four full-screen jump cuts. The
   page therefore holds two pins on a phone and three on a desktop, which is
   also the right way round: a phone has less patience for a held screen. */
@media (max-width: 47.99rem), (pointer: coarse) and (max-width: 63.99rem) {
  .js .stage--work { height: auto; }
  /* No pin here, so no entry to run off. --p is still published on this element
     but it is now four screens tall and its 1 → 0 has nothing to do with the
     section arriving: measured, it was still at 0.50 when the section's top
     reached the top of the screen, so the header would have been half uncovered
     and the photograph not started. The opening simply stands, which is what a
     column of four blocks wants anyway. */
  .js .work-pin {
    position: static;
    height: auto;
    overflow: visible;
    --eh1: 1; --eh2: 1; --ep: 1; --et: 1; --ec: 1;
  }
  .js .work { display: grid; width: 100%; height: auto; transform: none; }
  .js .work__panel {
    flex: none;
    width: 100%;
    height: auto;
    grid-template-rows: none;
    padding-inline: var(--gutter);
    --d: 0;
    --near: 1;
    --da: 0;
    --neara: 1;
    --dc: 0;
    --nearc: 1;

    /* THE CARD ARRIVES, ONE AT A TIME. data-mo on this element (HTML) publishes
       its own --p exactly like .about-pin's — nothing here is a new mechanism.
       Desktop's conveyor never reads --p off .work__panel, so the attribute is
       inert above 48rem.

       --wp-e turns --p into a 0 → 1 entrance: 0 the instant the card's top
       edge is still at the very bottom of the screen (--p 1), 1 once the
       card is still a little below centre (--p 0.45) — whole well ahead of
       "centred", so a reader who has stopped to read the card above never
       meets one still fading in. Fallback stays −1, "already long past",
       same reason .about-pin's does.

       The ceiling is what keeps a card lit once shown: continuing to scroll
       only pushes --p further negative, which only pushes (1 − --p) further
       past the 0.55 window this divides by, and clamp's upper bound absorbs
       all of that — --wp-e can climb toward 1 but never climbs back down as
       the card leaves. A plain function of --p with no ceiling (the shape
       .wp3__no's own desktop translate uses) would dim the card again the
       moment it started leaving the top of the screen. */
    --wp-e: clamp(0, calc((1 - var(--p, -1)) / 0.55), 1);

    /* five groups on their own window of --wp-e, each run through smoothstep
       so the fade itself isn't linear: numeral, then the service name
       (EN/JP + its rule), then the lead line, then the note, then the
       photograph-or-wire last — a fixed 0.09 stagger, identical on all four
       cards, so none of them reads as assembling differently from the rest. */
    --wp-no-r: clamp(0, calc((var(--wp-e) - 0.00) / 0.42), 1);
    --wp-no: calc(var(--wp-no-r) * var(--wp-no-r) * (3 - 2 * var(--wp-no-r)));
    --wp-nm-r: clamp(0, calc((var(--wp-e) - 0.09) / 0.42), 1);
    --wp-nm: calc(var(--wp-nm-r) * var(--wp-nm-r) * (3 - 2 * var(--wp-nm-r)));
    --wp-ld-r: clamp(0, calc((var(--wp-e) - 0.18) / 0.42), 1);
    --wp-ld: calc(var(--wp-ld-r) * var(--wp-ld-r) * (3 - 2 * var(--wp-ld-r)));
    --wp-nt-r: clamp(0, calc((var(--wp-e) - 0.27) / 0.42), 1);
    --wp-nt: calc(var(--wp-nt-r) * var(--wp-nt-r) * (3 - 2 * var(--wp-nt-r)));
    --wp-ph-r: clamp(0, calc((var(--wp-e) - 0.36) / 0.42), 1);
    --wp-ph: calc(var(--wp-ph-r) * var(--wp-ph-r) * (3 - 2 * var(--wp-ph-r)));
  }
  /* opacity + a small upward settle, capped at 1rem (16px) — the "ふわっと"
     the brief asks for is opacity-led, so the move is a garnish, never the
     point. The photograph/wire gets opacity only: moving it would fight the
     scroll it is riding on. */
  .js .work__panel .work__no { opacity: var(--wp-no); translate: 0 calc((1 - var(--wp-no)) * 1rem); }
  .js .work__panel .work__en,
  .js .work__panel .work__jp,
  .js .work__panel .work__rule {
    opacity: var(--wp-nm);
    translate: 0 calc((1 - var(--wp-nm)) * 1rem);
  }
  .js .work__panel .work__lead { opacity: var(--wp-ld); translate: 0 calc((1 - var(--wp-ld)) * 1rem); }
  .js .work__panel .work__note { opacity: var(--wp-nt); translate: 0 calc((1 - var(--wp-nt)) * 1rem); }
  .js .wp1__plate, .js .wp2__wire, .js .wp3__plate, .js .wp4__plate { opacity: var(--wp-ph); }
  .js .wp1, .js .wp3 { grid-template-rows: none; }
  .js .work__text, .js .wp1 .work__no, .js .wp4__no,
  .js .wp1__plate img, .js .wp3__plate img {
    transform: none;
  }
  /* 04 is off this list because its phone crop IS a transform — the portrait
     plate cannot be trimmed any other way (see .wp4__plate img in the static
     block above). What has to go here is the conveyor's translate, and this
     restates the resting scale without it rather than clearing both. */
  .js .wp4__plate img { transform: scale(1.35); }
  .js .wp2__no { transform: none; opacity: 1; }
  /* --neara is pinned to 1 above so both of these already evaluate to the drawn
     state; stated anyway, for the same reason the clips below are. */
  .js .wp2__wire { -webkit-mask-image: none; mask-image: none; }
  .js .wp2__wire-n circle { opacity: 1; }
  /* the conveyor is off, so there is no "which panel is up" to report — the
     counter goes back to being the section's label. */
  .js .work__count-n > span { opacity: 0; }
  .js .work__count-n > span:nth-child(1) { opacity: 1; }
  /* --near is pinned to 1 above, so the two clips are already inset(0);
     stated anyway, because a clip-path that silently evaluates to nothing is
     a thing that breaks the next time someone touches the formula. The 04
     numeral is in normal flow on a phone — nothing below 48rem is clipped,
     so nothing may hang below its own panel. */
  .js .wp2 .work__text, .js .wp3__plate { clip-path: none; transform: none; }
  .js .wp3__no { translate: none; }
}

/* ── 5 · the light band 【強】 ────────────────────────────────────────
   A shutter over a lamp, and the only inversion on the page.

     --lum  0 → 1  over q 0.04 → 0.46   the lamp comes up behind the blades
     --ap   0 → 1  over q 0.02 → 0.42   the blades part
     --w    0 → 1  over q 0.44 → 0.60   the words rise, on paper that is
                                        already 95% of the way up at 0.44 —
                                        no word on this page is ever legible
                                        against a half-lit ground
     --cl   0 → 1  over q 0.80 → 0.86   the blades close
     --k             q 0.855 → 1        the impulse, published by JS

   --sh is the shutter's closedness: 0 open, 1 shut. The impulse is added to
   it, so a value over 1 is the two blades past each other and a value under
   1 is a gap. They arrive with an overshoot of 8.5% of a blade and rebound
   to −1.6%, which at this blade height is about thirteen pixels of light
   let back out at 1280×800 before it settles. That is the ガチャン, and it
   is the only percussive moment on the page — a second one would make it
   a rhythm instead of an event. */
.js .light {
  --lum: clamp(0, calc((var(--q, 1) - 0.04) / 0.42), 1);
  --ap:  clamp(0, calc((var(--q, 1) - 0.02) / 0.40), 1);
  --w:   clamp(0, calc((var(--q, 1) - 0.44) / 0.16), 1);
  --cl:  clamp(0, calc((var(--q, 1) - 0.80) / 0.06), 1);
  --sh:  calc(1 - var(--ap) + var(--cl) + var(--k, 0) * 0.13);
  background: var(--void);
}
.js .light__room {
  background: color-mix(in oklab, var(--void), var(--paper) calc(var(--lum) * 100%));
}
/* the plate label and the message are on the lamp's own timing, not the
   shutter's, so the band still reads as a room being lit and then read in */
.js .light__plate { opacity: clamp(0, calc((var(--lum) - 0.55) * 2.6), 1); }
.js .light__h, .js .light__en {
  opacity: var(--w);
  transform: translate3d(0, calc((1 - var(--w)) * 1.5rem * var(--amp)), 0);
}
/* Each blade is 70.6svh but only travels 50.6 — the extra 20svh hangs off
   the top and bottom of the screen and is what the impact swings into. A
   blade sized exactly to its half and anchored to the edge it covers gets
   pushed off that edge by the overshoot: measured at k=0.88 the pair had
   slid 46px past each other and opened a 46px strip of lit room along the
   top and bottom of the screen, which is the opposite of a shutter closing.

   They rest 1.2svh overlapped, not 4. Enough that no seam shows at rest —
   sub-pixel rounding on two independently transformed layers will find a 1px
   one — and little enough that the rebound clears it: at 4% the blades
   sprang 22px apart and were still 10px overlapped, so the impact had no
   light in it at all. Now the rebound opens 23px, measured. */
.js .blade {
  display: block;
  position: absolute;
  left: -1px;
  right: -1px;
  height: 70.6svh;
  z-index: var(--z-shutter);
  background: var(--void-3);
  will-change: transform;
}
.js .blade--t { top:    -20svh; transform: translate3d(0, calc((var(--sh) - 1) * 50.6svh), 0); }
.js .blade--b { bottom: -20svh; transform: translate3d(0, calc((1 - var(--sh)) * 50.6svh), 0); }
/* the lit edge of each blade — what a shutter looks like when there is a
   lamp on the other side of it */
.js .blade--t::after, .js .blade--b::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  height: 1px;
  background: rgb(255 217 160 / calc(var(--lum) * 0.55));
}
.js .blade--t::after { bottom: 0; }
.js .blade--b::after { top: 0; }

/* ── 6 · contact 【中】 ─────────────────────────────────
   THE APERTURE IS GONE, and with it --o, --z, the circular clip-path, the
   scale-toward-the-reader and the plate's own DOM. What it cost was not just a
   metaphor introduced at the last moment — it also held the section's words back
   until the plate had dissolved, which is where the long black screen came from:
   .close-words did not reach full opacity until --z passed 0.62, and --z did not
   start until --p was under 0.42.

   Everything now reads --p directly and starts early. One line, then the label, the
   heading, and the two columns in the order a reader needs them.

     --p        what moves                        (--p counts DOWN from 1)
     1.00–0.80  --line   the handed-over line arrives and contracts
     0.86–0.68  --lab    FIG. 06, then CONTACT
     0.74–0.52  --cols   the form and the record rise together

   The line is the join. The light band closes on two blades whose meeting edges are
   1px lit lines; this one is the same 1px, the same warm amber, and it comes in at
   13x its own width so what the reader sees is the wide line of the closed shutter
   drawing in rather than a new rule being drawn. scaleX only — no layout, and the
   band already clips. */
.js .band--contact {
  --line: clamp(0, calc((1 - var(--p, 1)) / 0.2), 1);
  --lab:  clamp(0, calc((0.86 - var(--p, 1)) / 0.18), 1);
  --cols: clamp(0, calc((0.74 - var(--p, 1)) / 0.22), 1);
}
.js .close-line {
  transform: scaleX(calc(1 + (1 - var(--line)) * 12));
  transform-origin: center;
  opacity: var(--line);
  will-change: transform;
}
.js .close-words > .plate,
.js .close-lede {
  opacity: var(--lab);
  transform: translate3d(0, calc((1 - var(--lab)) * 0.55rem * var(--amp)), 0);
}
.js .colophon__cols {
  opacity: var(--cols);
  transform: translate3d(0, calc((1 - var(--cols)) * 0.7rem * var(--amp)), 0);
}

/* ── 7 · colophon 【終】 ────────────────────────────────────
   THE MARK IS SET DOWN. It is the one thing on this page that arrives downward —
   everything else rises — and that was already the idea when this was a 320px
   strip. On a screen of its own it gets room to be the last beat rather than a
   detail: the mark comes from above and slightly large, and settles.

   Off --p, not off an observer. This footer now carries data-mo, so main.js
   publishes the same +1 below → 0 centred value it publishes for ABOUT and CONTACT,
   and the ending is scroll-linked like every other movement on the page. The
   is-in one-shot it replaces fired once at a threshold and then the screen was
   inert while you were still arriving at it.

   The order is the sentence: the mark lands, the rule draws from its lit point, the
   name comes up under it, and the furniture last. Four elements, each reading one
   published number — no per-element observers and nothing measured in JS.

   AND THEN IT BREATHES. I wrote that nothing here loops and that an ending which
   keeps moving is not an ending; the client asked for the opposite and he is right
   about the effect he wants: 「マークをじんわり拡大していく…完全な静止じゃなくてちょっと
   要因がある感じにしたい」. A composition that has landed and is then perfectly frozen
   reads as a screenshot rather than as a place you have arrived at.

   5% over 8 seconds, alternating. It was 3.2% over 11s first, aiming at 「本当にすごい
   微妙に」, and that landed under the threshold rather than at it — 「マークも変化しなさ
   そうやし」. 3.2% of 210px is 6.7px across eleven seconds, slow enough that nothing
   appears to happen at all. 5% over 8s is 10.5px: sensed without being watchable.
   You still cannot catch it moving, but the screen is no longer a still.

   It runs on the SVG, not on the anchor, and that is structural rather than
   cosmetic: the anchor's transform is the descent, driven by --p, and an animation
   on the same property would win the cascade and the descent would stop happening.
   Two elements, two jobs.

   It also costs the page its idle. main.js parks itself when every value has settled
   but a running animation keeps the compositor awake — one composited scale on one
   element, which is the cheapest possible form of it, and it is only ever on the
   last screen. Under prefers-reduced-motion the .js class is never added, so this
   whole block never applies and the finale is a still. */
@keyframes mark-breathe { from { scale: 1; } to { scale: 1.05; } }
/* One schedule, written as four windows on --p, which counts DOWN from 1 to 0 as
   the screen arrives. The first version had the mark's opacity as 1 - p*1.7, which
   only reaches 1 at p = 0 — so the rule, the name and the furniture all completed
   before the mark did, and the sentence ran backwards. Measured that way and fixed:

     mark        p 0.80 → 0.34    comes from above, slightly large, and settles
     rule        p 0.40 → 0.24    drawn from its lit end, after the mark has landed
     name        p 0.30 → 0.16    up under the rule
     furniture   p 0.20 → 0.06    last, and it is furniture

   The mark's descent reads --m rather than --p directly, so it stops moving at the
   same instant it stops fading. Past p 0.34 the mark is still while the rest of
   the screen assembles beneath it, which is what makes it read as landed. */
.js .colophon {
  --m: clamp(0, calc((0.80 - var(--p, 0)) / 0.46), 1);
}
.js .colophon__logo {
  transform: translate3d(0, calc((1 - var(--m)) * -3.6rem * var(--amp)), 0)
             scale(calc(1 + (1 - var(--m)) * 0.055));
  opacity: var(--m);
  will-change: transform;
}
.js .colophon__logo svg {
  /* breathe stopped; frozen at the mid-breath scale (1 -> 1.05 midpoint), not the 1.0 start */
  scale: 1.025;
}
.js .colophon__rule {
  transform: scaleX(clamp(0, calc((0.40 - var(--p, 0)) / 0.16), 1));
  transform-origin: left center;
}
.js .colophon__name {
  opacity: clamp(0, calc((0.30 - var(--p, 0)) / 0.14), 1);
  translate: 0 calc(var(--p, 0) * 0.9rem);
}
.js .colophon__foot {
  opacity: clamp(0, calc((0.20 - var(--p, 0)) / 0.14), 1);
  translate: 0 calc(var(--p, 0) * 0.6rem);
}
