/* ════════════════════════════════════════════════════════════════════════
   css/work-portfolio.css — /work and /work/camera ONLY
   service.tnaado.ca

   WHY A NEW FILE. Ten agents share this repo. css/work.css is also linked by
   /work/software and css/work-camera.css predates this pass, so neither is
   edited here. This file is loaded AFTER them by the two pages that own it
   and adds only what those two pages gained:

     1. THE DISCLOSURE  (.wk-disc)   — <details>, native, no script.
     2. THE SCROLL LIFT (.wk-rise)   — CSS scroll-driven, no script.
     3. THE CAROUSEL FIT (.carou--sheet) — geometry + token bridge so the
        PORTED carousel (js/carousel.js + css/components/carousel.css) sits
        on the contact sheet's PAPER ground instead of the site's ink.
     4. Two small lists, .wk-who and .wk-inside.

   NO NEW ANIMATION ENGINE. The site has exactly one — the WebGL2 storm on
   /technology, /media, /newsroom and /apps. Neither page here is a storm
   page (see the note at .wk-rise), so the motion below is the browser's
   own: a details height transition and a view()-timeline lift. Both are
   declarative, both no-ops under prefers-reduced-motion, and neither
   introduces a second scroll loop that could disagree with the first.

   TOKENS ONLY. Every value below is either a token declared by the page's
   own stylesheet (css/work.css or css/work-camera.css) or a length in the
   same clamp() family those files already use. Nothing new is invented.
   ════════════════════════════════════════════════════════════════════════ */


/* ══════════════════════════════════════════════════════════════════════════
   1 · THE DISCLOSURE

   Ethan asked for "dynamic buttons / click to read more / dropdowns" by
   name. This is a bare <details> and a <summary>: it opens with no script,
   it is keyboard operable by construction, the closed text stays in the DOM
   so find-in-page and a crawler both reach it, and the browser scrolls to
   and opens a closed <details> when the reader searches text inside it.

   hidden="until-found" is NOT used, matching the ruling already recorded in
   css/contact.css: it is the tool for a collapsed region that is not a
   <details>, and putting it on details content fights the element's own
   open/closed state.

   The transition is ::details-content + interpolate-size, exactly as
   .ctq in css/contact.css. Where ::details-content is unsupported the rule
   drops and the row opens instantly — a supported state, not a broken one.
   ══════════════════════════════════════════════════════════════════════════ */
.wk-disc {
  --wkd-line: var(--hair-ink);
  --wkd-q:    var(--bone);
  --wkd-a:    var(--dim-ink);
  --wkd-mark: var(--red-ink);
  border-top: 1px solid var(--wkd-line);
  /* Opts `auto` into being an animatable length. Inherited, so it has to sit
     on the <details> and not on the pseudo-element. */
  interpolate-size: allow-keywords;
}

.wk-disc__q {
  display: flex; align-items: center; gap: .7rem;
  padding: .8rem .1rem;
  cursor: pointer;
  list-style: none;
  font-family: var(--mono);
  font-size: .75rem; font-weight: 500;
  letter-spacing: .13em; text-transform: uppercase;
  color: var(--wkd-q);
}
/* Three engines, three ways of drawing the default triangle. */
.wk-disc__q::-webkit-details-marker { display: none; }
.wk-disc__q::marker { content: ''; }

/* THE MARKER IS DRAWN, NOT TYPED — see section 5. It used to be content: '+'
   swapped for content: '\2212' on [open]. Both rules are gone rather than
   overridden: the [open] one carried higher specificity than section 5's
   plain .wk-disc__q::after, so leaving it in place re-asserted the MINUS
   GLYPH underneath the drawn bar and the open marker painted as two rules,
   one long and one short. Overriding a superseded rule from a later section
   is how that happens; deleting it is why it cannot happen again. */

/* Inset: a 2px outset ring on a full-width summary is clipped by the row's
   own top hairline. */
.wk-disc__q:focus-visible {
  outline: 2px solid var(--wkd-mark);
  outline-offset: -3px;
  border-radius: 2px;
}

