/* J Grigg Photography. Layered on scrollcraft.css (tokens + devices).
   Written phone-first: the base rules ARE the 390px composition. The
   (min-width: 960px) blocks re-compose it for a desktop, they do not patch it. */

/* Cormorant Garamond, variable, SIL OFL 1.1. The axis is 300-700, which covers
   the 400/500/600 this design asks for. Roman and italic are separate files
   because the family has a true italic, not a slant. */
@font-face { font-family: "Cormorant Garamond Variable"; font-weight: 300 700; font-style: normal;
  font-display: swap; src: url("/fonts/cormorant-garamond-latin-wght-normal.woff2") format("woff2"); }
@font-face { font-family: "Cormorant Garamond Variable"; font-weight: 300 700; font-style: italic;
  font-display: swap; src: url("/fonts/cormorant-garamond-latin-wght-italic.woff2") format("woff2"); }
@font-face { font-family: "Geist Variable"; font-weight: 100 900; font-display: swap;
  src: url("/fonts/geist-latin-wght-normal.woff2") format("woff2"); }

:root {
  --sc-canvas:      #E6E8E3;   /* plaster wall, cool, not cream */
  --sc-surface:     #F1F2EE;
  --sc-ink:         #1A1D1B;
  --sc-ink-soft:    #4F5652;
  --sc-accent:      #BF1E2E;   /* brand red, sampled from the logo artwork */
  --sc-accent-ink:  #F1F2EE;
  --sc-font-display: "Cormorant Garamond Variable", "Cormorant Garamond", Georgia, serif;
  /* Cormorant sets ~18% smaller than Archivo at the same font-size: a small
     x-height and narrow counters. Every display size is multiplied by this so
     the ramp in scrollcraft.css stays the single source for the steps. */
  --dsz: 1.18;
  --sc-font-text:    "Geist Variable", "Geist", system-ui, sans-serif;
  --bar-h: 3.5rem;   /* phone; the desktop bar is taller, see the 960px block */
  --tabs-h: 3.5rem;
  --safe-b: env(safe-area-inset-bottom, 0px);
  --r: 2px;                      /* surfaces and image tiles */
  /* Controls are rounded; surfaces are not. */
  --r-btn: 10px;
  /* Photographs: slightly, so the corner reads as deliberate and not as a
     rounded-off UI card. Separate from --r-btn so the two can move apart. */
  --r-img: 7px;
  --print-shadow: 0 22px 34px -18px rgb(26 29 27 / .42), 0 3px 8px -2px rgb(26 29 27 / .22);
  --wall-line: color-mix(in oklab, var(--sc-ink) 16%, var(--sc-canvas));
}

*, *::before, *::after { box-sizing: border-box; }
html { scroll-padding-top: 1rem; }
body { padding-bottom: calc(var(--tabs-h) + var(--safe-b)); min-height: 100svh; }
a { color: inherit; text-underline-offset: .2em; text-decoration-thickness: 1px; }
h1, h2, h3, p, figure, ul, ol, dl, dd { margin: 0; }
ul, ol { padding: 0; list-style: none; }
[hidden] { display: none !important; }
.vh { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.only-d { display: none !important; }
.num { font-variant-numeric: tabular-nums; }
.is-protected { -webkit-touch-callout: none; -webkit-user-select: none; user-select: none; }

/* ------------------------------------------------------------ type -- */
/* A serif at 500 wants the opposite treatment to a grotesk at 700: no negative
   tracking (the counters are already narrow) and a tighter leading is safe
   because the ascenders are long and the x-height is short. */
.t-display { font-family: var(--sc-font-display); font-weight: 500; letter-spacing: .005em;
  line-height: 1.08; text-wrap: balance; }
.t-display em { font-style: italic; font-weight: 500; }
.t-h1 { font-size: calc(var(--sc-t-2xl) * var(--dsz)); }
.t-h2 { font-size: calc(var(--sc-t-xl) * var(--dsz)); }
.t-body { max-width: 52ch; text-wrap: pretty; line-height: var(--sc-leading-body); }
.t-soft { color: var(--sc-ink-soft); }
/* Eyebrows are the counterweight to the serif: Geist, small, widely tracked.
   font-family is explicit so a display-font ancestor can never capture them. */
.k { font-family: var(--sc-font-text); font-size: .75rem; font-weight: 600; letter-spacing: .18em;
  text-transform: uppercase; color: var(--sc-ink-soft); }

/* ---------------------------------------------------- museum label -- */
/* One schema for every object on the site. Title, place, spec, price. */
.label { display: grid; gap: .15rem; font-size: var(--sc-t-sm); line-height: 1.4; }
.label__title { font-family: var(--sc-font-display); font-weight: 600; font-size: calc(var(--sc-t-base) * var(--dsz)); letter-spacing: .01em; }
.label__meta { color: var(--sc-ink-soft); }
.label__price { font-variant-numeric: tabular-nums; }

/* ------------------------------------------------------------ buttons -- */
.btn { display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
  min-height: 3rem; padding: .7rem 1.25rem; border: 1px solid var(--sc-ink); border-radius: var(--r-btn);
  background: transparent; color: var(--sc-ink); font-weight: 600; text-decoration: none; cursor: pointer;
  transition: background-color 140ms var(--sc-ease-out), color 140ms var(--sc-ease-out), transform 120ms var(--sc-ease-out); }
.btn--solid { background: var(--sc-accent); border-color: var(--sc-accent); color: var(--sc-accent-ink); }
.btn:active { transform: translateY(1px) scale(.985); }
.btn:disabled, .btn[aria-disabled="true"] { opacity: .45; cursor: not-allowed; transform: none; }
@media (hover: hover) and (pointer: fine) {
  .btn:not(:disabled):hover { background: var(--sc-ink); color: var(--sc-canvas); }
  .btn--solid:not(:disabled):hover { background: color-mix(in oklab, var(--sc-accent) 82%, black); border-color: transparent; color: var(--sc-accent-ink); }
}
.link { font-weight: 600; }
:where(a, button, summary, input, [tabindex]):focus-visible { outline: 2px solid var(--sc-accent); outline-offset: 3px; }

/* ----------------------------------------------------- chrome: phone -- */
.bar { display: none; }
.tabs { position: fixed; inset: auto 0 0 0; z-index: 60; display: grid; grid-template-columns: repeat(3, 1fr);
  padding-bottom: var(--safe-b); background: var(--sc-canvas); border-top: 1px solid var(--wall-line); }
.tabs a, .tabs button { display: flex; align-items: center; justify-content: center; min-height: var(--tabs-h);
  border: 0; background: none; font-weight: 600; font-size: var(--sc-t-sm); text-decoration: none; cursor: pointer; position: relative; }
.tabs [aria-current="page"]::after { content: ""; position: absolute; top: 0; left: 28%; right: 28%; height: 2px; background: var(--sc-accent); }
.tabs :active { background: color-mix(in oklab, var(--sc-ink) 6%, transparent); }
.i-cart { display: block; flex: none; }
/* The count sits tight to the icon now there is no word to sit after. */
.cart-n { margin-left: .15rem; min-width: 1.4rem; padding: 0 .35rem; text-align: center; border-radius: 1rem;
  background: var(--sc-accent); color: var(--sc-accent-ink); font-size: .75rem; line-height: 1.4rem; font-variant-numeric: tabular-nums; }
.cart-n[hidden] { display: none; }

/* gallery index sheet */
.sheet { border: 0; padding: 0; margin: auto 0 0; width: 100%; max-width: none; max-height: 86svh; background: var(--sc-canvas); color: var(--sc-ink);
  border-radius: var(--r) var(--r) 0 0; box-shadow: 0 -20px 50px -20px rgb(26 29 27 / .45); }
.sheet::backdrop { background: rgb(26 29 27 / .45); }
.sheet__in { padding: 1.25rem var(--sc-gutter) calc(1.25rem + var(--safe-b)); }
.sheet__head { display: flex; justify-content: space-between; align-items: center; margin-bottom: .5rem; }
.sheet ul a { display: flex; justify-content: space-between; align-items: baseline; min-height: 3.25rem; padding: .6rem 0;
  border-top: 1px solid var(--wall-line); text-decoration: none; font-family: var(--sc-font-display); font-weight: 600;
  font-size: calc(var(--sc-t-lg) * var(--dsz)); letter-spacing: .01em; }
.sheet ul a span { font-family: var(--sc-font-text); font-weight: 500; font-size: var(--sc-t-sm); color: var(--sc-ink-soft); font-variant-numeric: tabular-nums; }
.sheet ul a[aria-current="page"] { color: var(--sc-accent); }
.x { width: 2.75rem; height: 2.75rem; display: grid; place-items: center; border: 0; background: none; cursor: pointer; font-size: 1.5rem; line-height: 1; border-radius: 50%; }
.x:active { background: color-mix(in oklab, var(--sc-ink) 8%, transparent); }


/* ------------------------------------------------- gallery pages -- */
.g-head { padding: var(--sc-6) var(--sc-gutter) var(--sc-5); display: grid; gap: var(--sc-3); }
.g-head .t-body { color: var(--sc-ink-soft); }
.g-chips { display: flex; gap: .5rem; overflow-x: auto; padding: 0 var(--sc-gutter) var(--sc-4); scrollbar-width: none; }
.g-chips::-webkit-scrollbar { display: none; }
.g-chips a { flex: none; display: inline-flex; align-items: center; min-height: 2.75rem; padding: 0 1rem; border: 1px solid var(--wall-line);
  border-radius: 2rem; text-decoration: none; font-size: var(--sc-t-sm); font-weight: 600; white-space: nowrap; }
.g-chips a[aria-current="page"] { background: var(--sc-ink); color: var(--sc-canvas); border-color: var(--sc-ink); }

/* Justified rows: every photograph keeps its own aspect ratio, so a portrait
   wildlife frame is never cropped to fit a landscape cell. Still the default
   for seven of the eight galleries. */
.grid { --gap: .5rem; --row: 9rem; display: flex; flex-wrap: wrap; gap: var(--gap) var(--gap); padding: 0 var(--sc-gutter) var(--sc-7); }
.grid > li { flex-grow: calc(var(--ar) * 100); flex-basis: calc(var(--ar) * var(--row)); min-width: 0; }
.grid::after { content: ""; flex-grow: 1000000; }

/* MOSAIC - Wild America only for now, while the idea is judged on one gallery.
   Tiles take different footprints and grid-auto-flow: dense backfills the holes
   they leave, which is what makes it read as a packed wall rather than a stack
   of rows.

   grid-auto-rows tracks the COLUMN WIDTH, so the base cell is square and a span
   of C x R is a cell of aspect C/R. That is the whole trick: the builder picks
   C and R from each photograph's own ratio, so a 3:2 landscape gets a 3x2 cell
   (1.50) and a 2:3 portrait a 2x3 (0.67), and object-fit: cover only ever
   absorbs a few percent. A mosaic that hands out footprints arbitrarily has to
   crop hard to make them fit, and cropping a photographer's framing to make a
   layout work is backwards.

   The row unit is in vw because it has to equal the column width, and the
   column width is (container - gutters - gaps) / columns, which CSS cannot
   name. The values below are those numbers worked out.

   Every tile is PLACED, not flowed. grid-auto-flow: dense backfills holes when
   it happens to find a tile that fits and leaves the rest open, which is where
   the white gaps came from: with 35 landscapes and 12 portraits there are only
   two footprints in play, and two footprints cannot pack a ragged edge flush.
   The builder emits an exact column and row per tile from a block that tiles
   its 8x6 rectangle with no remainder, so there is nothing left to backfill
   and no gap to appear. */
.grid--mosaic { --gap: .5rem; --cols: 4; --ru: 1; --gut: 0px;
  /* Full-bleed on the phone, gutters back on at 960px. */
  padding-inline: var(--gut);
  /* The base cell must be SQUARE for a span of C x R to be a cell of aspect
     C/R, so grid-auto-rows has to equal the column width - which CSS will not
     hand back as a value. It is derivable though, and derived beats the
     hand-worked vw number that was here: that one was measured at one viewport
     and was wrong at every other, and wrong high means overflow. */
  --u: calc((100vw - 2 * var(--gut) - (var(--cols) - 1) * var(--gap)) / var(--cols));
  /* Row height as a FRACTION of the column width, because a square base cell
     is only exact where the slot spans divide the aspect cleanly. They do on
     desktop (3x2 = 1.50, the landscapes; 2x3 = 0.67, the portraits) so --ru is
     1 there. Four phone columns cannot do it: the nearest landscape slot is
     4x3 = 1.33, which crops 11% off the sides of 36 of the 48 photographs.
     Squashing the row to .94 split that error - but it bought the crop with
     HEIGHT, and height is the only axis a phone tile has left once it is full
     width, so it made the thumbnails SMALLER than before. The request was
     larger, and mobile is priority 1 here. Going full-bleed on the phone pays
     for both instead: 390x291 against 350x266, and the extra width lands the
     portrait slot at 0.66, which is these portraits exactly. --ru is back to
     1 and the landscape crop is 11%, the same as it has always been. */
  display: grid; grid-template-columns: repeat(var(--cols), 1fr);
  grid-auto-rows: calc(var(--u) * var(--ru)); gap: var(--gap); }
.grid--mosaic > li { flex: none; min-width: 0;
  grid-column: var(--mc) / span var(--mcs);
  grid-row: var(--mr) / span var(--mrs); }
.grid--mosaic::after { content: none; }
/* Full-bleed is a PHONE decision. Above 600px the 4-column pattern is still
   in force (the 8-column one starts at 960) but the screen is no longer a
   phone, and edge-to-edge there put a single 768px-wide photograph on a
   tablet. Gutters come back; the tiles stay large. */
@media (min-width: 600px) { .grid--mosaic { --gut: var(--sc-gutter); } }

.tile { display: grid; gap: .4rem; text-decoration: none; cursor: zoom-in; -webkit-tap-highlight-color: transparent; }
.tile picture { display: block; background: color-mix(in oklab, var(--sc-ink) 8%, var(--sc-canvas)); aspect-ratio: var(--ar);
  border-radius: var(--r-img); overflow: hidden; }
.tile img { width: 100%; height: 100%; display: block; }
.tile__cap { display: flex; justify-content: space-between; gap: .5rem; font-size: var(--sc-t-xs); line-height: 1.3; }
.tile__cap span { color: var(--sc-ink-soft); font-variant-numeric: tabular-nums; white-space: nowrap; }

/* In the mosaic the tile fills the cell the grid gave it, so the caption has to
   come off the flow and sit over the picture - a caption below would push the
   tile past its row span and break the packing. */
/* The caption is absolutely positioned on top of the photograph and spans its
   full width, so rounding only the picture left two square corners sitting
   proud of it. Clip at the tile instead. */
.grid--mosaic .tile { position: relative; display: block; height: 100%;
  border-radius: var(--r-img); overflow: hidden; }
.grid--mosaic .tile picture { width: 100%; height: 100%; aspect-ratio: auto; }
.grid--mosaic .tile img { object-fit: cover; }
.grid--mosaic .tile__cap { position: absolute; inset: auto 0 0 0;
  padding: 2.2rem .7rem .55rem; color: #fff;
  background: linear-gradient(to top, rgb(8 10 9 / .78) 0%, rgb(8 10 9 / .46) 45%,
                                      rgb(8 10 9 / 0) 100%);
  opacity: 0; transition: opacity 180ms var(--sc-ease-out); }
.grid--mosaic .tile__cap span { color: rgb(255 255 255 / .82); }
/* Shown on hover where there is a pointer, and always where there is not - a
   caption a touch reader can never reveal is a caption they do not have. */
@media (hover: hover) and (pointer: fine) {
  .grid--mosaic .tile:hover .tile__cap,
  .grid--mosaic .tile:focus-visible .tile__cap { opacity: 1; } }
@media not all and (hover: hover), (pointer: coarse) {
  .grid--mosaic .tile__cap { opacity: 1; } }

@media (hover: hover) and (pointer: fine) { .tile:hover picture { box-shadow: var(--print-shadow); } .tile picture { transition: box-shadow 160ms var(--sc-ease-out); } }

/* lightbox: image up top, the decision in the thumb zone */
.lb { border: 0; padding: 0; margin: 0; max-width: none; max-height: none; width: 100%; height: 100%; background: var(--sc-canvas); color: var(--sc-ink); overflow: hidden; }
.lb[open] { display: grid; grid-template-rows: minmax(0, 1fr) auto; }
.lb::backdrop { background: var(--sc-canvas); }
.lb__stage { position: relative; min-height: 0; background: color-mix(in oklab, var(--sc-ink) 6%, var(--sc-canvas)); touch-action: pan-y; }
/* The inset belongs to the PICTURE only. The img is inside it, so giving
   both the same inset compounded to 2rem and took 32px off each dimension
   of the photograph - which on a phone, where the stage is ~250px tall, is
   a quarter of the height a portrait print had to work with. */
.lb__stage picture { position: absolute; inset: .75rem; width: calc(100% - 1.5rem); height: calc(100% - 1.5rem); }
.lb__stage img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; }
.lb__stage picture { display: block; }
.lb__x { position: absolute; top: .5rem; right: .5rem; z-index: 2; background: var(--sc-canvas); box-shadow: 0 1px 6px rgb(26 29 27 / .25); }
.lb__nav { position: absolute; bottom: .75rem; z-index: 2; width: 2.75rem; height: 2.75rem; border-radius: 50%; border: 0; background: var(--sc-canvas); box-shadow: 0 1px 6px rgb(26 29 27 / .25); cursor: pointer; font-size: 1.25rem; }
.lb__nav--prev { left: .75rem; } .lb__nav--next { right: .75rem; }
.lb__panel { padding: .8rem var(--sc-gutter) calc(.7rem + var(--safe-b)); display: grid; gap: .75rem; max-height: 62svh; overflow-y: auto; border-top: 1px solid var(--wall-line); }
.buy__act { display: grid; gap: .5rem; }
.lb__zoom { position: absolute; top: .5rem; left: .5rem; z-index: 2;
  width: 2.75rem; height: 2.75rem; display: grid; place-items: center; border: 0;
  border-radius: 50%; background: var(--sc-canvas); color: var(--sc-ink);
  box-shadow: 0 1px 6px rgb(26 29 27 / .25); cursor: pointer; }
