/* spidleweb.net: the home page's stream.

   The headline and filters, the one set of cards the generator writes
   (scripts/build-stream.mjs, "Contract" in specs/one-page-home.md), and the
   pagination under the last column. From 1024px the cards sit in a
   five-column grid by the cells stream-layout.js gives them; below it they
   run as one list, with notes, and case studies as bands. style.css holds the
   tokens and the chrome; the open-in-place panels are in panel.css. Values
   come from Paper, "Quiet xenops", Page 2: "B · Desktop, no switcher" and
   "B · Phone, List by default". */

/* ---- Head: the headline over one action row ----
   The headline keeps its full measure. Under it, 20px down, one row holds
   "Watch the reel" at the left and the filters at the right, the two
   dropdowns from 800px and a filter button below it, the same 32px as the
   reel's square (Jason, 2026-10-03; the filters had a row of their own).
   The cards start 40px under the row (32px on a phone), so the reel reads
   with the headline it follows rather than with the cards (Jason,
   2026-10-11; the two gaps were the other way round). The head sits above
   the grid, so the phone's filter menu can open over the first cards. */

.craft-head {
  position: relative;
  z-index: 5;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 20px;
  padding: 64px var(--gutter) 40px;
}

.craft-head__actions {
  position: relative;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px 24px;
}

.craft-head__line {
  max-width: 960px;
  font-family: var(--display);
  font-size: 31px;
  font-weight: 400;
  line-height: 38px;
  letter-spacing: -0.015em;
  text-wrap: balance;
}

/* From 1024px the headline leads the page at 38/44 on a wider measure, as on
   the B board (Jason, 2026-10-01). It wraps on its own into three lines,
   balanced so no line ends on a lone "and". */
@media (width >= 1024px) {
  .craft-head__line {
    max-width: 1120px;
    font-size: 38px;
    line-height: 44px;
    letter-spacing: -0.025em;
  }
}

/* ---- Filters ----
   Progressive enhancement over each card's data-type and data-industry, so
   they only show when site.js runs. Each is a native select laid invisibly
   over its label, which keeps the platform's own menu and keyboard support.
   The value and its chevron sit in a square 1px ink box, so they read as a
   control rather than another label (Jason, 2026-10-03). */

.filters {
  display: flex;
  flex-wrap: wrap;
  gap: 0 22px;
  margin-left: auto;
  line-height: 14px;
}

html:not(.js) :is(.filters, .filter-toggle, .filter-menu) { display: none; }

.filter {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 0.6em;
  min-height: 44px;
  margin: -15px 0;
  white-space: nowrap;
}

/* The name is a label, so it is --ink-soft; the boxed value is the control. */
.filter__name { color: var(--ink-soft); }

.filter__value {
  display: inline-flex;
  align-items: center;
  gap: 0.6em;
  padding: 6px 8px 6px 10px;
  border: 1px solid var(--ink);
  transition: background-color 120ms var(--ease-fade), color 120ms var(--ease-fade), scale 150ms var(--ease-out);
}

.filter__select {
  position: absolute;
  inset: 0;
  width: 100%;
  opacity: 0;
  cursor: pointer;
  /* 16px keeps iOS from zooming in when the select takes focus */
  font-size: 16px;
  appearance: none;
}