.wk-disc__a {
  padding: 0 0 1rem;
  max-width: 62ch;
  font-family: var(--sans);
  font-size: .875rem; line-height: 1.6;
  color: var(--wkd-a);
}
.wk-disc__a p { margin: 0; }
.wk-disc__a p + p { margin-top: .7rem; }
.wk-disc__a b { color: var(--wkd-q); font-weight: 600; }

.wk-disc::details-content {
  block-size: 0;
  overflow: clip;
  transition:
    block-size .3s var(--ease),
    content-visibility .3s allow-discrete;
}
.wk-disc[open]::details-content { block-size: auto; }

/* A reader who asked for less motion gets the disclosure with no height
   animation at all — the row is instantly open or instantly closed. Same
   ruling as .ctq in css/contact.css. Measured omission: without this the
   0.3s block-size transition still ran under prefers-reduced-motion. */
@media (prefers-reduced-motion: reduce) {
  .wk-disc::details-content,
  .wk-disc[open]::details-content { transition: none; }
}

/* THE PAPER VARIANT. The contact sheet is the one light ground in this pair,
   so the four locals are re-pointed rather than the rules being written
   twice. --ink-body/--ink-mute/--hair-bone/--red-bone are all declared by
   css/work-camera.css and all measured there against --paper. */
.wk-disc--paper {
  --wkd-line: var(--hair-bone);
  --wkd-q:    var(--ink);
  --wkd-a:    var(--ink-mute);
  --wkd-mark: var(--red-bone);
}


/* ══════════════════════════════════════════════════════════════════════════
   2 · THE SCROLL LIFT

   WHY NOT THE STORM. The site's one animation engine is the WebGL2 storm
   whose camera drives both the GL uniforms and the DOM matrix3d transforms.
   It is a whole-page scene: a sticky stage over a multi-viewport track, with
   every body absolutely positioned inside it. It earns that on /technology,
   /media, /newsroom and /apps, which are arguments the reader walks through.

   /work is a signpost with two doors. Putting the second door behind a
   several-screen cinematic is the opposite of routing someone. /work/camera
   is photographs, and the storm's plates are 21rem wide — it would shrink
   the evidence to thumbnails and fly it away from the reader.

   So: no scene, no second scroll loop, no rival camera. The lift below is
   the browser's own view() timeline, which needs no JavaScript, cannot
   disagree with the storm's follower because there is no storm on these two
   pages, and is inert wherever it is unsupported.

   It is wrapped in BOTH a @supports and a prefers-reduced-motion:
   no-preference query, so a reader who asked for less motion never gets an
   opacity or a transform, and an engine without the timeline paints the
   final state — which is the plain document.
   ══════════════════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    @keyframes wk-rise {
      from { opacity: 0; transform: translateY(1.5rem); }
      to   { opacity: 1; transform: none; }
    }
    .wk-rise {
      animation: wk-rise linear both;
      animation-timeline: view();
      /* Finishes well before the element is centred, so a reader is never
         reading text that is still moving. */
      animation-range: entry 5% cover 26%;
    }
  }
}

/* THE ARRIVAL, FOR A PAGE THAT DOES NOT SCROLL.

   Measured: /work at 1440x900 has a maximum scrollTop of 223px. A view()
   timeline on a door there is already past the end of its range on frame one,
   so the scroll lift is a no-op at desktop widths and the page had no motion
   at all beyond hover. The doors therefore ARRIVE instead: one short
   translate-and-fade, the second door 90ms behind the first, so the pair reads
   as two objects landing rather than one block appearing.

   Kept under half a second, because this is a signpost and the reader's first
   act is to click one of the two things being animated. `both` so the first
   painted frame is the start of the animation and not a flash of the end. */
@media (prefers-reduced-motion: no-preference) {
  @keyframes wk-enter {
    from { opacity: 0; transform: translateY(1.15rem); }
    to   { opacity: 1; transform: none; }
  }
  .wk-enter    { animation: wk-enter .44s var(--ease) both; }
  .wk-enter--2 { animation-delay: .09s; }
}