.lb__zoom .i-in { display: none; }
.is-zoom .lb__zoom .i-out { display: none; }
.is-zoom .lb__zoom .i-in { display: inline; }
@media (max-width: 959px) {
  /* The BUTTON AND ITS CONFIRMATION, never the button alone. Pinning only the
     button left "Added. View cart" in normal flow below it, 54-166px under the
     fold on every phone and every surface: a buyer tapped the one button this
     panel exists for and saw nothing happen.
     Measured at 62svh: a 390x844 phone does not scroll this panel at all, so
     this rule is inert there. It earns its place at 360x740 and 375x667, where
     the button is otherwise 26-71px below the fold. (An earlier version of
     this comment claimed 26px at 390x844 - that was measured against the
     52svh panel this change set had already introduced, not against the real
     before. The numbers here are against the committed state.)
     Opaque backdrop because the description scrolls under it; a soft shadow
     alone sliced the text mid-line and read as a rendering fault. */
  .buy__act { position: sticky; bottom: 0; z-index: 1;
    background: var(--sc-canvas); padding-top: .45rem; margin-top: -.45rem; }
  /* A gradient, not a shadow: a soft shadow left the line it covered
     half-visible, which reads as a rendering fault rather than as "there is
     more below".
     ONLY WHEN THERE IS MORE BELOW. Unconditionally, it washed out the last
     line and a half of the description in the states where the panel does not
     scroll at all - the DEFAULT at 390x844 and 430x932 - and again whenever a
     scrolling panel was scrolled to its end. Measured 1.30-1.55:1 against a
     3.0 floor: fading text that the reader can already see in full, with
     nothing hidden under it, is just unreadable text. .has-more is set from
     gallery.js whenever the panel's content or scroll position changes. */
  .lb__panel.has-more .buy__act::before { content: ""; position: absolute;
    left: 0; right: 0; bottom: 100%; height: 2.25rem; pointer-events: none;
    background: linear-gradient(to top, var(--sc-canvas), transparent); }

  /* Full screen: the panel goes, the inset goes, and the surround turns dark
     so nothing of the page competes with the photograph. */
  .lb.is-zoom[open] { grid-template-rows: minmax(0, 1fr); }
  .lb.is-zoom .lb__panel { display: none; }
  .lb.is-zoom .lb__stage { background: #0b0d0c; }
  /* Not just a bigger letterbox. object-fit: contain on a portrait phone
     renders a 3:2 photograph at 390x260 whether the box is 341px tall or the
     whole 844 - the width is the binding constraint either way, so "full
     screen" would have bought about 20%. Instead the photograph is sized to
     the screen's HEIGHT and the stage pans, which is roughly 3x and is what
     "view it closely before buying" actually asks for.
     Gesture model, the one every photo viewer uses: not zoomed, drag moves to
     the next photograph and a tap zooms in; zoomed, drag pans and a tap zooms
     out. */
  /* pinch-zoom included: the fit-to-screen size is where looking closer
     STARTS, and a buyer judging a print should be able to go past it. */
  .lb.is-zoom .lb__stage { overflow: auto; touch-action: pan-x pan-y pinch-zoom;
    /* without this a pan that the stage cannot consume scrolls the PAGE
       behind the dialog - routine now that the stage is a scroll container */
    overscroll-behavior: contain;
    /* BOTH axes safe. Plain `center` puts half the overflow outside the
       scrollport on the start side, unreachable: in landscape that hid 438px
       of a portrait photograph - a third of the frame, head and all - at the
       very top of the scroll range. */
    display: grid; align-content: safe center; justify-content: safe center; }
  /* Fill whichever dimension binds. height:100svh alone only wins on a tall
     viewport: turn the phone to look closer - the exact moment a buyer would -
     and a portrait photograph rendered 260x390 inside 844x390, two thirds of
     the screen black and nothing to pan. Both maxima agree on one scale, so
     the aspect is preserved either way. --zar is the photograph's ratio, set
     per image in gallery.js. */
  .lb.is-zoom .lb__stage picture { position: static; inset: auto;
    width: max(100vw, calc(100svh * var(--zar, 1.5)));
    height: max(100svh, calc(100vw / var(--zar, 1.5))); }
  .lb.is-zoom .lb__stage img { position: static; inset: auto;
    width: auto; height: 100%; max-width: none; object-fit: contain; }
  /* FIXED, not absolute. The stage is the scroll container when zoomed, and an
     absolutely positioned child of a scroll container scrolls with its
     content - so opening centred carried the close and collapse buttons
     straight off the left of the screen. */
  .lb.is-zoom .lb__zoom, .lb.is-zoom .lb__x, .lb.is-zoom .lb__nav {
    position: fixed; background: rgb(255 255 255 / .92); color: #11130f; }
  .lb.is-zoom .lb__x { top: .5rem; right: .5rem; }
  .lb.is-zoom .lb__zoom { top: .5rem; left: .5rem; }
  .lb.is-zoom .lb__nav { bottom: calc(.75rem + var(--safe-b)); }
  .lb.is-zoom .lb__nav--prev { left: .75rem; }
  .lb.is-zoom .lb__nav--next { right: .75rem; }
}
/* No --dsz here. The panel is height-starved on a phone and every pixel it
   takes comes off the photograph; the Cormorant bump is for headlines, and
   this is a product name sitting two lines above the buy button. */
.lb__title { font-family: var(--sc-font-display); font-weight: 500; font-size: var(--sc-t-xl); letter-spacing: .005em; line-height: 1.1; }
/* The prev/next arrows are hidden on a phone: they sit on the photograph and
   a swipe already does the job. They stay at 960px and up, where there is no
   swipe and arrow keys are the only other way through the gallery. */
@media (max-width: 959px) { .lb__nav { display: none; } }
/* The photograph's description, in the buy panel only. Kept to the panel's
   small type so it reads as supporting copy under the title rather than
   competing with the surface note further down. */
/* xs, not sm. On a phone every line here comes off the photograph above it,
   and this sits next to the surface note at the same weight; at sm the two
   grey paragraphs read as one slab. Desktop takes it back up. */
.lb__desc { margin-top: .25rem; color: var(--sc-ink-soft); font-size: var(--sc-t-xs);
  line-height: 1.4; max-width: 46ch; }
.lb__desc[hidden] { display: none; }
.opt { border: 0; padding: 0; margin: 0; display: grid; gap: .55rem; min-width: 0; }
.opt legend { padding: 0; margin-bottom: .1rem; line-height: 1.15; }
.opt__row { display: grid; grid-template-columns: repeat(auto-fit, minmax(4.25rem, 1fr)); gap: .4rem; }
/* Three across, not three stacked rows. Stacked they cost 152px in a panel
   with ~410 to spend, which is what pushed the surface description under
   the fold and left the photograph 252px of stage. Same shape as the size
   chips above them, with the price on its own line under the name. */
.opt--stack .opt__row { grid-template-columns: repeat(3, minmax(0, 1fr)); }
/* .opt.opt--stack, not .opt--stack: `.opt label` sets space-between and is
   written later in this file, so at equal specificity it won and pushed the
   price to the bottom edge of the chip with a void above it. */
.opt.opt--stack label { flex-direction: column; justify-content: center; gap: .15rem;
  text-align: center; padding: .45rem .35rem; line-height: 1.2; font-size: var(--sc-t-sm); }
.opt.opt--stack label .num { font-size: var(--sc-t-xs); opacity: .85; font-weight: 600; }
.opt input { position: absolute; opacity: 0; pointer-events: none; }
.opt label { display: flex; justify-content: space-between; align-items: center; gap: .75rem; min-height: 2.4rem; padding: 0 .85rem;
  border: 1px solid var(--wall-line); border-radius: var(--r-btn); background: var(--sc-surface); cursor: pointer; font-weight: 600; font-variant-numeric: tabular-nums;
  transition: border-color 120ms var(--sc-ease-out), background-color 120ms var(--sc-ease-out); }
.opt:not(.opt--stack) label { justify-content: center; }
.opt label small { font-weight: 500; color: var(--sc-ink-soft); font-size: var(--sc-t-xs); }
.opt input:checked + label { border-color: var(--sc-ink); background: var(--sc-ink); color: var(--sc-canvas); }
.opt input:checked + label small { color: inherit; opacity: .8; }
.opt input:focus-visible + label { outline: 2px solid var(--sc-accent); outline-offset: 2px; }
.opt label:active { transform: scale(.985); }
.buy { display: grid; gap: .8rem; }
.buy__spec { color: var(--sc-ink-soft); font-size: var(--sc-t-sm); }
/* The chosen surface's description. min-height holds three lines so switching
   surface does not shunt the Add button up and down under the cursor. */
.buy__surface { margin: .25rem 0 0; color: var(--sc-ink-soft); font-size: var(--sc-t-xs);
  line-height: 1.4; min-height: calc(3 * 1.4em); }
/* The confirmation floats ABOVE the button instead of reserving a line under
   it. The reserve was there so "Added. View cart" could not shunt the button
   under the finger that just tapped it - taking it out of flow keeps that
   guarantee and gives the 25px back to the photograph, which is what put a
   dead band under the button at the bottom of the screen. */
.buy__act { position: relative; }
.buy__ok { position: absolute; left: 0; right: 0; bottom: 100%; padding-bottom: .35rem;
  font-size: var(--sc-t-sm); font-weight: 600; color: var(--sc-accent);
  background: var(--sc-canvas); }
.buy__ok:empty { display: none; }
/* While the confirmation is up it sits over the surface description, and a
   band of opaque colour across the middle of a paragraph reads as a
   rendering fault - the same thing the gradient above this block was
   narrowed for. The description keeps a min-height reserve, so hiding its
   text rather than removing it costs no layout shift at all. */
.lb__panel.is-added .buy__surface { visibility: hidden; }
/* 63x18 as a bare inline link, under the 24px target guideline, and it is the
   link a buyer taps right after adding to the cart. */
.buy__ok a { color: inherit; display: inline-block; padding: .35rem 0; min-height: 24px; }

/* ------------------------------------------------------------ cart -- */
.cart { padding: var(--sc-6) var(--sc-gutter) var(--sc-7); display: grid; gap: var(--sc-6); }
.cart__lines { display: grid; }
.line { display: grid; grid-template-columns: 5.25rem 1fr; gap: .25rem 1rem; padding: 1.1rem 0; border-top: 1px solid var(--wall-line); }
.line:last-child { border-bottom: 1px solid var(--wall-line); }
.line picture, .line img { display: block; width: 100%; height: auto; }
.line picture { grid-row: span 2; align-self: start; background: color-mix(in oklab, var(--sc-ink) 8%, var(--sc-canvas)); }
.line__row { display: flex; align-items: center; justify-content: space-between; gap: .75rem; flex-wrap: wrap; }
/* overflow clips the children's square :active tint, which at 2px was
   invisible and at 10px paints into the corner. */
.step { display: inline-flex; align-items: center; border: 1px solid var(--wall-line); border-radius: var(--r-btn); background: var(--sc-surface); overflow: hidden; }
.step button { width: 2.75rem; height: 2.75rem; border: 0; background: none; font-size: 1.25rem; cursor: pointer; }
.step button:active { background: color-mix(in oklab, var(--sc-ink) 8%, transparent); }
.step output { min-width: 2rem; text-align: center; font-variant-numeric: tabular-nums; font-weight: 600; }
.rm { border: 0; background: none; padding: .5rem 0; min-height: 2.75rem; cursor: pointer; color: var(--sc-ink-soft); text-decoration: underline; text-underline-offset: .2em; }
.sum { display: grid; gap: .6rem; align-content: start; }
.sum dl { display: grid; gap: .5rem; }
.sum dl div { display: flex; justify-content: space-between; gap: 1rem; font-variant-numeric: tabular-nums; }
.sum dl .total { padding-top: .75rem; border-top: 1px solid var(--sc-ink); font-weight: 600;
  font-size: calc(var(--sc-t-lg) * var(--dsz)); font-family: var(--sc-font-display); letter-spacing: .01em;
  font-variant-numeric: lining-nums tabular-nums; }
.sum .btn { width: 100%; min-height: 3.25rem; }
.alert { padding: .9rem 1rem; border: 1px solid var(--sc-ink); border-radius: var(--r); background: var(--sc-surface); font-size: var(--sc-t-sm); }
.alert[hidden] { display: none; }
.empty { padding: var(--sc-8) 0; display: grid; gap: var(--sc-4); justify-items: start; }

/* ------------------------------------------------------------- hero -- */
/* A FILM THAT PLAYS, behind one screen.

   The version this replaces bound Jordan's montage to the scrollbar frame by
   frame. It was a real answer to "nobody waits around for the next image, they
   are scrolling" - but it answered it by making the reader do the work, and the
   verdict on seeing it was "why am I SCROLLING through the video?". It also
   spent three viewport heights before the page proper began. Both are worse
   faults than the one it fixed, so the film runs on its own now and the hero is
   one screen.

   These rules ARE the 390px composition, not a desktop layout reflowed: the
   phone gets the 9:16 cut of the footage, and the type is placed for that
   frame rather than reflowed into it.

   Nothing here depends on JavaScript. Every element is in the document,
   visible and interactive at first paint; the only thing script does is swap
   the clip in behind the poster once the page is idle. */
.hero {
  position: relative;
  height: 100svh; min-height: 34rem;
  background: #0b0d0c; isolation: isolate;
}
.hero__stage {
  position: relative; height: 100%; overflow: clip;
  display: grid; grid-template-rows: auto 1fr auto;
  isolation: isolate;
}
/* The film and the frame that holds its place. Stated here as well as in the
   engine stylesheet because without JS neither carries .sc-stage's child
   selector and both would land in the grid flow. */
.hero__poster, .hero__film {
  position: absolute; inset: 0; z-index: 0;
  width: 100%; height: 100%; object-fit: cover; margin: 0;
}
.hero__poster img { display: block; width: 100%; height: 100%; object-fit: cover; }

/* WHO SHOWS WHAT, AND WHEN. The film sits above the poster and carries an
   opaque ground, so without this it is a black rectangle over the poster for
   as long as the clip takes to arrive - and the poster exists precisely so
   that nobody sees that. home.js adds .is-live only once the video reports
   readyState >= 2, which is the browser saying it has a frame to paint. Not a
   timer, and not loadedmetadata: a 2 MB clip on a slow line beats any timer
   you would pick, and metadata arrives long before a frame does. */
.hero__film   { background: #0b0d0c; opacity: 0;
                transition: opacity 420ms var(--sc-ease-out); }
.hero__poster { opacity: 1; transition: opacity 420ms var(--sc-ease-out); }
.hero.is-live .hero__film   { opacity: 1; }
.hero.is-live .hero__poster { opacity: 0; }

/* SCRIM, SHAPED TO THE TYPE RATHER THAN WASHED OVER THE PICTURE.
   The montage is mostly night and dusk, but at 31-33s it passes an owl against
   a pale blue sky: measured over the real footage, the backdrop luminance under
   the wordmark reaches 0.91 and under the gallery rows 0.75, which white type
   clears at 1.09:1 and 1.32:1 bare. One flat gradient strong enough for THAT
   frame has to be ~0.8 opaque, and a 0.8 wash over the other 24 scenes is what
   made the fox and the ocean read as mud in the first build of this.
   So it is two tight bands instead, each sized to the box it protects and gone
   within 20% of the frame edge. Everything between y=0.20 and y=0.46 - which is
   where the animal is in every wildlife frame - carries no scrim at all.
   There is one band now rather than two: the second served the gallery rows,
   which have been removed from the hero. */
.hero__scrim { position: absolute; inset: 0; z-index: 1; pointer-events: none;
  /* Down from the top, under the white wordmark. Seven stops approximating a
     quadratic falloff rather than three linear ones: a short linear ramp lands
     a LOCATABLE edge across the picture - a grey smear with a findable bottom -
     which on a site whose job is showing photographs is its own fault. The
     curve holds full density across both lines of the mark and then lets go
     slowly enough that there is no edge to find. */
  /* Holds full density across the two lines of the mark (the mark occupies the
     top ~10%) and then lets go faster than it used to: the old curve was still
     at .24 a quarter of the way down and did not clear until 40%, washing
     picture that carries no type at all. */
  background: linear-gradient(to bottom,
      rgb(8 10 9 / .88) 0%,  rgb(8 10 9 / .84) 8%,  rgb(8 10 9 / .60) 13%,
      rgb(8 10 9 / .32) 18%, rgb(8 10 9 / .13) 23%, rgb(8 10 9 / .04) 28%,
      rgb(8 10 9 / 0) 33%); }

/* The copy's ground. Full width and anchored to the stage, NOT a ::before on
   .hero__copy: boxed to the copy element its right edge landed in the middle of
   a 1440px frame as a visible vertical seam across the picture, and its top
   edge as a horizontal one. A scrim with a corner in it is worse than no scrim.

   This is now the ONLY wash over the lower frame - .hero__floor went with the
   gallery rows it existed for - so it is the single thing standing between the
   headline and the brightest frame in the montage, the owl against pale sky at
   t=31-34s. Deleting it by accident took the headline to 1.03:1 against a 3.0
   floor, over a backdrop reading (255,255,253). Do not remove it without
   re-running verify/hero.py: the poster frame is dark, so a screenshot and the
   page-level contrast check both pass happily with no veil at all. */
.hero__veil { position: absolute; left: 0; right: 0; bottom: 0; height: 78%;
  z-index: 1; pointer-events: none;
  /* Re-centred on where the type actually is, and lighter.
     Stops are % of the veil, which is 78% of the hero, measured up from the
     bottom. Measured positions: the headline sits at veil 36-61% on a wide
     screen and 31-46% on a phone; the CTA at veil 25-32% and 19-27%. Nothing
     is above veil 61%.
     The old curve peaked at .82 at 62% and was still .66 at 80% and .24 at
     92% - i.e. its heaviest band sat ABOVE every piece of text it existed for,
     and it kept washing picture a third of the way up a frame that carries
     none. It now peaks across the type and is clear by 72%, and the peak is
     .62 rather than .82: the headline measured 8.67:1 against a 3.0 floor, so
     there was 2.6x more wash than it needed. */
  /* Second pass. The first took the CTA band to .55 and the sweep came back
     3.78:1 on a phone and 4.04 on a wide screen against a 4.5 floor - the CTA
     is white on solid red but it is measured against what is behind its box,
     and the red is not opaque enough to carry a bright frame on its own. Back
     to ~.70 across veil 19-32%, which is where it sits, and the headline band
     up a little with it: 3.5:1 on desktop was passing but not by much.
     The clearing above is kept - 0 by veil 78%, so the top 39% of the frame
     carries nothing at all. */
  background: linear-gradient(to top, rgb(8 10 9 / .52) 0%, rgb(8 10 9 / .68) 20%,
                                      rgb(8 10 9 / .70) 32%, rgb(8 10 9 / .66) 46%,
                                      rgb(8 10 9 / .54) 60%, rgb(8 10 9 / .20) 70%,
                                      rgb(8 10 9 / 0) 78%, rgb(8 10 9 / 0) 100%); }

/* The gallery rows' ground is GONE WITH THE ROWS. It existed for one job -
   keeping a gallery name and its count legible over whatever the film was
   doing at the bottom of the frame - and with nothing left down there to
   protect it was darkening bare picture for no reason, on a hero whose whole
   complaint has been that too little of the film shows. The veil still runs
   to the bottom edge, so the type above it is unaffected: the CTA is white on
   solid red and never depended on the film behind it anyway. */

.hero__mast { position: relative; z-index: 2; grid-row: 1;
  padding: var(--sc-7) var(--sc-gutter) 0; }
/* The mark carries its OWN contrast rather than asking the scrim to darken the
   whole top of every frame for it. Over the owl at t=32.8s the second line,
   "LONGMONT, COLORADO", sat in the band's tail and measured 2.6:1 on desktop
   where the first line was fine - the mark is 10.5:1 wide and spans far more of
   the gradient than a line of text does. Deepening the band to reach it would
   have made the letterbox edge the scrim already draws across bright frames
   worse, on a site whose job is showing photographs.
   In the end BOTH were changed, so this comment should not claim a trade-off
   that was not made: the band also went from three linear stops to a seven-stop
   curve, which at the mark's rows raised alpha from about 0.59 to 0.79 while
   REMOVING the locatable edge rather than deepening it. Measured with the ring
   read from the frame with the mark absent - that is, with this shadow
   discounted entirely - the mark clears 8.7:1 on a phone and 9.8:1 on desktop,
   so the shadow is insurance rather than the thing carrying it. */
.hero__mark { width: min(26rem, 92%); height: auto;
  filter: drop-shadow(0 1px 2px rgb(0 0 0 / .9))
          drop-shadow(0 2px 16px rgb(0 0 0 / .72)); }

/* Beat one: who this is and what they sell. Leaves on scroll.
   The block is bottom-aligned in its row, so what sets how far down the frame
   the headline sits is the padding BELOW it, not above: padding-top on a
   bottom-aligned block grows it upwards and moves the text not at all. Less
   padding underneath drops the whole block, which is what leaves more of the
   film clear above it. What limits the drop is the foot row below - 47px of
   slack on a wide screen, 23 on a phone. (It used to be the gallery rows,
   which were lower and tighter; removing them let the block fall further
   than this padding alone intended.) */
.hero__copy { position: relative; z-index: 2; grid-row: 2;
  align-self: end; display: grid; justify-items: start;
  gap: var(--sc-4); color: #fff;
  padding: 0 var(--sc-gutter) 1.5rem; }
/* A shadow on the type as well as a wash behind it - the same trade the
   wordmark makes. It buys legibility against a bright frame without the veil
   having to darken the whole middle of the photograph to get there, and it is
   deliberately NOT counted by verify/hero.py, which reads the backdrop with the
   text hidden. Everything that passes there passes on the wash alone. */
.hero__h { font-size: clamp(2.06rem, 1.3rem + 4vw, 2.83rem); max-width: 15ch;
  color: #fff; text-wrap: balance; line-height: 1.08;
  text-shadow: 0 1px 2px rgb(0 0 0 / .55), 0 2px 18px rgb(0 0 0 / .45); }
.hero__cta { justify-self: start; margin-top: .4rem; }

/* Entrance: once, on load, while the page is still. It used to exclude the
   gallery rows deliberately - they were scroll's to bring in - but those have
   been removed from the hero, so the wordmark, headline and CTA are all of
   it now. */
.hero__copy > *, .hero__mark { animation: heroin .85s cubic-bezier(.2,.75,.25,1) backwards; }
.hero__mark { animation-delay: .12s; }
.hero__h    { animation-delay: .26s; }
.hero__cta  { animation-delay: .38s; }
@keyframes heroin { from { opacity: 0; transform: translateY(1rem); } }

/* A scroll affordance, and nothing more. The old foot carried a progress meter
   reading the scrub's --sc-p and a link labelled "Galleries" - the meter has
   nothing to measure now the film is not scroll-driven, and the link
   duplicated the gallery rows that used to sit above it (themselves since
   removed). */
.hero__foot { position: relative; z-index: 3; grid-row: 3;
  padding: 0 var(--sc-gutter) calc(var(--tabs-h) + var(--sc-4));
  display: flex; justify-content: center; pointer-events: none; }
.hero__cue { display: block; width: 1px; height: 2rem;
  background: linear-gradient(to bottom, rgb(255 255 255 / 0), rgb(255 255 255 / .75));
  transform-origin: top;
  animation: cuenudge 2.4s ease-in-out infinite; }
@keyframes cuenudge { 0%, 100% { transform: scaleY(.45); opacity: .5; }
                      50%      { transform: scaleY(1);   opacity: 1; } }

/* ----------------------------------------------- home: gallery shelves --
   One gallery per line, each row scrolled by hand.

   This replaces a single gallery pinned for 380vh whose sideways travel was
   driven by vertical scroll. The brief for what is here instead is exact: a
   reader may swipe through a category if they want it, and may leave for the
   next one at any moment without finishing the first. So:

   - overflow-x on an ordinary element. No scroll driver, no pinning, no
     transform, nothing that reads scroll position.
   - touch-action: pan-x PAN-Y. The pan-y is the whole promise. `pan-x` alone
     is the usual way this gets written and it does exactly the thing being
     asked against - a finger starting on a photograph can then only go
     sideways, so the row traps the page.
   - overscroll-behavior: contain x, auto y. Stops a sideways fling chaining
     into the browser's back gesture, without touching vertical chaining.
   - NO SNAPPING AT ALL. It was `scroll-snap-type: x proximity`, which
     computes to an active `x` and makes every item a snap target: each flick
     gets captured and re-targeted to an item edge, which on a phone is felt
     as the row fighting the finger rather than coasting. The row is for
     looking, not for paging, so free momentum is the right behaviour and
     also the smooth one.
   - no per-item box-shadow either. --print-shadow is a 34px blur, and six of
     them repainting across a scroll is the other half of the cost. A print
     needs to look like it is sitting off the wall; a thumbnail in a strip
     does not. */
.shelves { padding: var(--sc-8) 0 var(--sc-7); display: grid; gap: var(--sc-7); }
.shelves__head { display: grid; gap: var(--sc-3); padding-inline: var(--sc-gutter); }

.shelf { display: grid; gap: var(--sc-3); }
.shelf__head { display: flex; align-items: baseline; justify-content: space-between;
  gap: var(--sc-4); padding-inline: var(--sc-gutter); }
.shelf__t { font-family: var(--sc-font-display); font-weight: 600;
  font-size: calc(var(--sc-t-lg) * var(--dsz)); letter-spacing: .01em; }
.shelf__t a { color: inherit; text-decoration: none; }
.shelf__t a:hover { text-decoration: underline; text-underline-offset: .22em; }
.shelf__all { font-family: var(--sc-font-text); font-size: var(--sc-t-xs); letter-spacing: .18em; text-transform: uppercase;
  font-weight: 600; color: var(--sc-ink-soft); text-decoration: none; white-space: nowrap;
  min-height: 44px; display: inline-flex; align-items: center; }
/* At .18em the trailing tracking sits AFTER the arrow, so the word space in
   front of it reads short. Give the glyph its own gap. */
.shelf__all span { margin-left: .2em; }
.shelf__all:hover { color: var(--sc-accent); }

.shelf__row { --shelf-h: min(65.5vw, 20.3rem);   /* +30%, then +20% again */
  display: flex; gap: .6rem; margin: 0; padding: 0 var(--sc-gutter); list-style: none;
  overflow-x: auto; overflow-y: hidden;
  touch-action: pan-x pan-y pinch-zoom;
  overscroll-behavior-x: contain; overscroll-behavior-y: auto;
  scrollbar-width: thin; scrollbar-color: color-mix(in oklab, var(--sc-ink) 28%, transparent) transparent; }
.shelf__row:focus-visible { outline: 2px solid var(--sc-accent); outline-offset: 3px; }
.shelf__item { flex: 0 0 auto; }
.shelf__item a { display: block; text-decoration: none; }
.shelf__item picture { display: block; height: var(--shelf-h);
  width: calc(var(--shelf-h) * var(--ar));
  /* The row is sized by HEIGHT, so a 3:2 landscape at --shelf-h comes out
     98vw and fills the phone exactly: one photograph, no second one in sight,
     and nothing telling you the row moves sideways. Cap the width so the next
     item always peeks. A cap rather than a shorter row because the height is
     what was asked for twice; the cost is ~12% off the sides of a landscape in
     a preview strip, and the gallery page shows it uncropped. */
  max-width: 84vw; border-radius: var(--r-img);
  overflow: hidden; background: color-mix(in oklab, var(--sc-ink) 8%, var(--sc-canvas)); }
.shelf__item img { display: block; width: 100%; height: 100%; object-fit: cover; }

/* ------------------------------------------------ home: the size wall -- */
.wallact { --wall-h: calc(100svh - 27.5rem); --ppi: calc(var(--wall-h) / 60); }
.wall { background: var(--sc-canvas); display: grid; grid-template-rows: auto 1fr auto; padding-bottom: calc(var(--tabs-h) + var(--safe-b)); }
.wall__intro { padding: var(--sc-4) var(--sc-gutter) 0; display: grid; gap: .35rem; }
.wall__intro h2 { font-size: calc(var(--sc-t-xl) * var(--dsz)); }
.wall__intro p { font-size: var(--sc-t-sm); color: var(--sc-ink-soft); max-width: 38ch; }
.wall__scene { position: relative; align-self: end; height: var(--wall-h); margin-top: var(--sc-3); }
.wall__floor { position: absolute; left: 0; right: 0; bottom: 0; height: calc(var(--ppi) * 4); background: color-mix(in oklab, var(--sc-ink) 12%, var(--sc-canvas)); border-top: 1px solid var(--wall-line); }
.wall__ruler { position: absolute; left: 0; bottom: 0; width: 2.4rem; height: 100%; font-size: .66rem; color: var(--sc-ink-soft); font-variant-numeric: tabular-nums; }
.wall__tick { position: absolute; left: 0; width: .5rem; height: 1px; background: var(--sc-ink-soft); }
.wall__tick--ft { width: .9rem; background: var(--sc-ink); }
.wall__tick b { position: absolute; left: 1.05rem; top: -.55em; font-weight: 500; line-height: 1; white-space: nowrap; }
.wall__hang { position: absolute; left: calc(50% + 1.2rem); top: calc(var(--wall-h) * (1 - 38 / 60)); width: 0; height: 0; }
.wall__box { position: absolute; width: calc(var(--ppi) * 36); height: calc(var(--ppi) * 24); left: calc(var(--ppi) * -18); top: calc(var(--ppi) * -12); }
.wall__ol { position: absolute; left: 50%; top: 50%; border: 1px dashed color-mix(in oklab, var(--sc-ink) 45%, transparent); transform: translate(-50%, -50%); transition: border-color 160ms var(--sc-ease-out), opacity 160ms var(--sc-ease-out); }
.wall__ol i { position: absolute; left: .25rem; top: .1rem; font-style: normal; font-size: .62rem; color: var(--sc-ink-soft); font-variant-numeric: tabular-nums; white-space: nowrap; line-height: 1.2; }
.wall__ol.is-on { border-color: var(--sc-accent); border-style: solid; }
.wall__ol.is-on i { color: var(--sc-accent); font-weight: 700; }
.wall__print { position: absolute; inset: 0; transform: scale(var(--k, 1)); will-change: transform; filter: drop-shadow(0 1.1rem 1rem rgb(26 29 27 / .3)) drop-shadow(0 .15rem .25rem rgb(26 29 27 / .25)); }
.wall__clip { position: absolute; inset: 0; clip-path: inset(0 var(--cx, 0%) 0 var(--cx, 0%)); }
.wall__clip picture, .wall__clip img { display: block; width: 100%; height: 100%; object-fit: cover; }
.wall__dim { position: absolute; left: 0; top: 0; height: 1px; width: 100%; background: var(--sc-ink); transform-origin: 0 0; pointer-events: none; }
.wall__dimlab { position: absolute; top: 0; left: 0; font-size: .72rem; font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; pointer-events: none; background: var(--sc-canvas); padding: 0 .35rem; }
.wall__label { padding: var(--sc-4) var(--sc-gutter) 0; display: grid; gap: .5rem; }
.wall__size { font-family: var(--sc-font-display); font-weight: 500; font-size: calc(var(--sc-t-2xl) * var(--dsz)); line-height: 1; letter-spacing: .01em; font-variant-numeric: lining-nums tabular-nums; }
.wall__size small { font-size: var(--sc-t-sm); font-weight: 500; color: var(--sc-ink-soft); letter-spacing: 0; }
.wall__prices { display: flex; flex-wrap: wrap; gap: .1rem 1rem; font-size: var(--sc-t-sm); font-variant-numeric: tabular-nums; }
.wall__prices span { color: var(--sc-ink-soft); }
.wall__steps { display: grid; grid-template-columns: repeat(5, 1fr); gap: .35rem; padding: .75rem var(--sc-gutter) 0; }
.wall__steps button { min-height: 2.75rem; border: 1px solid var(--wall-line); border-radius: var(--r-btn); background: var(--sc-surface); font-weight: 600; font-size: var(--sc-t-xs); cursor: pointer; font-variant-numeric: tabular-nums; transition: background-color 120ms var(--sc-ease-out), color 120ms var(--sc-ease-out); }
.wall__steps button[aria-pressed="true"] { background: var(--sc-ink); color: var(--sc-canvas); border-color: var(--sc-ink); }
.wall__go { padding-top: .25rem; }

/* ---------------------------------------------------------- home: about -- */
.about { padding: var(--sc-9) var(--sc-gutter) var(--sc-9); display: grid; gap: var(--sc-8); }
.about__copy { display: grid; gap: var(--sc-5); max-width: 34rem; }
.about__copy .t-body { font-size: var(--sc-t-lg); line-height: 1.55; max-width: 36ch; }
.about__pic { display: grid; gap: .9rem; }
/* The card's own arrangement: the wide one, then the two square-ish ones
   side by side under it.
   minmax(0, 1fr), NOT 1fr. A bare 1fr takes an automatic minimum of
   min-content, and the min-content of these pictures is the intrinsic 466px of
   the file - so two of them forced the pair to 348px inside a 320px viewport
   and the whole page scrolled sideways at every width. Same trap the
   reduced-motion block below hit with .hangs__hold. */
.about__pair { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: .9rem; }
.about { grid-template-columns: minmax(0, 1fr); }
.about__pic picture, .about__pic img { display: block; width: 100%; height: auto; }
.about__pic img { border-radius: var(--r-img); }
.about__pic picture { aspect-ratio: var(--ar); box-shadow: var(--print-shadow); background: color-mix(in oklab, var(--sc-ink) 8%, var(--sc-canvas)); }

/* ---------------------------------------------------------- home: close -- */
.close { background: #0d0f12; }
.close__pic { position: absolute; inset: -5% 0; }
.close__pic picture, .close__pic img { display: block; width: 100%; height: 100%; object-fit: cover; }
.close__plate { position: absolute; z-index: 3; left: var(--sc-gutter); right: var(--sc-gutter); bottom: calc(var(--tabs-h) + var(--safe-b) + 1rem);
  padding: 1.25rem; background: var(--sc-canvas); color: var(--sc-ink); border-radius: var(--r); display: grid; gap: .85rem; box-shadow: var(--print-shadow); }
.close__plate h2 { font-size: var(--sc-t-xl); }
.close__plate ul { display: grid; gap: .1rem; font-size: var(--sc-t-sm); }
.close__plate ul a { display: inline-flex; align-items: center; min-height: 2.5rem; }
.close__cap { position: absolute; z-index: 2; left: var(--sc-gutter); top: 1rem; font-size: var(--sc-t-xs); color: #f1f2ee; text-shadow: 0 1px 8px rgb(0 0 0 / .6); }

/* --------------------------------------------------- where it hangs -- */
/* One viewport, five rooms. The page is held while they cross-fade in place,
   so the act costs one screen of height instead of four stacked sections -
   which was most of why the page felt long. */
.hangs { position: relative; }
.hangs__hold { position: sticky; top: 0; height: 100svh; overflow: hidden;
  display: grid; align-content: end; isolation: isolate; background: #0b0d0c; }
.hangs__head { position: absolute; z-index: 3; top: calc(var(--bar-h) + var(--sc-4));
  left: var(--sc-gutter); opacity: var(--chrome, 1); }
.hangs__head h2 { color: #fff; text-shadow: 0 1px 14px rgb(0 0 0 / .72); }
.hangs__stack { position: absolute; inset: 0; z-index: 0; }
/* No opacity transition: home.js sets opacity from scroll position every frame,
   so a transition would only lag behind it. [data-on] stays as the no-JS state
   for the first scene. */
.hang { position: absolute; inset: 0; margin: 0; opacity: 0;
  container-type: size; }
.hang[data-on] { opacity: 1; }
/* The print hangs at a percentage of the ROOM PHOTOGRAPH, so it has to live in
   the photograph's coordinate space. Giving the <img> object-fit:cover and then
   positioning the print against the viewport box put the two in different
   spaces: they agreed only at the one aspect ratio the numbers were eyeballed
   at, and at 390px the fireplace print floated up by the ceiling under a
   caption reading "above the fireplace".
   This frame IS the photograph's box - sized to cover the viewport, then
   shifted so the --fx/--fy focal point (the middle of the print) sits in the
   middle of the screen, clamped so an edge can never pull away and show
   backing. Everything inside is therefore in photo percentages at every
   viewport, which is what makes one set of coordinates art-direct both. */
.hang__frame {
  z-index: 0;
  --W: max(100cqw, calc(100cqh * var(--room-ar)));
  --H: calc(var(--W) / var(--room-ar));
  position: absolute; top: 0; left: 0; width: var(--W); height: var(--H);
  translate: clamp(calc(100cqw - var(--W)), calc(50cqw - var(--fx) * var(--W)), 0px)
             clamp(calc(100cqh - var(--H)), calc(50cqh - var(--fy) * var(--H)), 0px);
  /* The push-in home.js drives. Its origin is the print, not the middle of the
     photograph, so pushing in never walks the print toward an edge. */
  transform-origin: calc(var(--fx) * 100%) calc(var(--fy) * 100%);
  scale: var(--push, 1); }
.hang__room { position: absolute; inset: 0; display: block; }
/* No filter. --room-b/--room-s went with .hang__print - they existed to correct
   bad stock interiors - so this computed to brightness(1) saturate(1): an
   identity filter that still forms a containing block and promotes a compositing
   layer on the largest image on the page. */
.hang__room img { display: block; width: 100%; height: 100%; object-fit: cover; }
/* The scrim belongs INSIDE the scene, not on the hold. .hangs__stack carries
   z-index:0, which is a stacking context, so a scrim on .hangs__hold::after
   painted over every caption no matter what z-index the caption claimed - the
   captions rendered grey and strengthening the gradient only greyed them
   further. Here it is a sibling of the frame and below the caption, inside the
   one context they share.
   Reaching full strength by 22% is measured, not guessed: the caption band sits
   18-30% up and the rooms are bright there (pale floors, lit stone). */
.hang::after { content: ""; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background:
    /* up from the bottom, for the caption */
    linear-gradient(to top, rgb(8 10 9 / .92) 0%, rgb(8 10 9 / .78) 22%,
      rgb(8 10 9 / .42) 44%, rgb(8 10 9 / .08) 74%),
    /* and down from the top, for the heading. Without this the white "Where it
       hangs" sat on a lit wall at 1.2:1 - a text-shadow is not a contrast
       floor, it only keeps type from dissolving into detail. */
    linear-gradient(to bottom, rgb(8 10 9 / .74) 0%, rgb(8 10 9 / .52) 12%,
      rgb(8 10 9 / .18) 24%, rgb(8 10 9 / 0) 36%); }
.hang__cap { position: absolute; z-index: 3; left: var(--sc-gutter); right: 3rem;
  bottom: calc(var(--tabs-h) + var(--sc-6)); display: grid; gap: .3rem; color: #fff;
  /* Driven separately from the scene so the captions hand over cleanly rather
     than overlapping through the dissolve. --chrome additionally fades it
     while the hold is not pinned, where its position is wrong. */
  opacity: calc(var(--cap, 1) * var(--chrome, 1)); }
/* 12px type needs 4.5:1, not the 3.4-4.2 it measured at .74 over a lit floor. */
.hang__cap .k { color: rgb(255 255 255 / .94); }
.hang__t { font-family: var(--sc-font-display); font-weight: 600; font-size: calc(var(--sc-t-lg) * var(--dsz)); letter-spacing: .01em;
  font-variant-numeric: lining-nums tabular-nums; }   /* the print size, e.g. 12x18 */
.hang__cap .link { color: #fff; justify-self: start; }
.hang__dots { position: absolute; z-index: 4; right: var(--sc-gutter);
  bottom: calc(var(--tabs-h) + var(--sc-6)); display: grid; gap: .45rem;
  opacity: var(--chrome, 1); }
.hang__dot { width: 6px; height: 6px; border-radius: 50%; background: rgb(255 255 255 / .34);
  transition: background-color 300ms var(--sc-ease-out); }
.hang__dot[data-on] { background: #fff; }

/* Reduced motion: no hold, no dissolve, no push-in. The rooms become an
   ordinary stacked section and the page scrolls normally past them. Giving
   .hang the room photograph's own aspect ratio keeps .hang__frame exactly
   covering it, so the print stays on its wall here too - the coordinate space
   is the same one, it just is not being cropped to a viewport. */
@media (prefers-reduced-motion: reduce) {
  .hangs { height: auto !important; }
  /* minmax(0,1fr), not the default auto column. An auto column is sized by its
     items' min-content width, and .hang__frame's width is a container query on
     .hang - which is circular, so the column resolved to 404px inside a 390px
     grid and the page scrolled sideways. */
  .hangs__hold { position: static; height: auto; overflow: visible;
    display: grid; grid-template-columns: minmax(0, 1fr);
    gap: var(--sc-6); padding-block: var(--sc-7); }
  .hangs__head { position: static; padding-inline: var(--sc-gutter); }
  /* NOT ink. The hold keeps its near-black ground in this layout, so switching
     the heading to ink put dark grey type on black. */
  .hangs__stack { position: static; display: grid; gap: var(--sc-6); }
  .hang { position: relative; inset: auto; opacity: 1 !important;
    aspect-ratio: var(--room-ar); scale: none; overflow: hidden; }
  /* The cover-and-shift maths exists to crop a photograph to a viewport. Here
     .hang IS the photograph's shape, so the frame just fills it - leaving the
     container-query width active was what overflowed the page. */
  .hang__frame { inset: 0; width: auto; height: auto; translate: none; scale: none; }
  /* bottom, not just opacity. The pinned layout parks the caption a tab bar
     plus a gutter up from the bottom (88px), which is right when .hang is the
     whole viewport. Here .hang has collapsed to aspect-ratio: var(--room-ar) -
     263px tall at 390px - so the same 88px pushed the caption to 33-76% of the
     scene, above a scrim that is clear by 52%. It measured 1.00:1 against a
     4.5 floor on bare room photograph: "IN AN OFFICE" over the office curtain
     was invisible. The offset has to be read in the box it is actually in. */
  .hang__cap { opacity: 1 !important; bottom: var(--sc-4); right: var(--sc-gutter); }
  /* ...and the scrim has to reach the whole caption. The old ramp was shaped
     for a caption band sitting in the bottom sixth; this one covers the band
     the rule above creates and is clear by 62%, so the room still reads. */
  .hang::after { background: linear-gradient(to top, rgb(8 10 9 / .92) 0%,
    rgb(8 10 9 / .88) 20%, rgb(8 10 9 / .72) 34%, rgb(8 10 9 / .30) 46%,
    rgb(8 10 9 / 0) 62%); }
  .hang__dots { display: none; }
}

/* The bar exists only from 960px up, so only there does the stacked heading
   need to clear it. */
@media (min-width: 960px) and (prefers-reduced-motion: reduce) {
  .hangs__hold { padding-top: var(--sc-7); }
}

/* ------------------------------------------------- tone and accent -- */
/* The page was one flat plaster tone end to end, which reads as a blank
   sheet with pictures on it however good the pictures are. Three values
   now carry the scroll - plaster, a lighter surface, then ink and black at
   the close - and the brand red marks the things you can act on. */
.shelves, .about { background: var(--sc-surface); }
.hangs { background: var(--sc-canvas); }
/* Hairlines at the joins, so a tone change reads as deliberate. */
.shelves, .hangs, .about { border-block: 1px solid var(--wall-line); }

/* Eyebrow labels in brand red: small, frequent, and always on something
   structural, which is how an accent colour stays an accent. */
.k { color: var(--sc-accent); }
.connect__in .k { color: rgb(255 255 255 / .62); }

/* A short red rule under each section heading. */
/* The signature on Jordan's print card, which is where this copy comes from.
   A short rule stands in for the dash the card has, since the build refuses an
   em dash and a hyphen would read as a typo. */
.about__sign { margin-top: var(--sc-4); }
/* Jordan's own signature, sized by eye against the copy next to it rather
   than by its file: the scan is 1.27:1 with a long descender on the J, so a
   width that looks right makes it taller than a line of type. */
.about__sign picture, .about__sign img { display: block; width: 100%; height: auto; }
.about__sign picture { width: 7.5rem; }
.about__sign::before { content: ""; display: block; width: 2rem; height: 2px;
  background: var(--sc-accent); margin-bottom: var(--sc-4); }
.shelves__head h2, .hangs__head h2, .about__copy h2, .band__say h2 { position: relative; }
.shelves__head h2::after, .hangs__head h2::after, .about__copy h2::after {
  content: ""; display: block; width: 2.5rem; height: 3px; margin-top: .75rem;
  background: var(--sc-accent); border-radius: 2px; }
.band__say h2::after { content: ""; display: block; width: 2.5rem; height: 3px;
  margin-top: .75rem; background: #fff; border-radius: 2px; }

/* Words rise in sequence when the heading arrives, AND come into focus as
   they do. Driven by a class the observer adds, not by scroll position, so it
   plays once and stays put - type that re-hides on scroll-up is a defect, not
   an effect.
   The focus pull is the point. A 0.7rem rise with a fade is the effect this
   section had, and it is the same "technically present, imperceptible in
   practice" that got the parallax thrown out. Blur-to-sharp is legible at a
   glance, and on a page selling photographic prints it is the one piece of
   motion vocabulary that is actually about the subject. */
.band__h .w { display: inline-block; overflow: hidden; vertical-align: bottom;
  padding-bottom: .08em; margin-bottom: -.08em; }
.band__h .w i { display: inline-block; font-style: inherit; will-change: transform, filter; }
/* Armed by home.js, so no-JS and failed-JS both read the heading. */
.band[data-arm] .band__h .w i { transform: translateY(108%); opacity: 0; filter: blur(14px); }
.band[data-lit] .band__h .w i { animation: wordfocus .82s cubic-bezier(.16,.84,.26,1) forwards;
  animation-delay: calc(var(--i) * 85ms); }
@keyframes wordfocus {
  60%  { opacity: 1; }
  to   { transform: none; opacity: 1; filter: blur(0); } }

/* The accent rule draws itself before the words land. */
.band[data-arm] .band__say h2::after { transform: scaleX(0); transform-origin: left; }
.band[data-lit] .band__say h2::after { animation: ruledraw .5s cubic-bezier(.2,.75,.25,1) forwards; }
@keyframes ruledraw { to { transform: scaleX(1); } }
.band__say .btn { margin-top: .4rem; }

/* ------------------------------------------------------ tap targets -- */
/* EVALUATOR A2. These were 18-26px tall: under a fingertip, and under the
   44px floor the rubric sets. (The note here used to claim this was done with
   padding "so the text keeps its baseline". It is not - it is a 44px flex box,
   and on a link that sits INSIDE a sentence that is a 44px line box in the
   middle of the paragraph. See the exception below.) */
.link, .hang__cap .link { display: inline-flex; align-items: center; min-height: 44px; }
/* The one link that runs inside a sentence rather than standing on its own
   line. The rule above opened a 44px gap before it that read as a blank line
   between the last two sentences. WCAG 2.5.8 exempts a link inline in a
   sentence from the target floor, so this one goes back to flowing with the
   text and takes vertical padding for a little hit area instead. */
.band__sizes .t-soft .link { display: inline; min-height: 0; padding: .25em 0; }
.foot__ig a, .foot__by a { display: inline-flex; align-items: center; min-height: 44px; }
.foot__by { gap: .35rem .6rem; }
.ig__tile { min-height: 44px; }
@media (min-width: 960px) {
  /* The bar is 56px tall, so its links can fill it rather than sitting at 29-41px. */
  .bar__index a, .bar__cart { display: inline-flex; align-items: center; min-height: 44px; }
}

/* --------------------------------------------------------- footer -- */
/* Fades to black so the page ends on the mark rather than on another pale
   band. Sits outside <main>: page furniture, identical everywhere. */
.foot { background: #000; color: #f1f2ee; padding: var(--sc-9) var(--sc-gutter) var(--sc-8);
  /* The fade. A gradient on the footer itself rather than a separate element,
     so there is nothing extra in the DOM and nothing to keep in sync. */
  background-image: linear-gradient(to bottom, var(--sc-ink) 0, #000 22%); }
/* The grid belongs on the inner wrapper. Putting it on .foot centred the
   wrapper as a single shrink-wrapped item and left its children aligned to
   each other rather than to the page. */
.foot__in { display: grid; justify-items: center; text-align: center;
  gap: var(--sc-4); max-width: 62rem; margin-inline: auto; }
.foot__mark { width: min(26rem, 78%); height: auto; }
.foot__legal { max-width: 46ch; font-size: var(--sc-t-xs); color: rgb(241 242 238 / .66); line-height: 1.6; }
.foot__ig { font-size: var(--sc-t-sm); }
.foot a { color: inherit; }
.foot__ig a { color: rgb(241 242 238 / .9); }
.foot__by { display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
  gap: .5rem; margin-top: var(--sc-4); padding-top: var(--sc-4);
  border-top: 1px solid rgb(241 242 238 / .14); width: min(26rem, 78%);
  font-size: var(--sc-t-xs); color: rgb(241 242 238 / .55); }
.foot__by a { display: inline-flex; align-items: center; gap: .45rem; color: rgb(241 242 238 / .8); text-decoration: none; }
.foot__by a:hover { color: #fff; }
.foot__by img { height: 1.6rem; width: auto; }
.foot__by b { font-weight: 700; }

/* ------------------------------------------------------- instagram -- */
.connect__head { display: grid; justify-items: start; gap: var(--sc-4); }
.ig-glyph { width: 2.4rem; height: 2.4rem; fill: none; stroke: var(--sc-accent); stroke-width: 1.6; }
.ig-glyph .dot { fill: var(--sc-accent); stroke: none; }
/* The handle is one long unbreakable token, so the shared scale's REM FLOOR is
   what strains it: at 390px it set 35px and the word ran to within a few px of
   the gutter. Scaling off the viewport the whole way down leaves a gutter that
   matches the rest of the section. */
.connect__head h2 { font-size: clamp(2.06rem, 9.7vw, 4rem); letter-spacing: .005em; }  /* hand-written clamp, so --dsz does not reach it: scaled by hand, and the
     -.01em it used to carry was the last negative tracking on a Cormorant
     heading left after the swap. */
.connect__cta { margin-top: .5rem; }
/* Six square frames, so the section reads as a feed rather than a paragraph
   with a link in it. Three across on a phone: at two across each tile was big
   enough to show the crop failing. */
.ig__grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: .4rem; margin-top: 0; }
.ig__tile { display: block; aspect-ratio: 1; overflow: hidden; border-radius: var(--r);
  background: #000; min-height: 44px; }
.ig__tile picture, .ig__tile img { display: block; width: 100%; height: 100%; object-fit: cover; }
/* Per-tile crop anchor. Centre is wrong for most of these - the subject sits
   high in an animal portrait, so centring cuts its head off in a square. */
.ig__tile img { object-position: var(--pos, 50% 50%);
  transition: transform 420ms var(--sc-ease-out), opacity 220ms linear; opacity: .94; }
.ig__tile:hover img, .ig__tile:focus-visible img { transform: scale(1.05); opacity: 1; }

/* -------------------------------------------------------------- logo -- */
/* Sized from its height so a short, wide mark cannot outgrow the bar. */
.bar__mark img { display: block; height: 100%; width: auto; }
/* The lockup is ~10.4:1, so it is sized by WIDTH and lets height fall out;
   setting a height instead would overflow the bar. Checked at 390px: the
   "LONGMONT, COLORADO" subline still reads, so the full lockup is fine on a
   phone and no stacked variant is needed. */

/* ------------------------------------------------------- full bleed -- */
/* The photograph is a flat backdrop now. The sticky parallax was clunky and
   bought nothing the type does not already do. */
.band { position: relative; isolation: isolate; background: var(--sc-ink);
  margin: var(--sc-8) 0; padding: var(--sc-9) 0; overflow: hidden; }
.band__pic { position: absolute; inset: 0; z-index: 0; }
.band__pic picture, .band__pic img { display: block; width: 100%; height: 100%; object-fit: cover;
  /* The frame is 2400x1590 and the section is tall on a phone, so a centred
     cover crop is almost all sky and the Tetons sit on the bottom edge. Favour
     the lower part of the picture so the range is IN the section. Reset at
     960px, where the section is wide enough to take the frame as composed. */
  object-position: 50% 72%;
  /* NO brightness filter. An earlier pass lifted this 1.5x to make the frame
     read, and it did - into blue twilight with raised shadows and pumped
     saturation. That is not this photograph. Altering a photographer's own
     exposure on their own portfolio to make a section livelier is the wrong
     trade; the wash is what was hiding it, so the wash is what moved. */ }
.band__pic::after { content: ""; position: absolute; inset: 0; z-index: 1;
  /* .42-.64, down from .80-.95. The type keeps its own contrast with a shadow,
     the same trade the hero makes, instead of the whole picture paying for it. */
  background: linear-gradient(to bottom, rgb(10 12 11 / .24) 0%, rgb(10 12 11 / .36) 55%,
                                         rgb(10 12 11 / .44) 100%); }
.band__say { position: relative; z-index: 2; color: #F1F2EE;
  padding: 0 var(--sc-gutter); display: grid; gap: var(--sc-5);
  text-shadow: 0 1px 2px rgb(0 0 0 / .55), 0 2px 16px rgb(0 0 0 / .45); }
.band__lead { color: rgb(241 242 238 / .86); max-width: 40ch; }
.band__h { max-width: 18ch; }

.meds { display: grid; gap: var(--sc-5); margin-top: var(--sc-3); }
.med { display: grid; gap: .3rem; padding-top: var(--sc-4); position: relative; }
/* A pseudo-element, not border-top, so the rule can DRAW across rather than
   just appear with the block. */
.med::before { content: ""; position: absolute; left: 0; right: 0; top: 0; height: 1px;
  background: rgb(241 242 238 / .22); transform-origin: left; }
.med__n { font-family: var(--sc-font-display); font-size: calc(var(--sc-t-lg) * var(--dsz)); font-weight: 600;
  letter-spacing: .01em; color: #fff; }
.med__d { font-size: var(--sc-t-sm); color: rgb(241 242 238 / .74); max-width: 42ch; }

.band__sizes { display: grid; gap: .7rem; margin-top: var(--sc-4);
  padding-top: var(--sc-5); border-top: 1px solid rgb(241 242 238 / .18); }
.band__sizes .k { color: rgb(241 242 238 / .6); }
.band__sizes .t-soft { color: rgb(241 242 238 / .6) !important; font-size: var(--sc-t-sm); }
.sizes { display: flex; flex-wrap: wrap; gap: .5rem; }
.sizes li { font-variant-numeric: tabular-nums; font-size: var(--sc-t-sm); font-weight: 600;
  /* --r-btn: these are the same "11x14 / 12x18 / 16x24" pills as the size
     options in the buy panel, same labels and same shape. Two corner radii for
     one control on one site reads as an oversight. */
  color: #fff; padding: .45rem .8rem; border: 1px solid rgb(241 242 238 / .28); border-radius: var(--r-btn); }
/* A footnote, not body copy. 1.62 leading over two lines of 15px type under
   the pills is what made consecutive sentences read as separate blocks. */
.band__sizes .t-soft { line-height: 1.5; max-width: 46ch; }

/* The surfaces arrive after the heading and in three beats each: the rule
   draws across, the name lifts into focus behind it, then the description
   settles. Staggered by --i so the eye is led down the column rather than
   handed all three at once. */
.band[data-arm] .med::before { transform: scaleX(0); }
.band[data-lit] .med::before { animation: ruledraw .55s cubic-bezier(.2,.75,.25,1) forwards;
  animation-delay: calc(.40s + var(--i) * 140ms); }

.band[data-arm] .med__n { opacity: 0; transform: translateY(1.1rem); filter: blur(10px); }
.band[data-lit] .med__n { animation: wordfocus .7s cubic-bezier(.16,.84,.26,1) forwards;
  animation-delay: calc(.52s + var(--i) * 140ms); }

.band[data-arm] .med__d { opacity: 0; transform: translateY(.8rem); }
.band[data-lit] .med__d { animation: medrise .6s cubic-bezier(.2,.75,.25,1) forwards;
  animation-delay: calc(.66s + var(--i) * 140ms); }

/* The lead and the sizes block ride the same sequence - the brief was all of
   the type, and a paragraph sitting still in the middle of a staged reveal
   reads as the one thing that failed to load. */
.band[data-arm] .band__lead { opacity: 0; transform: translateY(1rem); filter: blur(8px); }
.band[data-lit] .band__lead { animation: wordfocus .74s cubic-bezier(.16,.84,.26,1) forwards;
  animation-delay: .34s; }

.band[data-arm] .band__sizes .k,
.band[data-arm] .band__sizes .t-soft { opacity: 0; transform: translateY(.7rem); }
.band[data-lit] .band__sizes .k { animation: medrise .55s cubic-bezier(.2,.75,.25,1) forwards;
  animation-delay: .54s; }
.band[data-lit] .band__sizes .t-soft { animation: medrise .55s cubic-bezier(.2,.75,.25,1) forwards;
  animation-delay: .86s; }
/* the rule above the sizes draws with them */
.band[data-arm] .band__sizes { border-top-color: transparent; }
.band[data-lit] .band__sizes { animation: rulefade .6s ease forwards; animation-delay: .5s; }
@keyframes rulefade { to { border-top-color: rgb(241 242 238 / .18); } }

.band[data-arm] .sizes li { opacity: 0; transform: translateY(.7rem) scale(.92); }
.band[data-lit] .sizes li { animation: chippop .5s cubic-bezier(.2,.9,.3,1.25) forwards;
  animation-delay: calc(.60s + var(--i) * 70ms); }
@keyframes medrise { to { opacity: 1; transform: none; } }
@keyframes chippop { to { opacity: 1; transform: none; } }

/* --------------------------------------------------------- connect -- */
/* Spruce, not plaster: the second dark-ish stop on the way down, and it marks
   the end of the browsing and the start of "talk to a person". */
.connect { background: var(--sc-ink); color: var(--sc-accent-ink); padding: var(--sc-10) 0; }
.connect__in { padding: 0 var(--sc-gutter); display: grid; gap: var(--sc-7); max-width: 46ch; }
.connect__in .k { color: rgb(241 242 238 / .66); }
.connect__in .t-body { color: rgb(241 242 238 / .9); }
.connect__links { display: grid; gap: .6rem; margin-top: .5rem; }
/* Inverted: on spruce, the outline button needs a light edge, and the solid
   one has to stop being spruce-on-spruce. */
.connect .btn { border-color: rgb(241 242 238 / .5); color: var(--sc-accent-ink); }
.connect .btn--solid { background: var(--sc-accent); border-color: var(--sc-accent); color: #fff; }
.connect .ig { width: 1.15rem; height: 1.15rem; flex: none; fill: none;
  stroke: currentColor; stroke-width: 1.7; }
.connect .ig .dot { fill: currentColor; stroke: none; }
.connect__where { color: rgb(241 242 238 / .66) !important; margin-top: .35rem; }

/* ============================================================ DESKTOP == */
@media (min-width: 960px) {
  /* THE BAR SITS AT THE BOTTOM on a wide screen, as it already does on a
     phone, so the two layouts agree and the top of every page is the
     photograph rather than a strip of navigation. */
  body { padding-top: 0; padding-bottom: var(--bar-h); }
  /* The hero runs the full height of the window and the fixed bar floats over
     its last 56px, so the film reaches all four edges.
     NOT a negative bottom margin: that does not extend the hero under the bar,
     it pulls the NEXT section up over the hero's last 56px - and that section
     has the pale canvas background, so the bar's dark scrim was compositing
     over cream instead of over the photograph and reading as a light plaster
     strip with white text on it. */
  .hero { height: 100svh; }
  .tabs { display: none; }
  .only-m { display: none !important; }
  .only-d { display: revert !important; }
  /* Taller than the phone bar, which is what gives the wordmark room to be
     read at all: it is 1100x105, so every pixel of height buys ten of width. */
  :root { --bar-h: 4.75rem; }
  .bar { display: flex; position: fixed; inset: auto 0 0 0; z-index: 60; height: var(--bar-h); align-items: center; gap: var(--sc-6);
    padding-inline: var(--sc-gutter); padding-bottom: var(--safe-b); background: var(--sc-canvas); border-top: 1px solid var(--wall-line);
    /* The brand red, carried on every page. Inside the bar rather than as a
       border, so it survives the transparent treatment over the hero below. */
    box-shadow: inset 0 -3px 0 0 var(--sc-accent);
    transition: background-color 260ms var(--sc-ease-out), color 260ms var(--sc-ease-out), border-color 260ms var(--sc-ease-out); }
  /* Over the hero photograph: no plaster strip across the picture. */
  body[data-hero-top] .bar { background: linear-gradient(to top,
      rgb(8 10 9 / .48) 0%, rgb(8 10 9 / .28) 55%, rgb(8 10 9 / 0) 100%);
    border-top-color: transparent; color: #fff; }
  /* The scrim is short; a text shadow carries the rest over a bright frame. */
  body[data-hero-top] .bar a, body[data-hero-top] .bar button { text-shadow: 0 1px 6px rgb(0 0 0 / .55); }
  body[data-hero-top] .bar__mark img { filter: brightness(0) invert(1); }
  /* 2.25rem, not the full inner height of the bar. The wordmark is 10.5:1, so
     height is a 10x lever on width: filling the bar put a 580px mark across
     40% of a 1440 screen. At 36px it is ~378px - present, not dominant - and
     the space left around it reads as deliberate rather than as a bar the logo
     has outgrown.
     It is also HIDDEN until the hero's own wordmark has gone: two of the same
     mark on screen at once is one too many, and the bar has nothing to say
     while the hero is still introducing the name. visibility, not opacity
     alone, so the hidden link is out of the tab order rather than an invisible
     target. */
  .bar__mark { display: inline-flex; align-items: center; min-height: 44px;
    flex: none; margin-right: auto; line-height: 0;
    transition: opacity 260ms var(--sc-ease-out), visibility 0s linear 0s; }
  /* Sized on the IMAGE, not the link. The link keeps min-height: 44px as a tap
     target, and `height: 100%` of that made the mark 44px tall and 461px wide
     no matter what height the link was given - the shorthand was being
     overridden by its own min-height. */
  .bar__mark img { height: 2rem; width: auto; }
  body[data-hero-mast] .bar__mark { opacity: 0; visibility: hidden;
    transition: opacity 180ms var(--sc-ease-out), visibility 0s linear 180ms; }
  .bar__index { display: none; gap: var(--sc-5); font-size: var(--sc-t-sm); font-weight: 500; }
  .bar__index a { text-decoration: none; padding: .5rem 0; white-space: nowrap; }
  .bar__index a sup { font-size: .65em; color: var(--sc-ink-soft); margin-left: .2em; font-variant-numeric: tabular-nums; }
  .bar__index a:hover { text-decoration: underline; }
  .bar__index [aria-current="page"] { color: var(--sc-accent); font-weight: 700; }
  .bar__btn { border: 0; background: none; font-weight: 600; cursor: pointer; padding: .5rem 0; }
  .bar__cart { text-decoration: none; font-weight: 600; display: inline-flex;
    align-items: center; justify-content: center; gap: .1rem;
    /* The icon is 20px, so without this the link is a 20px-wide target. */
    min-width: 44px; min-height: 44px; }
  .sheet { margin: 0 0 0 auto; height: 100%; max-height: none; width: 26rem; border-radius: 0; box-shadow: -20px 0 50px -20px rgb(26 29 27 / .35); }
  .g-head { padding: var(--sc-8) var(--sc-gutter) var(--sc-6); grid-template-columns: 1fr minmax(0, 28rem); align-items: end; }
  .g-chips { display: none; }
  /* Six columns on a wide screen. 15.7vw is the column width that
     (100vw - 2*gutter - 5*gap) / 6 works out to, which is what the square
     base cell needs grid-auto-rows to be. */
  .grid { --gap: .75rem; --row: 15rem; }
  /* Eight columns, so a 3-wide landscape slot is ~480px on a 1440 screen and a
     2-wide portrait is ~320px - against 380px and 250px at ten columns. */
  .grid--mosaic { --cols: 8; --ru: 1; --gut: var(--sc-gutter); }
  .grid--mosaic > li { grid-column: var(--dc) / span var(--dcs);
    grid-row: var(--dr) / span var(--drs); }
  .tile__cap { font-size: var(--sc-t-sm); }
  .lb[open] { grid-template-rows: none; grid-template-columns: minmax(0, 1fr) 25rem; }
  .lb__stage picture { inset: 2rem; width: calc(100% - 4rem); height: calc(100% - 4rem); }
  /* The panel's tight spacing is a PHONE compromise - every pixel there comes
     off the photograph. The desktop panel is a 25rem sidebar with room to
     spare, so it takes its rhythm back. */
  .lb__panel { padding: 2.25rem 2rem; gap: 1.75rem; max-height: none; align-content: start; }
  .opt { gap: .9rem; }
  .opt legend { margin-bottom: .3rem; }
  .buy { gap: 1.75rem; }
  .opt label { min-height: 3.1rem; font-size: var(--sc-t-base); }
  .opt__row { gap: .6rem; }
  .buy__surface { margin-top: 1rem; }
  .buy__act { margin-top: 1rem; }
  .buy__act .btn { min-height: 3.4rem; }
  /* Back to one row each. Three across is a PHONE fix - it bought ~100px of
     panel height there. The desktop sidebar has that height spare and then
     some, and a full-width row puts the surface name and its price at
     opposite ends of a line, which is easier to compare than three stacked
     pairs. */
  .opt.opt--stack .opt__row { grid-template-columns: 1fr; }
  .opt.opt--stack label { flex-direction: row; justify-content: space-between;
    text-align: left; padding: 0 1rem; font-size: var(--sc-t-base); }
  .opt.opt--stack label .num { font-size: var(--sc-t-base); opacity: 1; }
  .opt.opt--stack label br { display: none; }
  .buy__surface { font-size: var(--sc-t-sm); line-height: 1.45; min-height: calc(3 * 1.45em); }
  .lb__desc { font-size: var(--sc-t-sm); line-height: 1.5; margin-top: .4rem; }
  .lb__panel { max-height: none; border-top: 0; border-left: 1px solid var(--wall-line); align-content: start; padding: 4rem 1.75rem 2rem; }
  .lb__x { top: 1rem; right: 1rem; }
  .lb__nav { bottom: 50%; transform: translateY(50%); }
  .cart { grid-template-columns: minmax(0, 1fr) 22rem; align-items: start; padding-top: var(--sc-8); gap: var(--sc-8); }
  .sum { position: sticky; top: 2rem; }
  .line { grid-template-columns: 7rem 1fr; }

  /* hero: the phone composition given a landscape frame and more air. Same
     structure, not a different design - the mobile one is the design. */
  /* Clear the fixed bar the hero is pulled up under. */
  /* Up into the band's full-density zone. At the old padding the mark sat at
     y 88-145 of a 900px stage, which put its second line - the thin,
     letterspaced "LONGMONT, COLORADO" - in the gradient's tail, where it
     measured 2.6:1 over the owl at t=33s while the first line was fine. */
  .hero__mast { padding-top: var(--sc-6); }
  /* 3rem, down from var(--sc-9) at 96px: that put the headline 34% down a
     1440x900 frame with 216px of empty picture above it, so the type sat
     across the middle of the photograph rather than under it. */
  .hero__copy { padding: 0 var(--sc-gutter) 3rem; gap: var(--sc-5);
    max-width: 58rem; }
  .hero__mark { width: min(40rem, 46%); }
  .hero__h { font-size: clamp(2.83rem, 1.65rem + 2.6vw, 4.25rem); max-width: 17ch; }
  /* The veil's stops are tuned to MEASURED type positions, and the serif at
     +18% moved them: the desktop headline now spans veil 36-66% where it spanned
     36-61%. Its top line therefore landed in the 60->70% falloff, over .34 of
     wash instead of .51, and verify/hero.py came back 2.94:1 against a 3.0 floor
     on the owl-against-pale-sky frame at t=32.4s.
     This is a redistribution inside the band the veil already covers, not more
     veil: 0-46% is untouched, the clear point stays at 78%, and the shoulder is
     simply steeper so the headline keeps cover to its new top edge. Nothing
     above hero 61% changes. Phone is left alone - its headline tops out at veil
     48%, inside the flat .66 band, and measured 6.58:1. */
  .hero__veil { background: linear-gradient(to top, rgb(8 10 9 / .52) 0%, rgb(8 10 9 / .68) 20%,
                                      rgb(8 10 9 / .70) 32%, rgb(8 10 9 / .66) 46%,
                                      rgb(8 10 9 / .62) 60%, rgb(8 10 9 / .56) 66%,
                                      rgb(8 10 9 / .22) 72%,
                                      rgb(8 10 9 / 0) 78%, rgb(8 10 9 / 0) 100%); }
  .hero__foot { padding-bottom: calc(var(--bar-h) + var(--sc-4)); }


  .shelves { padding-top: var(--sc-10); gap: var(--sc-8); }
  .shelf__row { --shelf-h: min(28.1vw, 21.8rem); gap: var(--sc-4); }   /* +30%, then +20% */
  /* No cap: a desktop row already shows three or four, so the affordance
     is there without cropping anything. */
  .shelf__item picture { max-width: none; }
  .band__pic picture, .band__pic img { object-position: 50% 50%; }
  .shelf__t { font-size: calc(var(--sc-t-xl) * var(--dsz)); font-weight: 500; letter-spacing: .005em; }

  .wallact { --wall-h: min(calc(100svh - 7rem), 54rem); }
  .wall { grid-template-columns: 24rem minmax(0, 1fr); grid-template-rows: 1fr auto; padding: 0 0 var(--bar-h); }
  .wall__intro { grid-column: 1; grid-row: 1; align-content: start; padding: 3rem 0 0 var(--sc-gutter); gap: var(--sc-3); }
  .wall__intro h2 { font-size: calc(var(--sc-t-2xl) * var(--dsz)); }
  .wall__intro p { font-size: var(--sc-t-base); }
  .wall__scene { grid-column: 1 / -1; grid-row: 1 / span 2; align-self: end; margin-top: 0; height: calc(var(--wall-h) + 2rem); }
  .wall__scene .wall__floor, .wall__scene .wall__hang, .wall__scene .wall__ruler { bottom: 0; }
  .wall__label { position: absolute; left: var(--sc-gutter); bottom: calc(var(--ppi) * 4 + 2.5rem); width: 20rem; padding: 0; z-index: 3; }
  .wall__steps { position: absolute; left: var(--sc-gutter); bottom: 1.75rem; width: 20rem; padding: 0; z-index: 3; }
  .wall__ruler { left: calc(25rem + (100% - 25rem) / 2 - var(--ppi) * 18 - 4.5rem); }
  .wall__hang { left: calc(25rem + (100% - 25rem) / 2); }
  .wall__size { font-size: var(--sc-t-3xl); }

  /* The picture column is sized to the montage now rather than taking the
     rest of the row, and the pair is centred. At 1fr the two blocks drifted
     to opposite edges of a 1440px screen once the montage stopped being
     46rem wide. */
  .about { grid-template-columns: minmax(0, 30rem) 26rem; justify-content: center;
    gap: var(--sc-9); align-items: center; padding-block: var(--sc-10); }
  /* 26rem, not 46. At 46 the montage rendered 688x887 next to a column of
     copy 512x511 - the photographs were the section and the words were a
     caption. This brings the two blocks to about the same height. */
  .about__pic { max-width: 26rem; justify-self: end; width: 100%; }
  .close__plate { left: var(--sc-gutter); right: auto; bottom: 3rem; width: 26rem; padding: 1.75rem; }
  .close__cap { top: 1rem; }
  /* band: generous, but sized by its content. min-height:138svh was travel for
     the sticky parallax that no longer exists - with the type at the top it
     left about 726px of near-black with nothing on it, which a frame-hash
     check cannot see because the photograph underneath keeps changing by a
     percent or two. */
  .band { margin: var(--sc-9) 0; padding-block: var(--sc-10); }
  .band__say { max-width: 44ch; padding-bottom: var(--sc-8); }
  .band__cap { position: absolute; right: var(--sc-gutter); top: var(--sc-4); padding: 0; text-align: right; }
  /* connect: the ask and the links side by side, links right-aligned */
  .connect__in { max-width: none; grid-template-columns: 1fr auto; align-items: end;
    gap: 2rem var(--sc-8); max-inline-size: var(--sc-maxw, 82rem); margin-inline: auto; }
  .connect__in .k, .connect__in h2, .connect__in .t-body:not(.connect__where) { grid-column: 1; }
  .connect__links { grid-column: 2; grid-row: 1 / span 3; display: flex; flex-direction: column;
    align-items: stretch; margin-top: 0; min-width: 16rem; }
  .connect__where { grid-column: 1; }
  /* hangs: same single cross-fading viewport, caption given a measure. */
  .hang__cap { max-width: 44ch; right: auto; }
  /* band: the copy and the mediums side by side rather than one long column. */
  .band__say { max-inline-size: var(--sc-maxw, 82rem); margin-inline: auto;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: var(--sc-7) var(--sc-9);
    align-items: start; }
  .band__h, .band__lead { grid-column: 1; }
  .meds { grid-column: 2; grid-row: 1 / span 3; margin-top: 0; }
  .band__sizes { grid-column: 1; }
  /* instagram: the copy beside the grid */
  .connect__in { grid-template-columns: minmax(0, .85fr) minmax(0, 1fr); align-items: center;
    gap: var(--sc-9); max-width: var(--sc-maxw, 82rem); margin-inline: auto; }
  .connect__head { grid-column: 1; max-width: 34ch; }
  .ig__grid { grid-column: 2; grid-template-columns: repeat(2, 1fr); margin-top: 0; }
}
@media (min-width: 1200px) { .bar__index { display: flex; } .bar__btn { display: none; } }

@media (prefers-reduced-motion: reduce) {
  .wallact { height: auto !important; }
  .wallact .sc-stage { position: relative; }
  .btn, .tile picture, .opt label, .wall__steps button { transition: none; }
  /* A section that reserves scroll for an animation which is not going to run
     is just dead scroll. Collapse every held section to its content. */
  /* home.js never fetches the clip under reduce, so the poster IS the hero
     here - not a placeholder waiting on a film that is not coming. Everything
     else in the hero is already static. */
  .hero__film { display: none; }
  .hero__cue  { animation: none; transform: scaleY(1); }
  .band { min-height: 0; }
  .band__pic { position: static; height: 72svh; }
  .band__say { margin-top: 0; min-height: 0; padding-block: var(--sc-7); background: var(--sc-ink); }
  .band__h .w i { animation: none !important; }
}