.filter:has(.filter__select:focus-visible) .filter__value {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

/* Shown by site.js while either filter is set. */
.filters__clear {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  margin: -15px 0;
  padding: 0;
  border: 0;
  background: none;
  color: var(--ink);
  font: inherit;
  letter-spacing: inherit;
  text-transform: inherit;
  cursor: pointer;
  white-space: nowrap;
}

.filters__clear[hidden] { display: none; }

.filters__clear:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

/* A press gives a little, on a touch screen too, where nothing hovers. */
.filter:active .filter__value { scale: 0.96; }

@media (hover: hover) and (pointer: fine) {
  .filter:hover .filter__value { background: var(--ink); color: var(--paper); }
  .filters__clear:hover { text-decoration: underline; text-underline-offset: 4px; }
}

@media (prefers-reduced-motion: reduce) {
  .filter__value { transition: none; }
}

/* ---- The filter button and its menu, below 800px ----
   One 32px square, the reel's size, with Lucide's funnel, its target grown
   to 44px. It opens one menu holding both filters, Type and Industry side by
   side as radio groups, each choice with its count (Jason, 2026-10-03).
   Choosing applies at once and leaves the menu open; a tap outside, Esc or
   tabbing away closes it. A red square on the button's corner says a filter
   is set. The menu lies over the first cards rather than push them down. */

.filter-toggle,
.filter-menu { display: none; }

@media (max-width: 799px) {
  .filters { display: none; }

  .filter-toggle {
    position: relative;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    min-width: 32px;
    /* Names that no longer fit beside the reel take the row under it, where
       the action row wraps; only a pair wider than the phone clips. */
    max-width: 100%;
    height: 32px;
    margin-left: auto;
    padding: 0;
    border: 1px solid var(--ink);
    background: transparent;
    color: var(--ink);
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: scale 150ms var(--ease-out);
  }

  .filter-toggle::before { content: ""; position: absolute; inset: -7px; }
  .filter-toggle .icon { flex: none; width: 16px; height: 16px; }
  /* What is set, "Case studies · Healthcare", in the label, left of the
     funnel; the square grows into a box around both (Jason, 2026-10-12). */
  .filter-toggle:has(.filter-toggle__applied:not([hidden])) { padding: 0 7px 0 10px; }
  .filter-toggle__applied {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    font-family: var(--mono);
    font-size: var(--text-micro);
    font-weight: 400;
    letter-spacing: 0.06em;
    line-height: 14px;
    text-transform: uppercase;
  }
  .filter-toggle__applied[hidden] { display: none; }
  .filter-toggle[aria-expanded="true"] { background: var(--ink); color: var(--paper); }
  .filter-toggle:active { scale: 0.96; }

  .filter-toggle:focus-visible {
    outline: 2px solid var(--ink);
    outline-offset: 2px;
  }

  .filter-toggle__dot {
    position: absolute;
    top: -5px;
    right: -5px;
    width: 10px;
    height: 10px;
    border: 2px solid var(--paper);
    background: var(--site-accent);
  }

  .filter-toggle__dot[hidden] { display: none; }

  .filter-menu {
    position: absolute;
    top: calc(100% + 12px);
    left: 0;
    right: 0;
    display: block;
    border: 1px solid var(--ink);
    background: var(--paper);
  }

  .filter-menu[hidden] { display: none; }
}

.filter-menu__groups {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
}

.filter-menu__group {
  min-inline-size: 0;
  margin: 0;
  padding: 14px 0 8px;
  border: 0;
}

.filter-menu__group + .filter-menu__group { border-left: 1px solid var(--line); border-image: linear-gradient(to right, var(--line) var(--hairline), transparent 0) 1; }

.filter-menu__name {
  float: left;
  width: 100%;
  padding: 0 12px 6px;
  color: var(--ink-soft);
}

.filter-menu__group > div { clear: both; }

/* A row is the choice's label: its check, its name and its count. The radio
   itself is hidden, and its focus rings the row. */
.filter-option {
  position: relative;
  display: grid;
  grid-template-columns: 12px minmax(0, 1fr) auto;
  align-items: center;
  gap: 6px;
  /* A touch target, since the menu is the phone's alone. */
  min-height: 44px;
  padding: 0 12px;
  font-family: var(--body);
  font-size: 14px;
  font-weight: 500;
  line-height: 18px;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

.filter-option input {
  position: absolute;
  opacity: 0;
  pointer-events: none;
}

.filter-option .icon { width: 12px; height: 12px; visibility: hidden; }
.filter-option:has(input:checked) { background: color-mix(in srgb, var(--ink) 6%, transparent); }
.filter-option:has(input:checked) .icon { visibility: visible; }
.filter-option:has(input:disabled) { color: var(--ink-disabled); cursor: default; }

.filter-option:has(input:focus-visible) {
  outline: 2px solid var(--ink);
  outline-offset: -2px;
}

.filter-option__count {
  color: var(--ink-soft);
  font-family: var(--mono);
  font-size: var(--text-micro);
  font-weight: 400;
  letter-spacing: 0.06em;
  line-height: 14px;
}

.filter-menu__foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 44px;
  padding: 0 12px;
  border-top: 1px solid var(--line);
  border-image: linear-gradient(var(--line) var(--hairline), transparent 0) 1;
}

.filter-menu__count { color: var(--ink-soft); }
.filter-menu__foot .filters__clear { margin: 0; text-decoration: underline; text-underline-offset: 4px; }

/* ---- The grid, from 1024px ----
   Each card carries its cell as inline --col and --row, and a 2×2 case study
   data-span="2". A card without --col leaves both properties invalid, so it
   falls back to auto placement, as the fixture grid's cards do. An open panel
   takes a row of its own (panel.css), and stream.js sets --shift: 1 on every
   card below it.

   The grid is an inline-size container so a 2×2 can work out a small tile's
   height: its band ends level with the foot of the second row's tiles, and
   its caption sits where their titles do. */

.craft > .stream-grid {
  --stream-bleed-start: var(--gutter);
  --stream-bleed-end: var(--gutter);
}

.stream-grid {
  --columns: 5;
  --column-gap: 20px;
  --row-gap: 44px;
  --card-gap: 14px;
  --card-meta-gap: 7px;
  --stack-step: 7px;
  display: grid;
  grid-template-columns: repeat(var(--columns), minmax(0, 1fr));
  gap: var(--row-gap) var(--column-gap);
  padding: 0 var(--gutter);
}

/* Pagination and the filters hide cards with the hidden attribute. */
[data-stream] > [hidden] { display: none !important; }

/* The peek: the top of what Show more brings, fading out under the pager
   (Jason, 2026-10-04, Paper's "Peek"). site.js marks the next row from
   1024px, or the next card in the list. A peeking card keeps its hidden
   attribute and is inert and aria-hidden, so only its display returns. A
   2×2 takes one row, so the clip is the same height across the row. */
[data-stream] > [hidden][data-peek] { display: block !important; }

.stream-card[data-peek] {
  --span-rows: 1;
  max-height: var(--peek-h, 240px);
  overflow: clip;
  pointer-events: none;
  /* Gone by 55%, where the pager starts, so nothing reads behind it. */
  mask-image: linear-gradient(oklch(0 0 0), oklch(0 0 0 / 0.55) 25%, transparent 55%);
}

@media (width < 1024px) {
  .stream-card[data-peek] { --peek-h: 220px; }
}

@media (width >= 1024px) {
  .stream-grid {
    container-type: inline-size;
    /* A small card: its 256:182 frame, then the title and the meta line. */
    --tile-h: calc((100cqw - (var(--columns) - 1) * var(--column-gap)) / var(--columns) * 182 / 256);
    --caption-h: calc(var(--card-gap) + 20px + var(--card-meta-gap) + 14px);
  }

  .stream-grid > .stream-card {
    grid-column: var(--col) / span var(--span-cols, 1);
    grid-row: calc(var(--row) + var(--shift, 0)) / span var(--span-rows, 1);
  }

  .stream-grid > .stream-card[data-span="2"] {
    --span-cols: 2;
    --span-rows: 2;
  }
}

/* Below 1024px placement is ignored: one column, in DOM order, which the
   build and site.js keep in cell order. Each card runs the full width and
   carries the gutter itself, so a case study's band can bleed to the edges. */
@media (width < 1024px) {
  .craft > .stream-grid {
    --stream-bleed-start: 0px;
    --stream-bleed-end: 0px;
  }
  .stream-grid {
    grid-template-columns: minmax(0, 1fr);
    gap: 0;
    padding: 0;
  }
}

/* ---- Cards ----
   The theme class on a case-study card resets --paper and --ink to the
   project's, so the card puts the site's back. It keeps --case-dark and
   --chip for the stack, the chip, and the project name. */

.stream-card {
  --paper: var(--site-paper);
  --ink: var(--site-ink);
  min-width: 0;
  color: var(--ink);
}

.stream-card__link {
  display: flex;
  flex-direction: column;
  color: inherit;
  text-decoration: none;
}

.stream-card__media {
  position: relative;
  overflow: hidden;
  aspect-ratio: 256 / 182;
  background: var(--frame-empty);
}

/* ---- Frames ----
   How a screen sits in its card or row (specs/thumbnail-framing.md). A frame
   is a ground and one or more screens, with composition values supplied by
   build-stream.mjs and placement owned here. The cqw and cqh units use the
   frame as a size container. There is no
   outline and no radius: a screen separates from its tile by its ground. */

.frame {
  --frame-l: 0.89;
  --frame-c: 0.006;
  --frame-h: 90;
  --frame-k: 1;
  --frame-bg: var(--frame-ground, oklch(var(--frame-l) calc(var(--frame-c) * var(--frame-k)) var(--frame-h)));
  position: absolute;
  inset: 0;
  overflow: hidden;
  container-type: size;
  background: var(--frame-bg);
}

/* Ten lightness steps. The near-white steps take less of the hue, so they
   stay a tint, and the dark steps a little more, so the hue survives. */
.frame[data-ground="0"] { --frame-l: 0.975; --frame-k: 0.6; }
.frame[data-ground="1"] { --frame-l: 0.95; --frame-k: 0.6; }
.frame[data-ground="2"] { --frame-l: 0.92; }
.frame[data-ground="3"] { --frame-l: 0.89; }
.frame[data-ground="4"] { --frame-l: 0.855; }
.frame[data-ground="5"] { --frame-l: 0.81; }
.frame[data-ground="6"] { --frame-l: 0.74; }
.frame[data-ground="7"] { --frame-l: 0.6; --frame-k: 1.3; }
.frame[data-ground="8"] { --frame-l: 0.32; --frame-k: 1.3; }
.frame[data-ground="9"] { --frame-l: 0.22; --frame-k: 1.3; }

/* Four hues, each the nearest to a screen's own accent. Slate takes every
   blue, teal, and violet at half the chroma, since pale blues read louder. */
.frame[data-hue="clay"] { --frame-c: 0.026; --frame-h: 40; }
.frame[data-hue="sand"] { --frame-c: 0.026; --frame-h: 85; }
.frame[data-hue="sage"] { --frame-c: 0.022; --frame-h: 150; }
.frame[data-hue="slate"] { --frame-c: 0.012; --frame-h: 245; }

.frame__screen {
  position: absolute;
  top: var(--screen-top, auto);
  right: var(--screen-right, auto);
  bottom: var(--screen-bottom, auto);
  left: var(--screen-left, auto);
  width: var(--screen-width, auto);
  translate: var(--screen-translate, none);
  overflow: hidden;
  background: var(--frame-screen);
}

.frame__screen img {
  width: 100%;
  max-width: none;
  height: auto;
}

/* On a phone the card is the page's width, so a screen hung off an edge or a
   corner reads as cut off rather than placed. Below 1024px a frame that holds
   one screen centres it whole, as float does, 10% in from the frame's nearer
   sides (Jason, 2026-10-02). Frames that fill, phones and tall pages, which
   are centred and run off the foot, and multi-screen compositions keep their
   placement. Inline custom properties describe the authored composition;
   these declarations own its responsive placement. */
@media (width < 1024px) {
  .frame:not([data-family="own"], [data-family="bleed"], [data-family="phone"], [data-family="rise-page"], [data-family="mixed"], [data-family="spread"], [data-family="callout"]) > .frame__screen:only-of-type {
    top: 50%;
    right: auto;
    bottom: auto;
    left: 50%;
    width: min(80cqw, calc((100cqh - 20cqw) * var(--r)));
    translate: -50% -50%;
  }
}

/* An asset with its own ground, zoomed in, draws no white behind it. */
.frame__screen--bare { background: none; }

/* A crop: the image is sized and shifted so the crop box fills the screen. */
.frame__screen--crop img {
  position: absolute;
  top: calc(-100% * var(--crop-y) / var(--crop-h));
  left: calc(-100% * var(--crop-x) / var(--crop-w));
  width: calc(100% / var(--crop-w));
}

/* A layer over another sits on a mat of the ground, never a shadow. */
.frame__screen--mat { outline: 1.4cqw solid var(--frame-bg); }

/* A lens is the one exception: a magnified strip of the screen beneath it,
   lifted on a soft shadow like a loupe (Jason, 2026-10-09). */
.frame__screen--lens {
  border-radius: 1.2cqw;
  box-shadow:
    0 0 0 0.3cqw color-mix(in oklch, var(--frame-screen) 75%, transparent),
    0 0.2cqw 0.6cqw oklch(0 0 0 / 0.1),
    0 1cqw 3cqw oklch(0 0 0 / 0.18);
}

.frame__mark {
  position: absolute;
  height: auto;
  translate: -50% -50%;
}

/* own and bleed fill the frame, cropped from a focus point. */
.frame__fill {
  position: absolute;
  inset: 0;
}

.frame__fill img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--frame-focus, 50% 50%);
}