/* …AND THE HANDOFF, FOR THE WIDTH WHERE THAT ARRIVAL PLAYS TO NOBODY.

   The reasoning above holds at 1440 and breaks below it. Measured at 390x844:
   the doors stack, /work grows to 721px of scroll, and the SECOND door's top
   sits at y=951 — 107px past the fold. Its .wk-enter therefore runs and
   finishes, opacity 0 -> 1 over 440ms, entirely off-screen; by the time a
   reader scrolls down to it the animation is long settled and he sees a plain
   static card. The motion is not subtle there, it is absent.

   So at narrow widths the doors are handed to the view() timeline instead,
   which fires when the door actually reaches the reader. Nothing else changes:
   same keyframes, same range as .wk-rise, both animations are on the same
   properties so the later rule simply wins, and it stays inside the existing
   no-preference + @supports pair, so a reduced-motion reader and an engine
   without the timeline both still get the finished document.

   900px is the width at which the two doors stop sharing a row. */
@media (prefers-reduced-motion: no-preference) and (max-width: 900px) {
  @supports (animation-timeline: view()) {
    .wk-door.wk-enter {
      animation: wk-rise linear both;
      animation-timeline: view();
      animation-range: entry 5% cover 26%;
    }
    /* The 90ms stagger is what makes two side-by-side doors read as a pair
       landing. Stacked, each door arrives on its own scroll position, so the
       delay would only postpone a card the reader is already looking at. */
    .wk-door.wk-enter--2 { animation-delay: 0s; }
  }
}


/* ══════════════════════════════════════════════════════════════════════════
   3 · /work — THE TWO DOORS

   The <a class="route"> and everything inside it is css/work.css's, and it
   is left alone: the 16:9 frame, the gradient, the hover scale and the
   arrow slide are already right. What is added is the wrapper that lets a
   <details> sit BELOW the link — interactive content cannot go inside an
   <a>, which is why the disclosure is a sibling and not a child.
   ══════════════════════════════════════════════════════════════════════════ */
.wk-door { display: flex; flex-direction: column; min-width: 0; }
.wk-door > .route { flex: 1 1 auto; }

/* THE TWO CTAs SIT ON THE SAME LINE AS EACH OTHER, whatever the rows above
   them do. Measured at 1440: naming the two App Store apps instead of counting
   them wrapped the technology door's meta row onto a second line, which pushed
   its "Open the technology portfolio" 27px below the media door's and left a
   visible hole under the media one. The body is a column and the cue is
   bottom-aligned inside it, so the pair stays level however either row wraps —
   and the copy is free to be the right length rather than a length that
   happens to fit. */
.wk-door > .route .route__body { display: flex; flex-direction: column; flex: 1 1 auto; }
.wk-door > .route .route__go { margin-top: auto; padding-top: 1.15rem; }
.wk-door > .wk-disc {
  border-top: 0;
  border-left: 1px solid var(--hair-ink);
  border-right: 1px solid var(--hair-ink);
  border-bottom: 1px solid var(--hair-ink);
  background: var(--plate);
  padding-inline: clamp(1.25rem, 2.5vw, 1.75rem);
}

/* WHAT IS ACTUALLY IN THERE. A list of named things, and the own-brand /
   client label is the first thing on each row because it is the honest half
   of the line. No sentence explains the labelling; the label is the whole
   statement. */
.wk-inside { margin: 0; padding: 0; list-style: none; }
.wk-inside > li {
  display: grid; gap: .1rem .9rem;
  padding: .5rem 0;
  border-top: 1px solid var(--hair-ink);
}
.wk-inside > li:first-child { border-top: 0; padding-top: 0; }
@media (min-width: 30rem) {
  .wk-inside > li { grid-template-columns: 11.5rem minmax(0, 1fr); align-items: baseline; }
}
.wk-inside b {
  font-family: var(--mono); font-size: .6875rem; font-weight: 500;
  letter-spacing: .13em; text-transform: uppercase;
  color: var(--red-ink);
}
.wk-inside span { color: var(--bone); }