/* A callout's arrow, drawn in frame-width units. */
.frame__arrow {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: auto;
  overflow: visible;
  fill: none;
  stroke: var(--fixed-ink);
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-width: 0.35;
}

/* ---- Play once ----
   Motion on the home page plays once and lands on a still (Jason, 2026-10-11;
   "Play once" in specs/one-page-home.md). A frame or a cover the build marks
   data-once holds the still as its picture, and stream.js lays the motion
   over it when the tile comes into view: a clip, the cover entering in
   parts, or a callout's lens lifting off its screen. data-once-state runs
   armed, playing, done. Until it is done the
   still is out of sight, so the motion opens on its own first frame rather
   than on the frame it ends at. Reduced motion never arms, and neither does a
   page without the script, so both show the still alone. */

@media (prefers-reduced-motion: no-preference) {
  /* The gap before stream.js runs. If it never does, the still returns on
     its own, so no tile is left empty. */
  .js [data-once]:not([data-once="lift"], [data-once-state]) img { animation: once-await 4s step-end both; }

  [data-once-state="armed"]:not([data-once="lift"]) img,
  [data-once-state="playing"]:not([data-once="lift"]) img { opacity: 0; }

  /* A callout that lifts its lens keeps its screen in sight, and only the
     lens waits: stream.js starts it as the row it enlarges, which the screen
     beneath already shows. */
  .js [data-once="lift"]:not([data-once-state]) .frame__screen--lens { animation: once-await 4s step-end both; }
  [data-once="lift"][data-once-state="armed"] .frame__screen--lens { opacity: 0; }

  /* A card peeking under the pager is not on the page yet, so it shows its
     still: an armed tile there was an empty frame, which read as a picture
     that had failed to load. It waits for its motion once Show more brings
     it in. */
  .stream-card[data-peek] [data-once] img,
  .stream-card[data-peek] [data-once] .frame__screen--lens { opacity: 1; animation: none; }
}

@keyframes once-await { from { opacity: 0; } }

/* A clip takes the picture's own box and crop, so its last frame sits
   exactly on the still. It fades out over the still when it ends. */
.once-clip {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--frame-focus, 50% 50%);
  pointer-events: none;
  transition: opacity 320ms var(--ease-fade);
}

.once-clip[data-leaving] { opacity: 0; }

/* On a cover the motion follows the image as stream.js narrows it to a cut:
   the same width, centred, from the cover's top. */
.stream-card__cover[data-once] { position: relative; }

.stream-card__cover > .once-clip,
.once-parts {
  position: absolute;
  inset: 0 auto auto 50%;
  width: var(--cover-fit, 100%);
  height: auto;
  translate: -50% 0;
  pointer-events: none;
}

/* A part is one box of the cover, drawn from the cover's own image, rising
   12px into place. A wipe is a patch of the screen's colour over part of it,
   drawn back to the right, so what it covered appears left to right. */
.once-part {
  position: absolute;
  background-repeat: no-repeat;
  animation: once-rise 560ms var(--ease-out) var(--once-delay, 0ms) both;
}

.once-wipe {
  position: absolute;
  background: var(--once-color, var(--frame-screen));
  transform-origin: 100% 50%;
  animation: once-wipe var(--once-ms, 700ms) cubic-bezier(0.65, 0, 0.35, 1) var(--once-delay, 0ms) both;
}

/* From the foot: a part comes up from under the band's foot, half its own
   height away, in 380ms and without a fade, so the cover is never long empty
   (Jason, 2026-10-11, on Beacon FC's two drawers). */
.once-parts[data-from="foot"] .once-part {
  animation: once-foot 380ms var(--ease-out) var(--once-delay, 0ms) both;
}

@keyframes once-foot { from { translate: 0 62%; } }

/* Drawn back upward, what it covered grows from its foot, as a column does. */
.once-wipe[data-dir="up"] {
  transform-origin: 50% 0;
  animation-name: once-wipe-up;
}

@keyframes once-rise { from { opacity: 0; translate: 0 12px; } }
@keyframes once-wipe { to { scale: 0 1; } }
@keyframes once-wipe-up { to { scale: 1 0; } }

.stream-card__media--empty {
  background:
    repeating-linear-gradient(135deg, transparent 0 10px, var(--line) 10px 11px),
    var(--frame-empty);
}

/* A tool without a capture is a tile that repeats its name as cover art, on
   the empty frame's ground at a screenshot's weight, so the case-study bands
   stay the only dark blocks on the page (Jason, 2026-10-03; it was ink). */
.stream-card__media--tile {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  padding: 16px;
  background: var(--frame-empty);
  color: var(--site-ink);
}