/* THE COUNT ROW, LEVELLED. The two doors are grid siblings, so the row is as
   tall as the taller one. When the technology disclosure is open on nine named
   things and the media one on four, the shorter door used to absorb the
   difference as a hole in the MIDDLE of its card, above its own CTA. Measured
   at 1440: a 378px panel beside a 181px panel left roughly 200px of empty
   plate under "Client shoots". Two changes fix it: the meta row is a stack, so
   both bodies are the same height with either disclosure shut, and the doors
   align to the START of the row rather than stretching, so an open list makes
   its OWN card taller and never distorts its neighbour. */
.routes { align-items: start; }
.wk-door > .route .route__count { display: grid; gap: .1rem; }


/* ══════════════════════════════════════════════════════════════════════════
   4 · /work/camera — THE PORTED CAROUSEL ON THE CONTACT SHEET

   The carousel is js/carousel.js + css/components/carousel.css, ported and
   already in service on /firm. Nothing about its mechanics is re-implemented
   here: this block is the two things a drop-in component cannot know, which
   are how wide a card should be on THIS page and which ground it is sitting
   on.
   ══════════════════════════════════════════════════════════════════════════ */

/* GEOMETRY. The component's default card is clamp(15.5rem, 78vw, 23rem) —
   right for a project card, wrong for a photograph: 23rem lands a 3:2 frame
   at about 368px wide, which is a thumbnail. A frame is the evidence on this
   page, so the card runs to 30rem and two sit in the rail at 1440. */
.carou--sheet { margin-top: 0; }

/* THE SHEET'S INSET, AND WHY IT NEEDS scroll-padding AND NOT JUST padding.

   Measured defect, twice. The inset started life as `padding-inline` on
   .carou__track; a `scroll-snap-align: start` item snaps its own left edge to
   the SNAPPORT's left edge, and the snapport is the scrollport inset by
   scroll-padding — which was 0 — so frame 01 came to rest at scrollLeft = 26
   rather than 0. js/carousel.js reads `x <= 2` for "at the start", so the
   Previous arrow shipped ENABLED on a strip that could not go back. Measured
   at 1440: scrollLeft=26, prev=false, counter 01/08.

   Moving the same padding onto the scroll container did NOT fix it, for the
   same reason: padding on a scroll container sits inside the scrollable
   overflow area, so the content still begins 26px in and the snap still lands
   at 26. The inset has to be declared to the SNAPPORT as well, which is what
   scroll-padding-inline does. Then frame 01 rests at exactly 0, the arrow
   disables, and every subsequent card still snaps flush to the inset. */
.carou--sheet .carou__viewport {
  padding: clamp(1rem, 3vw, 1.6rem) clamp(1rem, 3vw, 1.6rem);
  scroll-padding-inline: clamp(1rem, 3vw, 1.6rem);
}
.carou--sheet .carou__track {
  /* The frame numbers are a CSS counter that .sheet__grid used to reset.
     The grid is gone; the track owns the counter now, so 01 is still 01. */
  counter-reset: frame;
  padding: 0;
}
.carou--sheet .carou__item { flex: 0 0 clamp(15.5rem, 84vw, 30rem); }

/* A card is now a link ABOVE a disclosure, so it stacks. */
.carou--sheet .frame { flex-direction: column; }
.carou--sheet .frame__link { flex: 1 1 auto; }

/* --frame-meta-min held three clamped lines of description at a fixed height
   so that a card WITHOUT a description cost the grid nothing — every card was
   the same height whether or not it had a .frame__desc. The descriptions have
   moved into the disclosure, so no card has one, and the reserve became about
   90px of empty paper under every title. Measured at 1440. The cards are equal
   height here because the carousel stretches them, so the reserve is not doing
   any work either. */
.carou--sheet .frame__meta { min-height: 0; }
.carou--sheet .frame > .wk-disc {
  border-left: 1px solid var(--hair-bone);
  border-right: 1px solid var(--hair-bone);
  border-bottom: 1px solid var(--hair-bone);
  border-top: 0;
  background: #fff;
  padding-inline: clamp(.85rem, 2.2vw, 1.1rem);
}

/* THE TOKEN BRIDGE, and it is scoped to the CONTROL BAR ONLY.
   css/components/carousel.css was written for the site's ink ground and
   reads --bone / --dim-ink / --hair-ink / --plate-2 / --red-ink for its
   counter and its two arrows. On this page those names hold ink-ground
   values and the sheet is paper, so the bar would render bone-on-paper.

   Re-pointing them on .carou__bar and NOT on .carou is deliberate: .frame__n
   is a dark chip INSIDE the photograph and it paints its numeral with
   var(--bone). Bridging the whole carousel would turn every frame number
   black on black. Measured: the bar is the only carousel chrome on the
   sheet, so the bar is the only place the bridge belongs.

   WHY NOT .fp-band--bone, WHICH PORTED-COMPONENTS.md §1 DOCUMENTS AS THE
   LIGHT-GROUND HOOK. That variant sets `color` DIRECTLY from --dim-bone and
   --ivory, and css/work-camera.css declares neither: it has --ink-body,
   --ink-mute and --hair-bone, all measured against --paper, which is this
   page's actual ground and not bone. Taking the documented hook would set the
   counter to an undefined custom property and leave it on its inherited
   colour, and its `color` declaration would beat this bridge on specificity,
   so the two cannot both be used. Checked against the file, not assumed. */
.carou--sheet .carou__bar {
  --bone:     var(--ink);
  --dim-ink:  var(--ink-mute);
  --hair-ink: var(--hair-bone);
  --plate-2:  var(--plate);
  --red-ink:  var(--red-bone);
  margin: 0;
  padding: .35rem clamp(1rem, 3vw, 1.6rem) clamp(1rem, 3vw, 1.6rem);
}

/* THE GRADE. PHOTO-RULING.md rules the Stellar frame STRONG but "needs a
   grade — it is a flat 16:9 video grab with a milky low-contrast veil. Lift
   contrast, set black point." Done in the browser rather than by rewriting
   the asset, so the original file is untouched and the call stays reversible
   in one line. Applied to the frame that was named, not to photography in
   general. */
.frame__box--grade img { filter: contrast(1.16) saturate(1.05) brightness(.97); }

/* THE LEVEL AND THE WHITE BALANCE ON THE SALON SHELF. Ⅶ, same file: "pull
   ~700K off the white balance and level ~1 degree clockwise" — the shelf edge
   runs downhill left to right and the warm cove LED puts a heavy amber cast on
   a wall that is not that colour. The rotation is counter-clockwise in CSS
   terms because that is what raises the low right end; the scale covers the
   corners the rotation would otherwise expose. Both niches stay in frame,
   which is the constraint the ruling put on any crop of this frame. */
.frame__box--level img {
  transform: rotate(-1deg) scale(1.045);
  filter: saturate(.9) contrast(1.04) hue-rotate(-5deg);
}

/* THE PACKAGING CROP. Ⅰ, same file: this is "the best packaging frame in the
   repo" with two faults — "a blue salon disinfectant jar soft in the left
   background, and a bare cardboard carton behind... Crop both out and it is
   excellent." Read off the pixels, everything that matters sits in the right
   two thirds; the left third is a black spray bottle and the blue jar. A 3:2
   file in a 3:2 box cannot be cropped with object-position, so the crop is a
   scale about the right edge, biased up so the standing carton keeps every
   line of its copy. The asset is untouched and the call is one line to undo. */
.frame__box--tight-r img {
  transform: scale(1.62);
  transform-origin: 100% 46%;
}

/* ══════════════════════════════════════════════════════════════════════════
   5 · /work/camera — THE CLIENT INDEX

   NAMED, NOT COUNTED. This replaced the three-figure tally that used to open
   the page: who the work was for, with no total that goes stale the next time
   a job lands.

   AND IT IS NOW A CONTROL. It was four inert <li>s, and the FIRST ONE carried
   a red border — so it looked selected while nothing was selectable, which is
   the worst kind of decoration: a control that lies. Each name is now a link
   to that client's first frame on the sheet, aria-current follows the frame
   the reader is actually on, and the red edge belongs to whichever name that
   is instead of to whichever happens to be written first.
   ══════════════════════════════════════════════════════════════════════════ */