.stream-card__kicker {
  color: var(--ink-soft);
  font-family: var(--mono);
  font-size: var(--text-micro);
  font-weight: 400;
  letter-spacing: 0.1em;
  line-height: 14px;
  text-transform: uppercase;
}

.stream-card__tile-title {
  max-width: 11em;
  font-family: var(--display);
  font-size: 17px;
  font-weight: 400;
  line-height: 19px;
  text-transform: uppercase;
}

/* A case study is a stack: its cover in front, two leaves of the project's
   colour behind, each a step up and to the right. The grid and the list
   each set --stack-step. A 2×2 and a list band redraw it on the band. */
.stream-card[data-type="case-study"] .stream-card__media {
  overflow: visible;
  background: none;
}

.stream-card[data-type="case-study"] .stream-card__media::before,
.stream-card[data-type="case-study"] .stream-card__media::after {
  content: "";
  position: absolute;
}

.stream-card[data-type="case-study"] .stream-card__media::before {
  inset: 0 0 calc(var(--stack-step) * 2) calc(var(--stack-step) * 2);
  background: var(--case-dark);
}

/* The middle leaf keeps the project's hue, 0.16 lighter and a touch more
   saturated, as Paper draws the Beacon FC stack (#191E2F behind #3F475E). The
   mix is the fallback where relative colour is not supported. */
.stream-card[data-type="case-study"] .stream-card__media::after {
  inset: var(--stack-step);
  background: color-mix(in oklch, var(--case-dark) 78%, var(--fixed-paper));
  background: oklch(from var(--case-dark) calc(l + 0.16) calc(c * 1.2) h);
}

.stream-card[data-type="case-study"] .stream-card__media .frame {
  z-index: 1;
  inset: auto auto 0 0;
  width: calc(100% - var(--stack-step) * 2);
  height: calc(100% - var(--stack-step) * 2);
}

/* Badges sit in the frame's top-left corner, and a video's play badge, the
   play mark and running time together, in its bottom-left. The play badge
   is translucent, so it sits quieter on the poster. Both sit on the picture,
   which keeps its colours in dark mode, so they stay ink in both schemes:
   --ink-veil and --fixed-ink. */

.stream-card__badge,
.stream-card__time {
  position: absolute;
  z-index: 2;
  display: flex;
  align-items: center;
  gap: 6px;
  background: var(--fixed-ink);
  color: var(--fixed-paper);
  font-family: var(--mono);
  font-size: var(--text-micro);
  font-weight: 400;
  letter-spacing: 0.06em;
  line-height: 14px;
  text-transform: uppercase;
}

/* Try it and Live sit in the lower left, with a clip's running time and
   the video button (Jason, 2026-10-01). */
.stream-card__badge {
  bottom: 10px;
  left: 10px;
  /* Every badge leads with an icon, so its side takes 1px less. */
  padding: 5px 8px 5px 7px;
}

.stream-card__time { background: var(--ink-veil); }

.stream-card__time {
  bottom: 8px;
  left: 8px;
  /* 1px less on the play mark's side, as on the badges. */
  padding: 3px 6px 3px 5px;
}

/* Every badge's icon draws in the fixed paper at rest and turns Archive Red
   while its card is hovered or focused, the label staying paper (Jason,
   2026-10-02, replacing a resting red ▶ and dot). The site's red, not the
   project's, so it holds on a case study's ground too. */
.stream-card__badge::before,
.stream-card__time::before {
  content: "";
  flex: none;
  width: 0.9em;
  height: 0.9em;
  background: var(--fixed-paper);
  -webkit-mask: var(--badge-icon) center / contain no-repeat;
  mask: var(--badge-icon) center / contain no-repeat;
  transition: background-color 120ms var(--ease-fade);
}

@media (hover: hover) and (pointer: fine) {
  .stream-card__link:hover :is(.stream-card__badge, .stream-card__time)::before {
    background-color: var(--site-accent);
  }
}

.stream-card__link:focus-visible :is(.stream-card__badge, .stream-card__time)::before {
  background-color: var(--site-accent);
}

@media (prefers-reduced-motion: reduce) {
  :is(.stream-card__badge, .stream-card__time)::before { transition: none; }
}

/* A prototype is there to be tried, so Try it leads with a pointing hand,
   which reads as a tap on a phone and a click on a laptop. It and Read's
   book are Phosphor's solid glyphs (style.css), boxed to their ink and
   sized to carry the weight of the solid ▶ and the Live dot, which
   Lucide's outlines did not (Jason, 2026-10-02). */
.stream-card[data-type="prototype"] .stream-card__badge {
  --badge-icon: var(--icon-hand-pointing);
}

.stream-card[data-type="prototype"] .stream-card__badge::before {
  width: 0.7em;
  height: 0.82em;
}

/* A post's badge leads with an open book, "Read", not an eye, which would
   read as a preview (Jason, 2026-10-03). */
.stream-card[data-type="post"] .stream-card__badge {
  --badge-icon: var(--icon-book-open);
}

.stream-card[data-type="post"] .stream-card__badge::before {
  width: 0.75em;
  height: 0.64em;
}

/* Live code and specimens run on their own, so their badge carries the live
   dot, not play. */
.stream-card:is([data-type="live-code"], [data-type="specimen"]) .stream-card__badge::before {
  width: 7px;
  height: 7px;
  border-radius: 50%;
  -webkit-mask: none;
  mask: none;
}

/* The play mark leads the running time, and the badge goes solid on hover. */
.stream-card__time {
  --badge-icon: var(--icon-play);
}

@media (hover: hover) and (pointer: fine) {
  .stream-card__link:hover .stream-card__time {
    background-color: var(--fixed-ink);
  }
}

/* One line; a title longer than its tile ends in an ellipsis, and the
   panel shows it in full. */