.wk-who {
  display: flex; flex-wrap: wrap;
  gap: .45rem .5rem;
  margin: 1.4rem 0 0; padding: 0;
}
.wk-who__go {
  display: block;
  padding: .3rem .65rem;
  border: 1px solid var(--hair-ink);
  font-family: var(--mono); font-size: .6875rem; font-weight: 500;
  letter-spacing: .12em; text-transform: uppercase;
  color: var(--dim-ink);
  text-decoration: none;
  /* Colour and border only. A transform here would move a hit target under a
     finger that is already on it. */
  transition: color .18s var(--ease), border-color .18s var(--ease),
              background-color .18s var(--ease);
}
.wk-who__go:hover,
.wk-who__go:focus-visible { color: var(--bone); border-color: var(--dim-ink); }
.wk-who__go:focus-visible { outline: 2px solid var(--red-ink); outline-offset: 2px; }

/* THE READER'S POSITION, and it is the only red on the row. Set by
   js/work-sheet.js off the frame currently under the reader — so with the
   script blocked no name is marked, which is honest: nothing is tracking. */
.wk-who__go[aria-current="true"] {
  color: var(--bone);
  border-color: rgba(200, 16, 46, .55);
  background: rgba(200, 16, 46, .07);
}
@media (prefers-reduced-motion: reduce) {
  .wk-who__go { transition: none; }
}


/* ══════════════════════════════════════════════════════════════════════════
   5 · THE PRESS, AND A MARKER THAT MOVES

   Two things every pressable element on these pages was missing.

   (a) PRESS FEEDBACK. css/work.css gives .route a hover and a focus ring but
   its :active is a border colour, and .wk-disc__q had no pressed state at
   all. A control that does not answer the finger reads as a picture of a
   control. Both get a scale, and the scale is DELIBERATELY tiny: 0.994 on a
   506px card is about three pixels of travel, which is felt and not seen.
   The usual 0.97 is calibrated for a button the size of a thumb; applied to
   half a viewport it reads as the page lurching.

   NOT gated behind (hover: hover). Hover states are, because a touch device
   fires them on tap and leaves them stuck — but :active is the one piece of
   feedback a touch reader actually depends on, so it ships everywhere.

   transform is safe to claim here. css/work.css animates .route__frame img
   and .route__go i, both CHILDREN, so nothing is fighting over a property:
   the card scales and its own image slides inside it, composing instead of
   overriding. This is the mistake called out in PORTED-COMPONENTS.md §7 and
   it is avoided by staying off the elements that already own a transform.

   (b) THE MARKER. It was content: '+' swapped for content: '\2212' on
   [open] — a substitution, which cannot transition, so the one moving part
   of the disclosure snapped while the height beside it eased. The plus is
   now DRAWN as two bars: ::after is the horizontal one and never moves,
   ::before is the vertical one and rotates flat. Closed reads +, open reads
   −, and the quarter turn between them runs on the same .3s var(--ease) as
   the height, so the row opens as one gesture instead of two.

   ::before is positioned rather than laid out because a summary is a flex
   container and a second pseudo-element would otherwise become a flex item
   at the START of the row. right: .1rem matches the summary's own inline
   padding, which is what puts it exactly over ::after.
   ══════════════════════════════════════════════════════════════════════════ */

.wk-door > .route { transition: border-color .2s var(--ease), background .2s var(--ease), transform .16s var(--ease); }
.wk-door > .route:active { transform: scale(.994); }

.wk-disc__q { position: relative; transition: transform .16s var(--ease), color .16s var(--ease); }
/* Anchored left: the label starts at the left edge, so scaling from the
   centre would slide the first character sideways under the reader's eye. */
.wk-disc__q:active { transform: scale(.99); transform-origin: left center; }
@media (hover: hover) and (pointer: fine) {
  .wk-disc__q:hover { color: var(--bone); }
}

/* The horizontal bar. Drawn as a background so the box can stay a fixed
   square and the bar stays centred in it at any font size. */
.wk-disc__q::after {
  content: '';
  margin-left: auto; flex: none;
  width: .7rem; height: .7rem;
  background: linear-gradient(var(--wkd-mark), var(--wkd-mark)) center / 100% 1.5px no-repeat;
}
/* The vertical bar, which is the only thing that moves. */
.wk-disc__q::before {
  content: '';
  position: absolute; right: .1rem; top: 50%;
  width: .7rem; height: 1.5px; margin-top: -.75px;
  background: var(--wkd-mark);
  transform: rotate(90deg);
  transition: transform .3s var(--ease);
}
/* IT RETRACTS, IT DOES NOT LIE DOWN. Rotating the vertical bar flat put two
   1.5px strokes on the same line, and they did not land on the same physical
   pixel — ::after is centred inside a .7rem box by the background painter and
   ::before by a -.75px margin, so at 2x they resolved about a pixel apart and
   the open marker rendered as a THICK DOUBLE RULE instead of a minus. Caught
   in a 2x screenshot, not in the computed style, which read exactly as
   intended. So the bar turns a quarter AND shrinks to nothing along its own
   length: the motion still reads as a plus becoming a minus, but what is left
   at the end is ::after alone, which is one stroke by construction. */
.wk-disc[open] > .wk-disc__q::before { transform: rotate(0deg) scaleX(0); }

@media (prefers-reduced-motion: reduce) {
  .wk-door > .route,
  .wk-disc__q { transition: none; }
  .wk-door > .route:active { transform: none; }
  .wk-disc__q:active { transform: none; }
  /* The marker still SWAPS between + and −, because that is state and not
     decoration. It simply stops taking a quarter second to do it. */
  .wk-disc__q::before { transition: none; }
}


/* ══════════════════════════════════════════════════════════════════════════
   6 · /work/camera — THE MAST ARRIVES

   THE FAULT. Measured on the page: .note and .book at the very bottom both
   carried .wk-rise, and the masthead, the two lines under it and the client
   row carried nothing. So the only part of /work/camera that moved was the
   part a reader reaches last, and the first screen — the one that is on
   display before he has done anything — was completely still. /work's two
   doors arrive; this page's opening did not, which made the pair feel like
   two different sites.

   WHY TIME AND NOT SCROLL. .wk-rise is a view() timeline, and a view()
   timeline on an element that is already in the viewport at load is past
   the end of its range on frame one — it would paint the finished state and
   do nothing, which is exactly the bug §2 of this file documents for /work
   at 1440. The mast is above the fold by definition, so it gets the
   time-based .wk-enter keyframes instead. Same distance, same curve, same
   duration as the doors, so the two pages open with one motion.

   THE STAGGER IS 55ms. Four rows at 55ms puts the last one at 165ms and the
   whole opening inside a quarter second. Longer looks like the page is
   loading; simultaneous looks like a slide.

   THE SHEET IS NOT IN THIS. The contact sheet below is left completely
   alone: it is the evidence, Ethan has said the photo sections are the part
   of this site he likes, and js/carousel.js measures each card off its own
   offsetLeft — putting a transform on an ancestor of a live scroll
   container to win a fade is a bad trade for a component that already works.
   ══════════════════════════════════════════════════════════════════════════ */
@media (prefers-reduced-motion: no-preference) {
  .wk-in > * { animation: wk-enter .44s var(--ease) both; }
  .wk-in > :nth-child(2) { animation-delay: .055s; }
  .wk-in > :nth-child(3) { animation-delay: .11s; }
  .wk-in > :nth-child(4) { animation-delay: .165s; }
  /* Anything past the fourth row lands with the fourth rather than trailing
     off — a stagger that keeps counting turns into a queue. */
  .wk-in > :nth-child(n+5) { animation-delay: .165s; }
}