.stream-card__title {
  margin-top: var(--card-gap);
  overflow: hidden;
  font-size: 15px;
  line-height: 20px;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.stream-card__meta {
  margin-top: var(--card-meta-gap, var(--card-gap));
  color: var(--ink-soft);
  line-height: 14px;
}

/* The note is the list's; the grid shows only the title. The facts belong
   to a case study's 2×2 and its band, and an ordinary 1×1 card leaves them
   out, so its row stays the height of the others. */
.stream-card__note,
.stream-card__facts { display: none; }

/* So are the list's open hint, its extra links and a band's industry. A
   cover band brings its "Full case study" back, on the 2×2 and the phone
   band alike (below). */
.stream-card__hint,
.stream-card__actions,
.stream-card__industry { display: none; }

/* A case study's chip and "Case study" read as one unit, and wrap as one.
   Only a case study shows the chip since 2026-10-02: an item's line is its
   type and client, "Video · SophConnect". */
.stream-card__case {
  display: inline-block;
  color: var(--case-accent);
}

.stream-card__chip {
  display: inline-block;
  width: 8px;
  height: 8px;
  margin: 0 8px 0 1px;
  background: var(--chip);
}

.stream-card[data-type="case-study"] .stream-card__chip { margin-left: 0; }

/* ---- Client colour (D2) ----
   Every client item's ground takes its client's tone at the ground's own
   lightness step, so a frame keeps its composition and changes only its
   hue. Work without a client stays on the neutral grounds, and a frame
   whose asset brings its own ground shows no ground to tint. Tried behind
   ?trial=colour and approved as it stands (Jason, 2026-10-03). */
.stream-card[data-client="conservis"] { --client-dark: var(--conservis-dark); }
.stream-card[data-client="ever-ag"] { --client-dark: var(--everag-dark); }
.stream-card[data-client="vidscrip"] { --client-dark: var(--vidscrip-dark); }
.stream-card[data-client="nda"] { --client-dark: var(--life-sciences-dark); }
.stream-card[data-client="kardion"] { --client-dark: var(--kardion-dark); }
.stream-card[data-client="dispatch"] { --client-dark: var(--dispatch-dark); }
.stream-card[data-client="activity-central"] { --client-dark: var(--activity-central-dark); }
.stream-card[data-client="sophconnect"] { --client-dark: var(--sophconnect-dark); }
.stream-card[data-client="blueshift"] { --client-dark: var(--blueshift-dark); }
.stream-card[data-client="ascensus"] { --client-dark: var(--ascensus-dark); }
.stream-card[data-client="vault-ai-systems"] { --client-dark: var(--vault-ai-systems-dark); }
.stream-card[data-client="plinth"] { --client-dark: var(--plinth-dark); }

/* A client with no tone of its own, as Ichnos, keeps the neutral ground. */
.stream-card[data-client]:not([data-type="case-study"]) .frame {
  --frame-bg: oklch(from var(--client-dark, oklch(0.3 0.007 90)) var(--frame-l) calc(c * 0.85 * var(--frame-k)) h);
}

.stream-card__link:focus-visible {
  /* 4px, twice the open card's outline, so focus and open never read the same. */
  outline: 4px solid var(--ink);
  outline-offset: 6px;
}

/* ---- A case study's 2×2, from 1024px ----
   The frame becomes the project's band, and the stack sits in it, scaled up:
   the cover with two solid leaves, each 8px up and to the right and a step
   lighter than the band (Paper's Beacon FC: #1C2030, #2E3552, #434C74). The
   band ends level with the second row's tiles, and the caption, the name with
   its chip and "Case study" on one baseline and the facts under it, sits
   where their titles do. The card keeps to its two rows; a caption taller
   than a small card's runs on into the row gap rather than growing the rows,
   which would pull the small tiles out of line with the band. */

@media (width >= 1024px) {
  .stream-grid > .stream-card[data-span="2"] {
    --stack-step: 8px;
    /* The stack's sides, as Paper draws them: 56px of a 532px band. */
    --stack-inset: 10.5%;
    height: calc(2 * var(--tile-h) + 2 * var(--caption-h) + var(--row-gap));
  }

  .stream-grid > .stream-card[data-span="2"] .stream-card__link {
    display: grid;
    grid-template-columns: minmax(0, 1fr) auto;
    grid-template-areas:
      "media media"
      "title meta"
      "facts facts";
    align-items: baseline;
    column-gap: 16px;
  }

  .stream-grid > .stream-card[data-span="2"] .stream-card__media {
    grid-area: media;
    height: calc(2 * var(--tile-h) + var(--caption-h) + var(--row-gap));
    aspect-ratio: auto;
    overflow: hidden;
    background: var(--case-dark);
  }

  /* The cover, centred with its leaves: the stack is the cover's height plus
     two steps, so the cover drops one step below the band's middle. */
  .stream-grid > .stream-card[data-span="2"] .stream-card__media .frame,
  .stream-grid > .stream-card[data-span="2"] .stream-card__media::before,
  .stream-grid > .stream-card[data-span="2"] .stream-card__media::after {
    inset: 50% auto auto var(--stack-inset);
    width: calc(100% - 2 * var(--stack-inset) - 2 * var(--stack-step));
    height: auto;
    aspect-ratio: 256 / 182;
  }

  .stream-grid > .stream-card[data-span="2"] .stream-card__media .frame {
    translate: 0 calc(-50% + var(--stack-step));
  }

  .stream-grid > .stream-card[data-span="2"] .stream-card__media::after {
    left: calc(var(--stack-inset) + var(--stack-step));
    translate: 0 -50%;
  }

  .stream-grid > .stream-card[data-span="2"] .stream-card__media::before {
    left: calc(var(--stack-inset) + 2 * var(--stack-step));
    translate: 0 calc(-50% - var(--stack-step));
  }

  .stream-grid > .stream-card[data-span="2"] .stream-card__title {
    grid-area: title;
    overflow: visible;
    font-family: var(--display);
    font-size: 26px;
    font-weight: 400;
    /* Set solid, since the name is capitals with no descenders, so the facts
       line under it lands level with the small cards' meta lines: 41px under
       the media, as their 14px gap, 20px title, and 7px gap make. */
    line-height: 26px;
    letter-spacing: -0.02em;
    text-transform: uppercase;
    white-space: normal;
    text-wrap: balance;
  }

  .stream-grid > .stream-card[data-span="2"] .stream-card__meta {
    grid-area: meta;
    margin-top: 0;
    color: var(--ink);
  }

  .stream-grid > .stream-card[data-span="2"] .stream-card__case { color: inherit; }

  .stream-grid > .stream-card[data-span="2"] .stream-card__facts {
    grid-area: facts;
    display: block;
    margin-top: 1px;
    color: var(--ink-soft);
    line-height: 14px;
  }
}

/* ---- A case study with a cover, from 1024px ----
   Paper's "Cover line" (Jason, 2026-10-01). The band fills both rows and
   holds the chip, the name and "Case study" on one line, the note set large,
   the facts, and a cover composed for the band that bleeds off its right and
   bottom edges. The cover starts under the facts, so a long note shows less
   of it and the band keeps its height. The stack and the caption below give way.
   A case study the layout rule drops to 1×1, as the "Case studies" filter
   does to all eight, keeps the same band at one cell (Jason, 2026-10-12; it
   fell back to the plain card and read as different work). The overrides
   for that size follow the band. */

.stream-card__cover { display: none; }

@media (width >= 1024px) {
  .stream-grid > .stream-card[data-type="case-study"]:has(.stream-card__cover) {
    --band-pad: 28px;
    position: relative;
    background: var(--case-dark);
    color: var(--case-paper);
  }

  /* "Full case study" sits across from the chip and the name on the top
     line, as it does on the phone band, its 44px target centred on the
     14px line, 28px down (Jason, 2026-10-12). "Case study" left the top
     line on 2026-10-03, and until now the block never said what it was, nor
     offered its page until its panel was open. The action sits outside the
     card's link, so it goes straight to /work/<slug> while the block still
     opens its panel. A 1×1 case study keeps the panel route alone: its top
     line has no room beside a name such as "Conservis Platform". */
  .stream-grid > .stream-card[data-span="2"]:has(.stream-card__cover) .stream-card__actions {
    position: absolute;
    top: calc(var(--band-pad) - 15px);
    right: var(--band-pad);
    display: flex;
    margin: 0;
    line-height: 14px;
  }

  .stream-grid > .stream-card[data-type="case-study"]:has(.stream-card__cover) .stream-card__link {
    display: grid;
    height: 100%;
    grid-template-columns: auto minmax(0, 1fr);
    grid-template-rows: auto auto auto minmax(0, 1fr);
    grid-template-areas:
      "title meta"
      "note note"
      "facts facts"
      "cover cover";
    column-gap: 0;
    padding: var(--band-pad) var(--band-pad) 0;
  }

  .stream-grid > .stream-card[data-type="case-study"]:has(.stream-card__cover) .stream-card__media { display: none; }

  /* The name drops to the label size, and the chip moves in front of it. */
  .stream-grid > .stream-card[data-type="case-study"]:has(.stream-card__cover) .stream-card__title {
    margin-top: 0;
    font-family: var(--mono);
    font-size: var(--text-micro);
    font-weight: 400;
    line-height: 14px;
    letter-spacing: 0.06em;
    /* Capitals here, not only on the 2×2's own title rule above, so the
       band at one cell sets its name as every other label is set. */
    text-transform: uppercase;
    white-space: nowrap;
  }

  .stream-grid > .stream-card[data-type="case-study"]:has(.stream-card__cover) .stream-card__title::before {
    content: "";
    display: inline-block;
    width: 8px;
    height: 8px;
    margin-right: 8px;
    background: var(--chip);
  }

  /* The band names its client alone; "Case study" stays in the card link's
     name, "Beacon FC Case study", for screen readers (Jason, 2026-10-03). */
  .stream-grid > .stream-card[data-type="case-study"]:has(.stream-card__cover) .stream-card__meta {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  /* The note is the band's headline, 30px at 1440. */
  .stream-grid > .stream-card[data-type="case-study"]:has(.stream-card__cover) .stream-card__note {
    grid-area: note;
    display: block;
    max-width: 15em;
    margin-top: 14px;
    font-family: var(--display);
    font-size: clamp(1.375rem, 2.2cqw, 2.125rem);
    font-weight: 400;
    line-height: 1.13;
    letter-spacing: -0.02em;
    text-wrap: balance;
  }

  .stream-grid > .stream-card[data-type="case-study"]:has(.stream-card__cover) .stream-card__facts {
    grid-area: facts;
    display: block;
    margin-top: 14px;
    color: color-mix(in oklch, var(--case-paper) 70%, var(--case-dark));
  }

  /* The cover starts on the text's left edge, 28px under the facts, stays
     inside the band's right padding, centred when narrowed, and runs off the band's bottom edge
     only, the same way for every case study (Jason, 2026-10-03; it ran off
     the right too). The image is composed for the band and carries its own
     shape, a window's rounded corner or whole phone screens on a transparent
     ground. It is at least 0.72 as tall as it is wide, so its foot always
     runs off, and stream.js narrows it, to 75% at most, until the foot falls
     on one of its data-cuts, between two rows of the screen. */
  .stream-grid > .stream-card[data-type="case-study"] .stream-card__cover {
    grid-area: cover;
    display: block;
    overflow: hidden;
    align-self: stretch;
    min-height: 0;
    margin: 28px 0 0;
  }

  /* Narrowed to meet a cut, the image centres in the box (Jason, 2026-10-03).
     Its corners stay square (Jason, 2026-10-09). */
  .stream-grid > .stream-card[data-type="case-study"] .stream-card__cover img {
    display: block;
    width: 100%;
    max-width: none;
    height: auto;
    margin-inline: auto;
  }
}

/* The band at one cell: one row tall, tighter padding, the headline at its
   smallest size, and no facts line, which the cell has no room for beside
   the cover. The cover keeps a gap under the headline and runs off the
   foot as it does on the 2×2. */
@media (width >= 1024px) {
  .stream-grid > .stream-card[data-type="case-study"]:not([data-span="2"]):has(.stream-card__cover) {
    --band-pad: 16px;
    height: calc(var(--tile-h) + var(--caption-h));
  }

  /* The plain card's 15/20px, since the cell is half the 2×2's and its 30px
     headline ran to five lines at 20px (Jason, 2026-10-12). */
  .stream-grid > .stream-card[data-type="case-study"]:not([data-span="2"]):has(.stream-card__cover) .stream-card__note {
    margin-top: 8px;
    font-size: 15px;
    line-height: 20px;
  }

  .stream-grid > .stream-card[data-type="case-study"]:not([data-span="2"]):has(.stream-card__cover) .stream-card__facts {
    display: none;
  }

  /* The name may wrap at this width: "Conservis Platform" ran to an
     ellipsis at 1024. */
  .stream-grid > .stream-card[data-type="case-study"]:not([data-span="2"]):has(.stream-card__cover) .stream-card__title {
    white-space: normal;
  }

  .stream-grid > .stream-card[data-type="case-study"]:not([data-span="2"]) .stream-card__cover {
    margin-top: 16px;
  }

  /* Under 1240px of grid, about 1400px of window, the cell is too short to
     show more than a sliver of the cover under the headline, so the band
     keeps its chip, name and headline alone. */
  @container (width < 1240px) {
    .stream-grid > .stream-card[data-type="case-study"]:not([data-span="2"]) .stream-card__cover {
      display: none;
    }
  }
}

/* The leaves on a band, whether a 2×2 or a list band: lighter than the band
   and a little more saturated, as Paper draws them. The mixes are the
   fallback where relative colour is not supported. */
@media (width >= 1024px) {
  .stream-grid > .stream-card[data-span="2"] .stream-card__media::after {
    background: color-mix(in oklch, var(--case-dark) 88%, var(--fixed-paper));
    background: oklch(from var(--case-dark) calc(l + 0.09) calc(c * 1.65) h);
  }

  .stream-grid > .stream-card[data-span="2"] .stream-card__media::before {
    background: color-mix(in oklch, var(--case-dark) 76%, var(--fixed-paper));
    background: oklch(from var(--case-dark) calc(l + 0.18) calc(c * 2.2) h);
  }
}

@media (width < 1024px) {
  .stream-grid > .stream-card[data-type="case-study"] .stream-card__media::after {
    background: color-mix(in oklch, var(--case-dark) 88%, var(--fixed-paper));
    background: oklch(from var(--case-dark) calc(l + 0.09) calc(c * 1.65) h);
  }

  .stream-grid > .stream-card[data-type="case-study"] .stream-card__media::before {
    background: color-mix(in oklch, var(--case-dark) 76%, var(--fixed-paper));
    background: oklch(from var(--case-dark) calc(l + 0.18) calc(c * 2.2) h);
  }
}

/* ---- The list, below 1024px ----
   The same cards in the same order, one under another (Paper, "B · Phone,
   List by default"). An item is its image at full width, then the title and
   the meta line; only a case study shows its note. A hairline separates two
   ordinary items; a case study runs as a band in its project's colour, edge
   to edge, so nothing separates it from its neighbours. */

@media (width < 1024px) {
  .stream-grid {
    --card-gap: 8px;
    --stack-step: 8px;
  }

  .stream-grid > .stream-card {
    padding: 40px var(--gutter);
    border-top: 1px solid var(--line);
    border-image: linear-gradient(var(--line) var(--hairline), transparent 0) 1;
  }

  .stream-grid > .stream-card:first-child,
  .stream-grid > .stream-card[data-type="case-study"],
  .stream-grid > .stream-card[data-type="case-study"] + .stream-card {
    border-top: 0;
  }

  /* The first item sits under the filters, which leave their own space. */
  .stream-grid > .stream-card:first-child:not([data-type="case-study"]) { padding-top: 0; }

  /* An open item hands its space to the panel under it. */
  .stream-grid > .stream-card:not([data-type="case-study"]):has(.stream-card__link[aria-expanded="true"]) {
    padding-bottom: 0;
  }

  .stream-grid .stream-card__link { gap: var(--card-gap); }
  .stream-grid .stream-card__media { margin-bottom: 8px; }

  /* The title leads and the meta line follows it, as in the grid (Jason,
     2026-10-03; the meta line led, as Paper drew it). */
  .stream-grid .stream-card__title { order: 1; }
  .stream-grid .stream-card__meta { order: 2; margin-top: 0; }
  .stream-grid .stream-card__note { order: 3; }
  .stream-grid .stream-card__facts { order: 4; }

  .stream-grid .stream-card__title {
    margin-top: 0;
    overflow: visible;
    font-size: 17px;
    line-height: 22px;
    white-space: normal;
    text-wrap: pretty;
  }

  /* Only a case study's note shows in the list. Every other card's is in
     its lightbox or panel, once opened (Jason, 2026-10-12; each card showed
     its own). */
  .stream-grid > .stream-card[data-type="case-study"] .stream-card__note {
    display: block;
    max-width: 34em;
    font-family: "EB Garamond", "EB Garamond Fallback", Georgia, serif;
    font-size: 19px;
    font-weight: 400;
    font-synthesis: none;
    line-height: 25px;
    text-wrap: pretty;
  }

  /* Phones show no buttons under a card: no open hint ("Play here ↓", "Try
     it here ↓", "Open ↓") and no line of extra links. A card plainly opens
     when tapped, and its panel or case page carries the same links (Jason,
     2026-10-02). The rule above keeps both hidden at every width, except a
     cover band's "Full case study →" (2026-10-03, below). A band without a
     cover still shows its project's first industry. */
  .stream-grid .stream-card__industry { display: inline; }

  /* A band keeps the project's colours, where every other card puts the
     site's back: its paper is the text on its dark ground. */
  .stream-grid > .stream-card[data-type="case-study"] {
    padding: 40px var(--gutter) 36px;
    background: var(--case-dark);
    color: var(--case-paper);
  }

  .stream-grid > .stream-card[data-type="case-study"] .stream-card__link { gap: 10px; }
  .stream-grid > .stream-card[data-type="case-study"] .stream-card__media { margin-bottom: 14px; }

  .stream-grid > .stream-card[data-type="case-study"] .stream-card__meta,
  .stream-grid > .stream-card[data-type="case-study"] .stream-card__facts { color: var(--on-ink); }
  .stream-grid > .stream-card[data-type="case-study"] .stream-card__case { color: inherit; }

  .stream-grid > .stream-card[data-type="case-study"] .stream-card__title {
    font-family: var(--display);
    font-size: 26px;
    font-weight: 400;
    line-height: 30px;
    letter-spacing: -0.02em;
    text-transform: uppercase;
    overflow-wrap: anywhere;
  }

  .stream-grid > .stream-card[data-type="case-study"] .stream-card__facts {
    display: block;
    line-height: 14px;
  }

  .stream-grid > .stream-card[data-type="case-study"] .stream-card__link:focus-visible {
    outline-color: var(--case-paper);
  }
}

/* ---- A band with a cover, below 1024px ----
   The 2×2's cover line, at the width of the page: the chip and the name on
   the top line, with "Full case study →" across from them (Jason,
   2026-10-03), the note as the band's headline, the facts, and the
   cover between the gutters, bleeding off the band's bottom edge only, so
   the band's foot is the cover's. The whole band links to the case page;
   the action says so, with a 44px target of its own. */

@media (width < 1024px) {
  .stream-grid > .stream-card[data-type="case-study"]:has(.stream-card__cover) {
    /* Room on the top line for "Full case study →", so a long name wraps
       rather than runs into it. */
    --band-action: 150px;
    position: relative;
    padding: 40px var(--gutter) 0;
  }

  /* The name shares its line with the action; every other piece takes a
     line of its own. */
  .stream-grid > .stream-card[data-type="case-study"]:has(.stream-card__cover) .stream-card__link {
    flex-flow: row wrap;
    align-items: baseline;
    gap: 0;
  }

  .stream-grid > .stream-card[data-type="case-study"]:has(.stream-card__cover) .stream-card__media { display: none; }

  /* The name drops to the label size, with the chip in front of it. */
  .stream-grid > .stream-card[data-type="case-study"]:has(.stream-card__cover) .stream-card__title {
    order: 1;
    max-width: calc(100% - var(--band-action));
    font-family: var(--mono);
    font-size: var(--text-micro);
    line-height: 14px;
    letter-spacing: 0.06em;
  }

  .stream-grid > .stream-card[data-type="case-study"]:has(.stream-card__cover) .stream-card__title::before {
    content: "";
    display: inline-block;
    width: 8px;
    height: 8px;
    margin-right: 8px;
    background: var(--chip);
  }

  /* The name stands alone, as on the 2×2: "Full case study →" says what
     the band is, and "Case study" stays in the link's name for screen
     readers (Jason, 2026-10-03). */
  .stream-grid > .stream-card[data-type="case-study"]:has(.stream-card__cover) .stream-card__meta {
    position: absolute;
    width: 1px;
    height: 1px;
    overflow: hidden;
    clip-path: inset(50%);
    white-space: nowrap;
  }

  /* The headline keeps its 15em measure inside a full line. */
  .stream-grid > .stream-card[data-type="case-study"]:has(.stream-card__cover) .stream-card__note {
    order: 3;
    flex: 0 0 100%;
    padding-right: max(0px, 100% - 15em);
    margin-top: 14px;
    font-family: var(--display);
    font-size: clamp(1.375rem, 6.2vw, 1.875rem);
    line-height: 1.13;
    letter-spacing: -0.02em;
    text-wrap: balance;
  }

  .stream-grid > .stream-card[data-type="case-study"]:has(.stream-card__cover) .stream-card__facts {
    order: 4;
    flex: 0 0 100%;
    margin-top: 14px;
    color: color-mix(in oklch, var(--case-paper) 70%, var(--case-dark));
  }

  /* A long role, "Design · Typography · Build", wraps as one piece, so no
     line starts with a separator. */
  .stream-grid > .stream-card[data-type="case-study"]:has(.stream-card__cover) .stream-card__role { display: inline-block; }

  /* "Full case study →" sits across from the name, its 44px target centred
     on the top line, 40px down. */
  .stream-grid > .stream-card[data-type="case-study"]:has(.stream-card__cover) .stream-card__actions {
    position: absolute;
    top: 25px;
    right: var(--gutter);
    display: flex;
    margin: 0;
    line-height: 14px;
  }

  /* Phone bands keep the shared arrow visible without hover. */
  .stream-grid > .stream-card[data-type="case-study"]:has(.stream-card__cover) .credit-link__arrow {
    width: var(--arrow-width);
    margin-left: var(--arrow-gap);
    opacity: 1;
    transform: none;
  }

  /* The cover keeps one shape, 1.6:1, so a band's height follows its words
     alone; the image is 4:3 or taller, so its foot always runs off, and
     stream.js narrows it until the foot falls on one of its data-cuts. */
  .stream-grid > .stream-card[data-type="case-study"] .stream-card__cover {
    order: 5;
    flex: 0 0 100%;
    display: block;
    overflow: hidden;
    aspect-ratio: 1.6;
    max-height: 440px;
    margin: 24px 0 0;
  }

  .stream-grid > .stream-card[data-type="case-study"] .stream-card__cover img {
    display: block;
    width: 100%;
    max-width: none;
    height: auto;
    margin-inline: auto;
  }
}

/* The action on a cover band, the 2×2 and the phone band alike: the
   project's paper, underlined at rest, since a finger has no hover and a
   screener should not have to hover to find the link. On desktop its → slides
   in on hover or focus and takes its room as it comes, as a credit's ↗ does
   (Jason, 2026-10-12; the arrow stood beside the words at rest, and the
   underline came only on hover). */
.stream-grid > .stream-card[data-type="case-study"]:has(.stream-card__cover) .stream-card__action {
  --arrow-width: 1.1em;
  --arrow-gap: 0.5em;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  color: var(--case-paper);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  white-space: nowrap;
}

.stream-grid > .stream-card[data-type="case-study"]:has(.stream-card__cover) .stream-card__action:focus-visible {
  outline: 2px solid var(--case-paper);
  outline-offset: 2px;
}

/* A cover composed to run off the band's sides as well as its foot takes
   back the band's side paddings, at both widths. It follows both cover
   sections, whose margins it overrides. Only Ever.Ag Mobile's
   tilted screens do (Jason's sketch, 2026-10-11); every other cover stays
   inset and runs off the foot alone. */
@media (width >= 1024px) {
  .stream-grid > .stream-card[data-type="case-study"] .stream-card__cover--bleed {
    margin-inline: calc(-1 * var(--band-pad));
  }
}

@media (width < 1024px) {
  .stream-grid > .stream-card[data-type="case-study"] .stream-card__cover--bleed {
    flex-basis: calc(100% + 2 * var(--gutter));
    margin-inline: calc(-1 * var(--gutter));
  }
}

/* ---- No matches ---- */

.stream-empty {
  padding: 8px var(--gutter) 0;
  font-family: var(--display);
  font-size: 20px;
  line-height: 28px;
}

.stream-empty__clear {
  min-height: 44px;
  margin-left: 12px;
  padding: 0 14px;
  border: 1.5px solid var(--ink);
  background: none;
  color: var(--ink);
  cursor: pointer;
  vertical-align: middle;
  transition: background-color 120ms var(--ease-fade), color 120ms var(--ease-fade), scale 150ms var(--ease-out);
}

/* ---- Pagination: under the first column, where the reading starts ----
   It sat under the last column, where it was easy to miss, then spanned the
   stream with a centred button, which Jason turned down on 2026-10-01 as the
   only centred thing on the page. */

.stream-foot {
  --columns: 5;
  display: grid;
  grid-template-columns: repeat(var(--columns), minmax(0, 1fr));
  column-gap: 20px;
  padding: 52px var(--gutter) 44px;
}

/* The list keeps the pager narrow at the left, as the grid's first column. */
@media (width < 1024px) { .stream-foot { --columns: 3; } }
@media (max-width: 599px) { .stream-foot { --columns: 2; column-gap: 14px; padding-top: 28px; } }

.pager { grid-column: 1 / 2; }

/* Over a peek the pager rides up into the fade, so the next row reads as
   what the button brings. It is pulled up by the peek's height less how
   far into the peek it should start, plus the foot's own top padding. */
.stream-foot[data-peeking] {
  position: relative;
  z-index: 1;
  margin-top: calc(-1 * (var(--peek-h, 240px) - var(--peek-into, 132px)) - var(--foot-pad, 52px));
}

@media (width < 1024px) {
  .stream-foot[data-peeking] { --peek-h: 220px; --peek-into: 136px; }
}
@media (max-width: 599px) {
  .stream-foot[data-peeking] { --foot-pad: 28px; }
}
.pager[hidden] { display: none; }
/* Information, not a control, so it takes --ink-soft as the filters' names
   do; ink and a mark (box, underline, arrow) are kept for what can be
   pressed (Jason, 2026-10-03). */
.pager__count {
  color: var(--ink-soft);
  line-height: 14px;
  /* Paper, as on Show more. The peek has faded out by here, but contrast
     checkers can't read its mask and test the count against the peeking
     card's own ground. */
  background: var(--paper);
}

.pager__bar {
  height: 3px;
  margin-top: 8px;
  background: color-mix(in srgb, var(--ink) 15%, transparent);
}

.pager__fill {
  display: block;
  width: 0;
  height: 100%;
  background: var(--ink);
}

.pager__more {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  width: 100%;
  margin-top: 16px;
  /* 2px less on the arrow's side, as on every icon-and-label control. */
  padding: 14px 16px 14px 18px;
  border: 1.5px solid var(--ink);
  /* Paper rather than transparent, so it stays solid over a peek. */
  background: var(--paper);
  color: var(--ink);
  line-height: 14px;
  cursor: pointer;
  text-align: left;
  transition: background-color 120ms var(--ease-fade), color 120ms var(--ease-fade), scale 150ms var(--ease-out);
}

.pager__more[hidden] { display: none; }

.pager__more:focus-visible,
.stream-empty__clear:focus-visible {
  outline: 2px solid var(--ink);
  outline-offset: 2px;
}

.pager__more:active,
.stream-empty__clear:active { scale: 0.96; }

@media (hover: hover) and (pointer: fine) {
  .pager__more:hover,
  .stream-empty__clear:hover {
    background: var(--ink);
    color: var(--paper);
  }
}

@media (prefers-reduced-motion: reduce) {
  .pager__more, .stream-empty__clear, .filter-toggle { transition: none; }
}

/* ---- Narrow windows and phones ---- */

@media (max-width: 799px) {
  .craft-head { padding: 56px var(--gutter) 32px; }
}

@media (max-width: 599px) {
  .craft-head__line {
    font-size: 18px;
    line-height: 24px;
  }

  .stream-card__media--tile { padding: 12px; }
  .stream-card__badge { bottom: 8px; left: 8px; padding: 4px 7px 4px 6px; }
  .pager__more { padding: 15px 14px 15px 16px; }

  /* A phone runs only a Mobile prototype live, so any other prototype drops
     its Try it badge, by the same test stream.js applies when one opens.
     Jason's ruling of 2026-09-24. data-platform is on the card (the card
     contract). Specimens are drawn for a desktop screen, so
     they drop their Live badge the same way, and open to their still with
     a note to try them on a laptop (Jason's ruling of 2026-09-30). */
  .stream-card:is([data-type="prototype"]:not([data-platform="mobile"]), [data-type="specimen"]) .stream-card__badge {
    display: none;
  }
}

/* ---- Dark scheme ----
   On a dark page a band's ground sits within 1.1 to 1.6:1 of the paper
   around it, so a hairline edge, the one images get on ink, marks where it
   starts. An outline, not a border, so no box moves. */
@media (prefers-color-scheme: dark) {
  .stream-grid > .stream-card[data-type="case-study"]:has(.stream-card__cover) {
    outline: 1px solid var(--image-outline-on-ink);
    outline-offset: -1px;
  }
}
