/* Astratto, implementation of the "Astratto Homepage v4 Live" design, extended sitewide */

:root {
  /* Three tiers, two loaded families, plus a system mono stack for the mail window.
     Poster tier (--font-display) is Comfortaa, a rounded geometric. Applied by hand to the
     hero, page titles and the oversized numerals, never to h1/h2/h3 wholesale.
     Heading tier and body tier are both Archivo, a neutral grotesque, separated by weight
     (700 vs 400) rather than by family.
     The poster tier has been through four faces now: Anton, Quicksand, Fraunces, Comfortaa.
     Each swap changed the set width of the same string, so each one forced a rescale of every
     display SIZE below -- the measured ratios are noted where the sizes are. */
  /* Comfortaa, standing in for ITC Ronda Light.

     Ronda itself is a licensed ITC face and is not on Google Fonts, so it cannot be linked
     here; it would have to be bought and self-hosted. Comfortaa is the closest free face to
     it: a rounded geometric with fully circular bowls, monoline strokes and the same wide,
     soft 1970s cast. If the real Ronda is ever licensed, drop the woff2 into a @font-face and
     change this one line -- but re-measure first, because the sizes below are tied to a
     specific set width.

     Display weight is 600 rather than 700. The reference is Ronda LIGHT, and Comfortaa's
     lighter grades read closer to it, but the hero still sits on a photograph, so this stops
     at 600 rather than going down to 300 where it would start to disappear.

     Every display size below came DOWN by .7807. Measured in the page with both faces live:
     Comfortaa 600 sets the hero string 1.2809x wider than Fraunces did at the same pixel
     size, so holding the existing line breaks means dividing by that. */
  --font-display: Comfortaa, "Century Gothic", "Avenir Next", sans-serif;
  /* Palette: Moonlight White / Radome Tan / Blackberry / Romaine / Salem / Rousseau Green.
     Token names are slots, not colour names -- --sand is the secondary surface and --blush the
     process field -- so each mapping is spelled out below. */
  --cream: #F9F0DE;        /* Moonlight White */
  --ink: #14261D;
  --forest: #175844;       /* Rousseau Green */
  /* Salem -- the palette's brightest green. Links, bullets, numerals, and the column behind
     the process half circles. */
  --leaf: #177B4D;         /* Salem */
  /* Only the contact form's error text now. The .wine surface modifier was renamed
     .aubergine when those surfaces moved to #3f1e33. */
  --wine: #43182F;
  --plum: #5A2340;
  --plum-dark: #43182F;
  --sand: #C0D2AD;         /* Romaine */
  --sand-deep: #B0C69A;
  --sand-line: #C9B893;
  /* One token for every editorial rule on the site, so section breaks, column dividers and
     table rows all draw the same line. Ink at 55%, NOT --sand-line: a pale tan hairline on a
     cream ground is effectively invisible, which is why the ruled layout was not reading. The
     references all use a near-black line and that is what makes the structure legible. */
  --rule: rgba(20, 38, 29, .55);
  --tan: #F1C7A1;          /* Radome Tan */
  /* Fibra pink. TWO sanctioned uses, and they have opposite contrast rules, so keep them apart.

     1. AS TEXT, which is the original use: small type on the DARK grounds only. It clears AA on
        the aubergine at 6.65:1 and higher on the pine, but only 3.82:1 on Rousseau, so pink
        type stays off the green bands. Used on the step numbers, the footer and menu heads,
        the newsletter error, and the doughnut ring on the closing CTA.

     2. AS A SURFACE carrying INK type, added 2026-09-02: the bar that slices a numbered chapter
        heading. Measured 7.26:1 live, which is comfortably AA, because the contrast question
        inverts when the pink is the ground rather than the ink. The earlier note here said
        "never a surface"; that was an aesthetic caution about a saturated colour carrying an
        area, not a contrast limit, and Michelle asked for exactly this.

     Rationed on purpose. It is the loudest thing in the palette and it only works while it
     stays rare. */
  --pink: #F98BCE;
  --taupe: #444E41;
  --hero-ink: #F1E6D2;
  /* Process band: deep green half circles on a Radome Tan field, with a Salem column behind.
     The band was pink before this palette, which has no pink -- the warm ground comes from the tan. */
  --blush: #F1C7A1;        /* Radome Tan */
  --pine: #0E3B2C;
  --coral: #177B4D;        /* Salem */
  /* A soft rose, used ONLY where contrast rules do not apply: underlines, and the selection
     highlight where it is the ground rather than the ink. As body text it fails everywhere on
     this palette (2.56:1 even on the lightest ground), so it never colours a word directly. */
  --rose: #F26E72;
  /* Link underlines. Coral under words reads as a spellchecker marking a mistake, which is
     the one thing an underline must never say. Green says the same "this is a link" without
     the alarm. Two values because an underline has to survive both grounds: Salem is legible
     on cream, and vanishes on the dark bands, where Romaine takes over. */
  --underline: #177B4D;
  --underline-on-dark: #C0D2AD;
  --paper: #F3E6CE;
  /* The site's dark surface: nav frost, dark panels, dark stat tiles, footer. Left at the
     exact logo purple rather than moved to the palette's Blackberry #43182F -- the two are
     four units apart and indistinguishable, so there is nothing to gain by changing it. */
  --aubergine: #3f1e33;
  /* A lighter Blackberry, mixed off the logo colour rather than invented. Full-strength
     aubergine reads as near-black at donut scale, where the ring is only 12px of wall, and on
     a mid green like Romaine it goes muddy instead of purple. This measures 4.69:1 there, so
     it stays a colour. Used on the Romaine rows only: on the cream cards the full-strength
     one has the paler ground to read against and is the stronger mark. */
  --aubergine-soft: #7A4165;
  --font-mono: ui-monospace, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
  --aubergine-rgb: 63 30 51;
  /* The nav's scrolled frost, the same colour at 72%. Used by BOTH the scroll-timeline
     keyframe and the .nav.frost class the JS fallback toggles; they must match or Chromium
     and everything else render a different nav, so they read it from here. */
  --nav-frost: rgb(var(--aubergine-rgb) / .72);

  /* One corner radius for every card on the site. */
  --radius: 24px;
  --ease-out: cubic-bezier(.2, .7, .2, 1);
}

* { box-sizing: border-box; }

/* Browser surfaces, selection, caret, and scrollbars ship with defaults that
   belong to no design system. Theme them from the palette. */
::selection { background: var(--rose); color: var(--ink); }
:root { scrollbar-color: var(--leaf) var(--sand); }
::-webkit-scrollbar { width: 12px; height: 12px; }
::-webkit-scrollbar-track { background: var(--sand); }
::-webkit-scrollbar-thumb { background: var(--leaf); border: 3px solid var(--sand); border-radius: 8px; }
::-webkit-scrollbar-thumb:hover { background: var(--forest); }
input, textarea, select { caret-color: var(--plum); }

/* The page sits inside a drawn frame with a margin around it. html carries the ground, so the
   margin stays cream on every page and at any scroll position; the border goes on body so it
   wraps the CONTENT and grows with the document rather than boxing the viewport.

   Everything fixed or sticky has to be inset by the same amount, because those position
   against the viewport and would otherwise sit outside the frame: the nav, and the hero pin.
   The hero also loses twice the edge from its height, or a 100svh pin inside a page that is
   24px shorter overflows the frame by exactly that much. */
html { background: var(--cream); }
body {
  --page-edge: 12px;
  margin: var(--page-edge);
  border: 1.5px solid var(--ink);
  background: var(--cream);
  color: var(--ink);
  font-family: Archivo, system-ui, sans-serif;
  font-weight: 400;
  -webkit-font-smoothing: antialiased;
}

a { color: var(--ink); text-decoration: none; }
/* :where() on the pseudo-class, so this whole rule weighs the same as the bare `a` above it
   instead of (0,1,1).

   As a normal `a:hover` it outranked every component that sets its own link colour, and won
   on hover no matter what the component wanted. On the plum deck card that meant the text
   turned plum on a plum ground and vanished under the cursor. Any link painting its own
   ground had the same bug waiting.

   At this weight it still styles plain links, which set no colour of their own and lose the
   tie to it by source order, and it now yields to any component that states a colour. */
a:where(:hover) { color: var(--plum); text-decoration: underline; text-decoration-color: var(--underline); text-decoration-thickness: 2px; text-underline-offset: 3px; }
h1, h2, h3 { margin: 0; font-weight: 700; letter-spacing: -.01em; }
p { margin: 0; }
/* height:auto is required now that build.ps1 stamps intrinsic width/height on every
   <img>, without it, max-width would scale the width while the height attribute held,
   squashing the image. Every framed image (.pf-thumb, .collage-fig, .hero-bg,
   .portrait-frame) overrides this with an explicit height:100% + object-fit. */
img { max-width: 100%; height: auto; }

/* Focus ring: two-tone so it stays visible on cream, sand, forest and aubergine alike. */
a:focus-visible, button:focus-visible, input:focus-visible,
textarea:focus-visible, select:focus-visible, [tabindex]:focus-visible {
  outline: 2px solid var(--plum);
  outline-offset: 2px;
  box-shadow: 0 0 0 4px rgba(249, 240, 222, .85);
  border-radius: 2px;
}
.band-forest a:focus-visible, .page-hero a:focus-visible, .topbar a:focus-visible,
.topbar button:focus-visible, footer a:focus-visible, footer input:focus-visible,
footer button:focus-visible, .panel.aubergine a:focus-visible, .panel.forest a:focus-visible,
.lb button:focus-visible, .mobile-menu a:focus-visible {
  outline-color: var(--tan);
  box-shadow: 0 0 0 4px rgba(20, 38, 29, .55);
}

/* Clipped interactive surfaces need an INSET ring. clip-path removes everything outside
   the silhouette, and an outline sits outside the border box by definition, so the ring
   above would be cut away entirely and these controls would show no focus at all.
   Inset shadows are painted inside the box, so they survive the clip. */
.pf-card:focus-visible, .svc-row:focus-visible {
  outline: none;
  box-shadow: inset 0 0 0 3px var(--plum), inset 0 0 0 7px rgba(249, 240, 222, .9);
}

/* Figures in stat tiles and galleries line up column-wise. */
.stat-num, .step-num, .step-row .n, .lb-count { font-variant-numeric: tabular-nums; }

/* ---------- shape vocabulary ----------
   Five silhouettes, defined once as objectBoundingBox clipPaths in the template and
   stretched to whatever they are applied to. This is the whole vocabulary, the arch
   (border-radius: 999px 999px 0 0) and the circles it replaced are gone, and nothing
   should introduce a sixth shape.

   Only .shape-rect is safe on a text container. The other four are concave, so they eat
   into their own box and would clip copy; they are for imagery and decoration only. */
.shape-rect   { clip-path: url(#shape-rect); }
.shape-clover { clip-path: url(#shape-clover); }
.shape-tag    { clip-path: url(#shape-tag); }
.shape-arches { clip-path: url(#shape-arches); }
.shape-scurve { clip-path: url(#shape-scurve); }

/* ONE fixed radius on every card, not a percentage.
   The old `shape-rect` clip sized its corners as 7.79% of width by 9.77% of height, so the
   corner was an ellipse whose shape changed with the box: near-circular 28x25 on a service
   card, but squashed to 20x13 on a wide chip and stretched to 17x26 on a tall step. Cards that
   should have matched visibly didn't. A fixed px radius gives every card the same circular
   corner regardless of its proportions. Change --radius, not the individual rules. */
.panel, .chip, .card, .stat, .step, .step-row, .svc, .svc-row, .fact, .pf-card,
.marquee-fig {
  border-radius: var(--radius);
}
/* Small bordered label, a full-size radius would read as a pill at 30px tall. */
.hero-kicker { border-radius: 10px; }

/* ---------- quote band ----------
   The mark is built from two blocks, each a rectangle with a single corner rounded right
   away, offset diagonally so they interlock, the pair reads as an opening and a closing
   quotation mark. Percentage radii are correct here, unlike on the cards: the shape IS the
   proportion, so it should stretch with the block rather than hold a fixed corner. */
/* Square tile with contrasting grout, and the ONE place on the site that gets it. The quote
   band is already a pause rather than a working section, so it can carry a feature without
   competing with anything.

   Two repeating-linear-gradients, one per axis, over the paper ground: 48px tiles with a 2px
   grout line. No image, no extra request.

   The grout can be this visible because it is LIGHT. Where a line passes behind a glyph the
   local ground becomes Radome Tan, and ink on that is still 10.1:1 -- so the worst case is far
   above AA. A dark grout would have had to be nearly invisible to stay safe. */
.quote-band {
  background-color: var(--paper);
  background-image:
    repeating-linear-gradient(to right,  var(--tan) 0 2px, transparent 2px 48px),
    repeating-linear-gradient(to bottom, var(--tan) 0 2px, transparent 2px 48px);
  padding: clamp(64px, 9vw, 104px) 0;
}
.quote-inner { display: flex; align-items: center; gap: clamp(28px, 5vw, 64px); flex-wrap: wrap; }

/* --qm exists so the wall thickness can be derived from the mark's own size. border-width
   cannot take a percentage, so a fixed px would be right at one size and wrong at the other
   end of the clamp: at 126px a 32px wall would leave no hole at all. */
.quote-mark { --qm: clamp(126px, 15vw, 196px); position: relative; flex: none; width: var(--qm); aspect-ratio: 450 / 670; }
/* Each block is exactly half the width, so the two sit edge to edge down the centre rather
   than overlapping sideways, and two thirds the height, so they interlock over a third of
   their run, both taken off the reference. */
/* Open, not solid: a wall of .16 of the mark's width on each side leaves a hole .36 of the
   block, the same ratio as the donuts elsewhere. Each block is 1:2, exactly like the process
   half donuts, so the same treatment reads correctly here.
   box-sizing: border-box applies (these are real elements, not pseudo-elements), so the wall
   grows inward and the blocks keep meeting down the centre line. */
.quote-mark span { position: absolute; width: 50%; height: 67%; background: transparent; border: calc(var(--qm) * .16) solid var(--ink); }
/* The arc spans the block's FULL width: `100% / 50%` splits the horizontal and vertical radii
   so that, at these proportions, both resolve to the same number, a true circular quarter
   turn rather than a stretched ellipse or a merely rounded corner. */
.quote-mark span:first-child { top: 0; left: 0; border-radius: 100% 0 0 0 / 50% 0 0 0; }
.quote-mark span:last-child { bottom: 0; right: 0; border-radius: 0 0 100% 0 / 0 0 50% 0; }

.quote-body { margin: 0; flex: 1 1 340px; }
.quote-body p {
  font-weight: 480;
  font-size: clamp(26px, 3.6vw, 46px); line-height: 1.12; letter-spacing: .01em;
  color: var(--ink); text-wrap: balance; max-width: 18ch;
}
/* The emphasis is carried by a coral rule under the words, not by recolouring them, coral
   text on paper measures about 2.8:1, well under AA, so the words stay ink at 13.78:1. */
.quote-body em {
  font-style: normal;
  text-decoration: underline;
  text-decoration-color: var(--rose);
  text-decoration-thickness: .085em;
  text-underline-offset: .13em;
}
.quote-body cite {
  display: block; margin-top: clamp(18px, 2.4vw, 28px);
  font-style: normal; font-size: 12px; letter-spacing: .18em; text-transform: uppercase;
  color: var(--taupe);
}

/* ---------- section blade signs ----------
   A vertical marquee sign standing in the outer margin beside each major section, letters
   stacked one per box with an extruded, hatched side face.

   It lives in the gutter outside the 1200px container, so it only exists where that gutter is
   genuinely wide enough: 1200 + 2x(44 sign + 58 offset) = 1404. Below 1404 there is no room
   and it would sit on top of the content, so it is simply not rendered. This is a flourish for
   wide screens, not layout the page depends on, the section headings still carry the meaning,
   which is also why every sign is aria-hidden. */
.sign { display: none; }

@media (min-width: 1404px) {
  .has-sign { position: relative; }
  /* `.has-sign .sign`, not `.sign`, the sections carrying signs are also `.noise`, and
     `.noise > *` sets `position: relative; z-index: 1` at equal specificity later in this
     file. A bare `.sign` selector loses that cascade fight, the sign drops into normal flow,
     adds ~180px of height, and the marquee (same z-index, later in the DOM) paints over it.
     z-index 3 then puts the sign above the band's contents, which is how a blade sign hangs. */
  /* The rail is the absolutely positioned part; the sign inside it is sticky.

     Those two cannot be the same element: position: sticky and position: absolute are mutually
     exclusive, and an absolutely positioned sign cannot travel. So the rail takes the placement
     and spans the section from the sign's start to the section's foot, and the sign sticks
     inside it. A sticky element is bounded by its containing block, which is exactly what makes
     it ride down the section and then leave with it rather than following forever. */
  .has-sign .sign-rail {
    position: absolute; z-index: 3;
    top: var(--sign-top, 72px); bottom: clamp(24px, 4vw, 64px);
    left: max(14px, calc(50% - 600px - 82px));
    width: max-content; pointer-events: none;
  }
  .has-sign.container .sign-rail { left: -82px; }

  .has-sign .sign {
    display: flex; flex-direction: column;
    /* 116px, the same clearance the pinned numerals use.

       This was 28px, on the reasoning that signs only appear above 1404px where a rail nav has
       replaced the horizontal bar. There is no such rail nav: .topbar is position: fixed at
       every width, and its foot measures 96px at 1404 and at 1920 alike. A sign sticking at 28
       parks its top cells underneath the bar, which sits at z-index 60 against the rail's 3.
       It went unnoticed while the signs travelled only a section at a time; the moment one
       rides a long run it spends most of that run clipped. 96 plus a 20px gap. */
    position: sticky; top: 116px;

    /* The logo's slab, turned on its side. Same cream ground, same corner radius and the same
       cell rhythm as .brand-slab in the nav, so the blade sign reads as the wordmark rather
       than as a separate device that happens to be nearby.

       No outer border and no fixed width. The cells carry the frame now, and the sign
       shrink-wraps to one cell plus its padding, which is what keeps the two in proportion if
       --cell is ever changed.

       82px, not 58: the sign is 67px wide with the cell padding rather than 44, and the offset
       has to clear it and still leave the 15px gap the old number left. At the 1404 breakpoint
       the left edge still lands 20px inside the viewport. */
    --cell: 40px;
    gap: calc(var(--cell) * .03);
    padding: calc(var(--cell) * .38) calc(var(--cell) * .34);
    background: var(--sign-bg, var(--cream));
    border-radius: calc(var(--cell) * .15);
    /* The photo strip is full-bleed, so the WORK sign genuinely hangs over the renderings
       rather than sitting in a gutter. The shadow keeps it reading as in front of them. */
    box-shadow: 0 6px 20px rgba(20, 38, 29, .28);
  }
  /* The logo's bevelled cell, transposed. In the nav the cells run across, so each is .92 of a
     cell wide by a full cell tall; stacked, that swaps, and the rhythm going down matches the
     rhythm going across.

     The four-colour mitre only reads on SQUARE corners: the browser joins four borders at 45
     degrees on its own, but rounding the cell turns the mitre into four arcs and the frame
     flattens back to an outline. Hence radius 0 here while the slab around it is rounded. */
  .sign span {
    display: flex; align-items: center; justify-content: center;
    width: var(--cell); height: calc(var(--cell) * .92);
    border: calc(var(--cell) * .1) solid;
    border-top-color: var(--sand);
    border-right-color: var(--leaf);
    border-left-color: var(--forest);
    border-bottom-color: var(--pine);
    border-radius: 0;
    font-family: var(--font-display); font-weight: 700;
    font-size: calc(var(--cell) * .5); line-height: 1;
    color: var(--ink);
  }

  /* The extruded side: a hatched face skewed back, as on a projecting blade sign. */
  .sign::after {
    content: ""; position: absolute; top: 0; right: -12px; width: 12px; height: 100%;
    background: repeating-linear-gradient(180deg, var(--ink) 0 1px, transparent 1px 5px);
    border-right: 1.5px solid var(--ink);
    transform: skewY(-42deg); transform-origin: left top;
  }
}

/* ---------- shared ---------- */

.container { max-width: 1200px; margin: 0 auto; padding-left: clamp(20px, 4vw, 40px); padding-right: clamp(20px, 4vw, 40px); }
/* ---- section rhythm -------------------------------------------------------------
   ONE value governs both sides of every section boundary.

   Before this, a section was top-padded only (112px) and picked up a bottom pad (88px)
   just when another section followed. Two consequences, both visible: a section's own
   two edges never matched, so each block looked like it was drifting down the page
   rather than sitting still; and the hairline between two sections landed 88 below one
   and 112 above the next, so it rode high in every gap on the site.

   Now every section carries the same padding above and below. Each block reads as a
   unit, and the rule between any two of them lands exactly halfway.

   --pad-continuous is the smaller value, for the boundary between two sections that
   continue ONE subject instead of starting a new one. It is applied to both sides of
   that boundary, so the pair tightens evenly and the rule stays centred. */
:root {
  --pad-section: clamp(48px, 6vw, 76px);
  --pad-continuous: clamp(28px, 3.5vw, 44px);
}
.section-pad { padding-top: var(--pad-section); padding-bottom: var(--pad-section); }

/* Marks a section as continuing the one above it. Takes the smaller value on the shared
   boundary: its own top, and the previous section's bottom. */
.section-pad.pad-snug { padding-top: var(--pad-continuous); }
.section-pad:has(+ .section-pad.pad-snug) { padding-bottom: var(--pad-continuous); }

/* For a section that opens on a rule or a number rather than a heading. The standard
   padding is sized to give a heading room to breathe; with no heading there is nothing
   to breathe around and the same gap reads as a hole. */
.section-pad.pad-tight { padding-top: var(--pad-continuous); }

/* Air under an intro text block, matched to the air above it.

   A section opens with a `.prose` block and then shows the work. The gap ABOVE that text is
   the section padding, 76px. The gap BELOW it was whatever margin-top the media component
   underneath happened to carry, which across the site was 32, 36, 40 or 44px. So every intro
   sat high in its own space, pinned to the images below it, and no two sections agreed on by
   how much.

   One value, tracking the section's own top padding so the snug and tight variants stay
   proportional. Specificity (0,3,0) beats a component's own margin-top without !important. */
.section-pad { --pad-intro: var(--pad-section); }
.section-pad.pad-snug,
.section-pad.pad-tight { --pad-intro: var(--pad-continuous); }
.section-pad > :not(.sign-rail) + .prose { margin-top: var(--pad-intro); }
.section-pad > .prose + * { margin-top: var(--pad-intro); }

/* And the same on the way out. A section that shows the work and then signs off with a
   closing paragraph had 20 to 36px above that paragraph against a full section pad below it,
   so the afterword read as welded to the images rather than as its own remark. `:not(.sign-rail)`
   because the blade sign is decoration, not the thing the text is spaced from. */
.section-pad > :not(.sign-rail) + .prose:last-child { margin-top: var(--pad-outro); }

/* The closing block is measured against the section's BOTTOM padding, which is not always
   the same as its top: a section can open snug against the one above it and still close at
   full height. --pad-outro tracks padding-bottom the way --pad-intro tracks padding-top. */
.section-pad { --pad-outro: var(--pad-section); }
.section-pad:has(+ .section-pad.pad-snug) { --pad-outro: var(--pad-continuous); }

/* A section already pads its own top. A first child that also carries a margin-top adds a
   second gap on top of the first: 116px above the list on /blog, /faq and /services, 152px
   above the Related line on /3d-photorealistic-renderings. The section owns that edge. */
.section-pad > :first-child { margin-top: 0; }
.section-pad > .sign-rail + * { margin-top: 0; }

/* Page-level rules, the kind that divide the reference layouts: a hairline running the FULL
   container between one section and the next, not a short line over a text column.
   Only between two plain sections. Where the next section paints its own background, the
   colour edge is already the division and a rule on top of it reads as a mistake. */
.section-pad + .section-pad,
.section-pad + .cta-pad { border-top: 1px solid var(--rule); }
/* Plain sections are top-padded only, so their last line sits flush against whatever follows.
   Between two plain sections on the same cream ground that is invisible and the rhythm is
   right; against a section that paints a background it is not, and the colour edge lands
   welded to the last line.

   Keyed on the BAND, not on `.section-pad`: the section above is not always a `.section-pad`.
   On /accuracy-pass it is a bare `.container`, which is exactly how the Related line ended up
   stuck to the green band after the first version of this fix.

   `:is()` takes the specificity of its most specific argument, so the reset below is (0,2,0)
   against (0,1,0) here and wins regardless of order. */
:is(.collage-band, .services-band, .process-band, .quote-band, .section-band, .band-forest) {
  margin-top: var(--pad-section);
}
/* ...except after a padded section, which now supplies that clearance itself. Keeping both
   would stack a full section pad on top of a full band margin and open a gap twice the size
   of any other on the page. The margin above still covers the case this is keyed on: a band
   following a bare `.container`, which has no padding of its own. */
/* `.has-sign` counts as a padded section here. It is a wrapper around a run of sections so one
   blade sign can travel the whole run, and it ends on a padded section like any other; without
   it in this list the wrapper breaks the adjacency, the band takes its own margin back on top
   of that section's pad, and the page opens the doubled gap this rule exists to stop. */
:is(.section-pad, .has-sign) + :is(.collage-band, .services-band, .process-band, .quote-band, .section-band, .band-forest) {

  margin-top: 0;
}
/* A block's top margin exists to clear a heading above it. When the block is the FIRST thing
   inside a band there is no heading, so that margin lands on the band's own padding and the
   band opens deeper than it closes: on the About page the card grid's 40px sat on top of 92px
   of padding, against 92px clean below the last card.

   Safe on bands that DO lead with a heading: headings already carry no top margin, so this
   zeroes nothing, and the grid underneath keeps its 40px. Same cause as the ledger. */
:is(.collage-band, .services-band, .process-band, .quote-band, .section-band, .band-forest)
> .container:first-child > :first-child { margin-top: 0; }
/* ...except against another band or straight off a hero, where the colours are meant to butt. */
:is(.hero, .page-hero, .collage-band, .services-band, .process-band, .quote-band, .section-band, .band-forest)
+ :is(.collage-band, .services-band, .process-band, .quote-band, .section-band, .band-forest) {
  margin-top: 0;
}
/* Equal edges here as well. The extra 16px it used to carry below was invisible as long as
   nothing followed it, and read as a lean the moment anything did. */
.cta-pad { padding-top: clamp(64px, 9vw, 112px); padding-bottom: clamp(64px, 9vw, 112px); }
.center { text-align: center; }
.green { color: var(--forest); }
.mt-16 { margin-top: 16px; } .mt-20 { margin-top: 20px; } .mt-22 { margin-top: 22px; } .mt-24 { margin-top: 24px; }

.h-display { font-family: var(--font-display); font-weight: 600; font-size: clamp(27px, 3.5vw, 49px); line-height: 1.06; letter-spacing: 0; text-wrap: balance; }
.h-panel { font-family: var(--font-display); font-weight: 600; font-size: 30px; line-height: 1.06; letter-spacing: 0; }
.h-card { font-family: var(--font-display); font-weight: 600; font-size: 25px; line-height: 1.08; letter-spacing: 0; text-wrap: pretty; }
.body-copy { font-size: 16px; line-height: 1.5; color: var(--taupe); text-wrap: pretty; max-width: 68ch; }
.body-copy strong { font-weight: 480; color: var(--ink); }
.on-aubergine { color: #E9DCD0; }
.on-forest { color: #CBDCCE; max-width: 520px; }
.link-strong { font-weight: 480; }
.on-dark { color: var(--cream); }
.on-dark:hover { color: var(--cream); }

/* Moulded pill, after the recessed drawer pulls: a true stadium with a lit top lip and a
   shaded lower inside, so the shape reads as pressed out of the surface rather than drawn on
   it. 999px, not 32px -- the pull's ends are full half circles, and 32px on a 44px-tall button
   leaves flat sides that give the shape away.
   All three layers are INSET. An outer drop shadow would make it a floating chip; the pull is
   part of the drawer face, so the modelling has to happen inside the silhouette. */
.btn {
  display: inline-block; border-radius: 999px;
  font-size: 12px; letter-spacing: .12em; text-transform: uppercase; font-weight: 480;
  transition: box-shadow .25s var(--ease-out), background-color .25s var(--ease-out);
}
.btn:hover { text-decoration: none; }
/* The whole DRAWER, not just its pull: a soft rounded-rect face with a stadium recess carved
   into it, the label sitting down inside the recess.

   That needs two shapes. The button is the face; `::before` is the recess. It has to paint
   above the face but below the label, and an absolutely positioned pseudo-element normally
   paints above inline content -- so it takes z-index -1 and the button takes `isolation`,
   which makes the button its own stacking context and puts negative-z children after the
   face's background and before the text. Without the isolation, -1 would drop it behind the
   whole button and vanish.

   The recess gradient runs top-dark to bottom-light: the upper inside of a concave form faces
   away from the light and the lower inside catches it. Shadows alone only tint edges; the
   gradient is what says "hollow". */
/* Pills, from the reference: one flat colour, fully rounded, and a contrasting disc riding at
   the tail. No frame and no cast shadow here on purpose -- this is the reference shown
   straight, so its own flatness is the point. The framed version is parked in
   _build/experiments/poster-frame-button.css and is one paste from coming back.

   The disc is sized from the button's own height rather than from its text: top and bottom are
   pinned 5px in and `aspect-ratio: 1` derives the width, so it stays a true circle filling the
   pill at every size without a value per variant.

   One --dot per variant, never the same colour as the face. */
.btn {
  display: inline-block; position: relative;
  border: 0; border-radius: 999px;
  font-size: 12px; letter-spacing: .12em; text-transform: uppercase; font-weight: 480;
  --dot: var(--tan);
  transition: transform .24s var(--ease-out), background-color .24s var(--ease-out);
}
.btn::after {
  content: ""; position: absolute; z-index: 1;
  right: 5px; top: 5px; bottom: 5px; width: auto; aspect-ratio: 1;
  border-radius: 50%; background: var(--dot);
  transition: transform .24s var(--ease-out);
}
.btn:hover { text-decoration: none; transform: translateY(-2px); }
.btn:hover::after { transform: translateX(3px); }
.btn:active { transform: translateY(0); }

.btn-primary { --dot: var(--tan); background: var(--plum); color: #FFFFFF; padding: 13px 58px 13px 28px; }
.btn-primary:hover { background: var(--plum-dark); color: #FFFFFF; }
.btn-lg { padding: 16px 68px 16px 34px; font-size: 13px; }
/* The hairline variant from the reference: no fill at all, a thin outline, and the disc doing
   all the colour. */
.btn-outline { --dot: var(--plum); background: transparent; box-shadow: inset 0 0 0 1.5px var(--ink); color: var(--ink); padding: 13px 58px 13px 28px; font-size: 13px; }
.btn-outline:hover { background: var(--ink); color: var(--cream); }
/* Pressed: the frame turns over, so the lit edges and the shadowed ones swap sides. */
.btn:active {
  transform: translate(0, 0);
  border-top-color: var(--pine);
  border-right-color: var(--tan);
  border-left-color: var(--cream);
  border-bottom-color: var(--forest);
}
/* Pressed also swaps the whole colour combination: the face drops into the deep green and the
   disc goes coral. Every button presses into the SAME pair, whatever it looks like at rest, so
   the gesture reads as one thing across the site rather than as a per-button effect.

   Coral is on the disc and not on the copy on purpose. Coral type on this green measures about
   4.4:1, just under the threshold for text this size, and the disc has no such constraint. It
   is also the louder position, so the colour arrives more clearly there than it would as
   letters.

   .is-pressed rides along with :active because these buttons navigate. See app.js. */

/* The bar is aubergine and the primary is plum: two purples one shade apart, which read as
   a single smudge rather than as a button. Romaine is the only palette colour that carries
   a light green against that ground (9.0:1) and still takes ink copy (9.8:1), so the CTA
   becomes the one green thing up there and the eye lands on it first.
   Hover inverts into the deep green rather than lightening, so the press reads as the card
   turning over.
   NOT extended to the mobile menu CTA: that panel is dark green, so the same light green
   would put green on green there, which is the exact problem this rule exists to fix. The
   plum primary is already the contrasting colour against that ground. */
/* Moonlight, not Romaine. Romaine is in the frame, so a Romaine face swallowed the whole
   left side of it. The green stays in the frame and in the nav links either way. */
.btn-nav { --dot: var(--plum); background: var(--sand); color: var(--ink); }
/* The quiz is the one thing on the site a visitor can do rather than read, so it gets a button
   in the bar of its own. Same pill as Start a Project with a forest dot instead of plum: a
   pair, not a competitor. */
.btn-quiz { --dot: var(--forest); }
.btn-nav:hover { background: var(--sand); color: var(--ink); }

.lift { transition: transform .35s cubic-bezier(.2,.7,.2,1); }
.lift:hover { transform: translateY(-4px); }

.noise { position: relative; }
.noise::before {
  content: ""; position: absolute; inset: 0; pointer-events: none; opacity: .05; z-index: 0;
  background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20width%3D'180'%20height%3D'180'%3E%3Cfilter%20id%3D'n'%3E%3CfeTurbulence%20type%3D'fractalNoise'%20baseFrequency%3D'0.8'%20numOctaves%3D'2'%2F%3E%3C%2Ffilter%3E%3Crect%20width%3D'180'%20height%3D'180'%20filter%3D'url(%23n)'%2F%3E%3C%2Fsvg%3E");
}
.noise > * { position: relative; z-index: 1; }

/* One authored entrance per page: the title stack rises once, in sequence.
   Content is visible by default, the animation starts from the settled state,
   so a JS or font failure never leaves the page blank. */
@keyframes riseIn { from { opacity: 0; transform: translateY(22px); } }
.js-anim .page-hero .crumbs,
.js-anim .page-hero .page-kicker,
.js-anim .page-hero .page-title,
.js-anim .page-hero .page-lede,
.js-anim .page-hero .btn { animation: riseIn .8s var(--ease-out) both; }
.js-anim .page-hero .page-title { animation-delay: .06s; }
.js-anim .page-hero .page-lede { animation-delay: .14s; }
.js-anim .page-hero .btn { animation-delay: .22s; }

/* Section arrival is a quiet fade only, no travel, so it reads as the page
   settling rather than as an effect repeated on every band. */
.reveal { opacity: 0; }
.reveal.in { opacity: 1; transition: opacity .55s ease-out; }

/* ---------- top bar / nav ---------- */

/* Flex column capped at the visible viewport so the open mobile menu claims exactly the
   space the nav leaves it, rather than a hardcoded offset. */
.topbar { position: fixed; top: var(--page-edge); left: var(--page-edge); right: var(--page-edge); z-index: 60; display: flex; flex-direction: column; max-height: 100vh; max-height: 100svh; }

.nav { flex: none; display: flex; align-items: center; justify-content: space-between; gap: 12px; height: 84px; padding: 0 clamp(16px, 3vw, 40px); }
.nav.frost, .supports-scrolltl .nav { }
.nav { transition: background-color .3s ease; }
.nav.frost { background-color: var(--nav-frost); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); }

.brand { display: flex; align-items: center; color: var(--cream); }
.brand:hover { color: var(--cream); text-decoration: none; }

/* Modular stencil wordmark: one solid slab, each letter in its own hairline-outlined cell.
   Proportions read off the reference (slab 533x156, six cells 78x87 inset 36/33):
   cells all but touching, slab padding ~.35 of a cell, and every
   corner -- cell and slab -- on a small uniform radius rather than a domed top. Everything derives from --cell, so one value resizes the whole mark.
   Value is inverted from the reference -- light slab, dark letters -- because this mark sits over
   the hero photo in the nav and over aubergine in the footer, and a dark slab dies on both. */
.brand-slab {
  --cell: 21px;
  display: flex; gap: calc(var(--cell) * .03);
  padding: calc(var(--cell) * .34) calc(var(--cell) * .38);
  background: var(--cream);
  border-radius: calc(var(--cell) * .15);
}
.brand-slab span {
  display: flex; align-items: center; justify-content: center;
  /* .72, not the reference's .90: Anton is condensed, so cells cut for a normal-width face
     left the letters filling only .48 of them against the reference's .64. */
  /* Bevelled cells, from the POPCLUB poster: a name set one letter per framed panel, each
     panel a mitred frame rather than a flat outline.

     The mitre is free. Give one element four borders in four colours and the browser joins
     them at 45 degrees on its own, which is exactly the four trapezoids the poster prints.
     It only reads that way on SQUARE corners though, so the cell radius goes to 0 -- rounding
     it turns the mitre into four arcs and the frame flattens back out.

     Colours run light to dark clockwise from the top, so the frame is lit from the upper
     right, the same corner the buttons and the stat tiles throw their shadows from. The
     poster's own colours are arbitrary pop; a consistent light model is what stops this
     reading as four random stripes.

     The cell had to get wider and the letter smaller. A .1 border on each side eats .2 of the
     cell, so at the old .72 width the face came out 10px across at nav scale and the capitals
     collided with the frame. .92 and .5 restore the same air the flat version had. */
  width: calc(var(--cell) * .92); height: var(--cell);
  border: calc(var(--cell) * .1) solid;
  border-top-color: var(--sand);
  border-right-color: var(--leaf);
  border-left-color: var(--forest);
  border-bottom-color: var(--pine);
  border-radius: 0;
  color: var(--ink);
  /* .82 puts the cap height at ~.6 of the cell, matching the reference's .69 fill without the
     line box outgrowing the cell interior at line-height 1. */
  font-family: var(--font-display);
  font-size: calc(var(--cell) * .5); font-weight: 700; line-height: 1;
}
.brand-slab.small { --cell: 19px; }
@media (max-width: 480px) { .brand-slab { --cell: 17px; } }

.nav-links { display: flex; align-items: center; gap: 4px; font-size: 16px; }
.nav-links a { display: flex; align-items: center; gap: 7px; color: var(--cream); padding: 9px 20px; border-radius: 0; }
.nav-links a:hover { background: rgba(249,240,222,.14); color: var(--cream); text-decoration: none; }
/* Romaine, so the green in the CTA is echoed by the links instead of sitting there alone.
   Applied at all times rather than only on the frosted bar: the frost arrives two different
   ways (a scroll-driven animation in Chromium, a .frost class everywhere else), and a
   colour keyed to one of them would be wrong in the other. Hover lifts to cream. */
.nav-links a { color: var(--sand); }
.nav-links a:hover { color: var(--cream); }
.nav-links .caret { font-size: 9px; opacity: .75; }

/* ---------- hero ---------- */

/* One source of truth for the hero's pinned height. The pin is stuck for exactly this
   distance, and the scrub animation has to run over exactly the same distance, if the
   two ever drift the hero unpins mid-fade. svh, not vh: on mobile 100vh is the *large*
   viewport (URL bar hidden), so vh would clip the hero behind the URL bar on load and
   would also be the longer of the two, ending the pin before the scrub finished. */
:root { --hero-pin: calc(100vh - 24px); }
@supports (height: 100svh) { :root { --hero-pin: calc(100svh - 24px); } }

.hero { position: relative; height: calc(var(--hero-pin) * 2); }
.hero-pin { position: sticky; top: var(--page-edge); height: var(--hero-pin); overflow: hidden; }
.hero-bg { position: absolute; inset: 0; transform: scale(1.1); will-change: transform; }
.hero-bg picture { display: block; width: 100%; height: 100%; }
.hero-bg img { width: 100%; height: 100%; object-fit: cover; object-position: 59% 50%; display: block; }
/* Must track the <picture>'s portrait `media` exactly: that file is already cropped
   around the 59% focal point, so re-offsetting it would push the subject out of frame. */
@media (max-aspect-ratio: 2/3) { .hero-bg img { object-position: 50% 50%; } }
/* Film grain over the photo. Same turbulence texture the bands already use, so it costs no
   new asset, at a heavier opacity than the .05 those get.
   It hangs off the SCRIM, not the pin: the scrim sits below .hero-layout in source order, so
   the grain lands on the image and never crosses the headline. Grain over type would read as
   even more filmic but it would eat contrast on the one piece of copy that has to work over a
   photograph. */
.hero-scrim::after {
  content: ""; position: absolute; inset: 0; pointer-events: none; opacity: .12;
  background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20width%3D'180'%20height%3D'180'%3E%3Cfilter%20id%3D'n'%3E%3CfeTurbulence%20type%3D'fractalNoise'%20baseFrequency%3D'0.8'%20numOctaves%3D'2'%2F%3E%3C%2Ffilter%3E%3Crect%20width%3D'180'%20height%3D'180'%20filter%3D'url(%23n)'%2F%3E%3C%2Fsvg%3E");
}
.hero-scrim {
  position: absolute; inset: 0; pointer-events: none;
  background:
    radial-gradient(120% 90% at 50% 45%, transparent 45%, rgba(38,24,15,.28) 78%, rgba(38,24,15,.55) 100%),
    linear-gradient(180deg, rgba(38,24,15,.52) 0%, rgba(38,24,15,.30) 45%, rgba(38,24,15,.62) 100%);
}
/* Clears the fixed nav (84px). Was 130px when the announce bar sat above it. The nav is the
   same height at every viewport, so this no longer needs a per-height override. */
.hero-layout { position: absolute; inset: 0; display: flex; flex-direction: column; padding-top: 90px; }
.hero-content { position: relative; z-index: 1; flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 0 40px; will-change: transform, opacity; }
.hero-kicker { font-weight: 600; font-size: 12px; letter-spacing: .18em; text-transform: uppercase; color: var(--hero-ink); border: 1px solid rgba(249,240,222,.55); padding: 7px 16px; }
/* Sizes here have been rescaled three times, each measured on this same string so the line
   breaks survived: Anton to Quicksand at 0.83, Quicksand to Fraunces at 1.183, Fraunces to
   Comfortaa at 0.7807. Line-height came back down with the last one -- the serif needed room
   for its descenders, a rounded geometric does not. */
.hero-content h1 { font-family: var(--font-display); font-weight: 600; font-size: clamp(35px, min(5.6vw, 7.6vh), 78px); line-height: 1.04; letter-spacing: 0; color: var(--cream); max-width: min(980px, 100%); margin: 28px 0 0; text-wrap: balance; }
.hero-sub { font-size: 18px; line-height: 1.5; font-weight: 420; color: var(--hero-ink); max-width: min(640px, 100%); margin: 16px 0 0; text-wrap: pretty; }
.hero-ctas { display: flex; flex-direction: column; align-items: center; gap: 16px; margin-top: 24px; }
.hero-secondary { color: var(--cream); font-size: 16px; }
.hero-secondary:hover { color: var(--cream); }
  .hero-sub { font-size: 17px; margin-top: 16px; }
  .hero-ctas { margin-top: 24px; gap: 14px; }
}
@media (max-height: 620px) {
  .hero-kicker { font-size: 11px; padding: 5px 12px; }
  .hero-content h1 { font-size: clamp(28px, 6.5vw, 92px); margin-top: 14px; }
  .hero-sub { font-size: 15px; margin-top: 12px; }
  .hero-ctas { margin-top: 18px; gap: 10px; }
}

/* scroll-driven versions (Chromium); JS fallback drives the same motion elsewhere */
/* Pan, not zoom. The old keyframe drove scale 1 -> 1.35, which reads as a modern Ken Burns
   push; a slow vertical drift over a fixed frame is closer to a slide projector. The 1.1 scale
   is HELD, not animated: it exists only to give the drift room before the frame edge shows.

   The drift is a PERCENTAGE, not pixels. scale(1.1) yields slack of 5% of the hero height at
   top and bottom, so a fixed 34px is fine at 900px tall and overruns the frame on a 600px
   window. 3% is always inside 5%, at any viewport height. */
@keyframes heroPan { to { transform: translateY(3%) scale(1.1); } }
@keyframes heroFade { to { opacity: 0; transform: translateY(-80px); } }
@keyframes navFrost { to { background-color: var(--nav-frost); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); } }
@supports (animation-timeline: scroll(root)) {
  /* Same --hero-pin as the pin height above: the scrub must finish at the exact scroll
     offset the pin releases, or the hero slides away still half-faded. */
  .hero-bg { animation: heroPan linear both; animation-timeline: scroll(root); animation-range: 0px var(--hero-pin); }
  .hero-content { animation: heroFade linear both; animation-timeline: scroll(root); animation-range: 0px var(--hero-pin); }
  .nav { animation: navFrost linear both; animation-timeline: scroll(root); animation-range: 0px 120px; transition: none; }
}

/* ---------- collage band ---------- */

/* Full-bleed: the strip runs to both window edges so it reads as a continuous band with
   more of it always waiting off-screen. */
.collage-band { --band-pad: clamp(64px, 7.5vw, 96px); background: var(--forest); padding: var(--band-pad) 0; }

/* A real scroll container, not a transform track. Native scrolling means touch can swipe
   it and a keyboard can arrow through it for free; app.js only nudges scrollLeft on hover.
   The scrollbar is hidden because the motion itself is the affordance. */
.marquee { overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain; scrollbar-width: none; -ms-overflow-style: none; }
.marquee::-webkit-scrollbar { display: none; }
.marquee:focus-visible { outline: 2px solid var(--tan); outline-offset: -4px; }
/* Plates, still moving. The strip keeps its scroll behaviour exactly as it was, only the
   figures change: each is a flat square throwing a hard offset shape instead of a padded
   panel with a rounded picture inside it.

   The OFFSET is cream, not ink. This band is Rousseau, and ink on Rousseau measures about
   2:1, so a dark offset came out as a smudge rather than a shape. A light one reads as a
   printed backing plate. It falls down and to the LEFT, the same corner the buttons and the
   stat tiles are lit from.

   The KEYLINE is the opposite: ink, drawn as the first shadow layer so it paints on top of
   the cream offset. It has the contrast the offset could not, because it sits against that
   cream on two sides rather than against the green, and it is what separates a photograph
   from its own backing plate instead of letting the two bleed together.

   Two things the offset forced, and both are load-bearing:

   The gap went from a 1.5px seam to ~34px. At a hairline seam the shadow lands on the next
   figure instead of on the band and the whole strip fuses into one dark mass. Widening it is
   safe: app.js recomputes the loop distance from live offsetLeft on every frame, so the two
   halves of the track stay in sync at any gap. The figure WIDTH still must not change, that
   one is measured against the image sources.

   The track carries bottom padding it did not need before. `.marquee` is overflow-y: hidden,
   so a shadow hanging below the figures would be sheared off flat; the padding puts it inside
   the track's own box. 20 against an offset of 16.

   No knobs here, this strip is not clickable and a pull would promise a door that does not
   open. */
.marquee-track { display: flex; gap: clamp(24px, 2.6vw, 34px); width: max-content; padding: 0 clamp(20px, 4vw, 40px) 20px; }
/* Sources are 800x450, and a square crop uses only their 450px height, so a tile's effective
   resolution is 450/width. The 400px cap holds that at ~1.13x; wider goes sub-1x on a retina
   screen, the wrong trade on a page selling photoreal renderings.
   Raising this cap means re-exporting first. NOT from `Portfolio Preview Images`, that folder
   is itself 800x450 and is where the current files came from. The real originals are 1920x1080
   and 3840x2160, in the per-project client folders alongside it. */
.marquee-fig { flex: none; margin: 0; width: clamp(240px, 30vw, 400px); aspect-ratio: 1; overflow: hidden; border-radius: 0; background: var(--pine); box-shadow: 0 0 0 1.5px var(--ink), -16px 16px 0 var(--cream); }
.marquee-fig img { width: 100%; height: 100%; object-fit: cover; display: block; border-radius: 0; }
@media (max-width: 700px) {
  .marquee-track { gap: 20px; padding-bottom: 14px; }
  .marquee-fig { box-shadow: 0 0 0 1.5px var(--ink), -10px 10px 0 var(--cream); }
}

/* Onward links sitting on a dark band. These carry real navigation, the homepage's only
   route into the portfolio runs through the first one, so they are sized to be read, not
   treated as a caption. */
.band-caption { margin-top: clamp(28px, 3.5vw, 40px); text-align: center; font-size: 15px; line-height: 1.6; color: #FBA8DB; text-wrap: pretty; }
.band-caption a { color: var(--cream); font-weight: 480; text-decoration: underline; text-underline-offset: 4px; text-decoration-thickness: 2px; text-decoration-color: var(--underline-on-dark); }
.band-caption a:hover { color: #FBA8DB; text-decoration-color: currentColor; }
.band-caption.cream { color: var(--taupe); }
.band-caption.cream a { color: var(--forest); text-decoration-color: rgba(23,88,68,.4); }
.band-caption.cream a:hover { color: var(--plum); }

/* ---------- the route into the portfolio ----------

   This was one sentence with three underlined phrases inside it, which made the homepage's
   only link to the work look like a footnote. It is a row of framed panels now: the same
   poster frame the buttons carry, so it costs no new frame CSS at all -- the chips are just
   .btn .btn-outline and inherit the mitre, the cast shadow, the lift on hover and the
   turn-over on press.

   Three gains beyond the look. The targets go from 14px of underlined text to full 46px
   panels. The labels shed the word "work", which was in all three of them and in the heading
   above. And the count moves out of the sentence into a mono label, where it reads as the
   index line it always was.

   The comb between the two is the footer hem again, at caption scale: three segments on a
   2, 4 and 7px pitch, the same fine-to-coarse run. Nothing under 2px. */
/* The gap above equals the band's bottom padding, which is the space below. The label used to
   sit hard against the marquee with 96px under the chips. */
.work-index { margin-top: var(--band-pad); display: flex; flex-direction: column; align-items: center; gap: 14px; }
.work-index-label { margin: 0; font-family: var(--font-mono); font-size: 11px; letter-spacing: .22em; text-transform: uppercase; color: var(--sand); }
.work-index-tick {
  display: block; width: 84px; height: 10px;
  background:
    repeating-linear-gradient(to bottom, var(--sand) 0 2px, transparent 2px 4px) 0 0 / 26px 100% no-repeat,
    repeating-linear-gradient(to bottom, var(--sand) 0 3px, transparent 3px 6px) 29px 0 / 26px 100% no-repeat,
    repeating-linear-gradient(to bottom, var(--sand) 0 5px, transparent 5px 10px) 58px 0 / 26px 100% no-repeat;
}
.work-chips { display: flex; flex-wrap: wrap; justify-content: center; gap: 16px; margin-top: 4px; }
/* These sit on the Rousseau band, where the shared .btn-outline was doing them no favours: a
   transparent face with an ink hairline measures about 2:1 on that green, and its ink LABEL
   measures worse, so the three chips were close to invisible.

   Each gets a filled face and its own pair, the way the reference sets out a row of pills in
   different combinations rather than three of the same. All three faces clear 5:1 on the band
   and all three labels clear 9:1 on their own face:
     Radome Tan 5.31   Romaine 5.13   Moonlight 7.23
   Dots are picked to contrast the face they sit on, never to match it. */
.work-chips .btn { box-shadow: none; }
.work-chips .btn:nth-child(1) { background: var(--tan);   color: var(--ink); --dot: var(--plum); }
.work-chips .btn:nth-child(2) { background: var(--sand);  color: var(--ink); --dot: var(--plum); }
.work-chips .btn:nth-child(3) { background: var(--cream); color: var(--ink); --dot: var(--leaf); }
.work-chips .btn:hover { background: var(--cream); color: var(--ink); }
.work-chips .btn:nth-child(3):hover { background: var(--sand); }
/* Small print under the homepage's primary CTA, on forest. */
.cta-fineprint { margin-top: 22px; font-size: 14px; line-height: 1.6; color: #CBDCCE; text-wrap: pretty; }
.cta-fineprint a { color: var(--cream); font-weight: 480; text-decoration: underline; text-underline-offset: 3px; text-decoration-color: rgba(249,240,222,.45); }
.cta-fineprint a:hover { color: var(--tan); }

/* ---------- judgment / proof ---------- */

.split { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr)); gap: clamp(32px, 5vw, 56px); align-items: start; }
.split.tight { gap: 12px; align-items: stretch; }
@media (min-width: 760px) {
  .split:not(.tight) > * + * {
    border-left: 1px solid var(--rule);
    padding-left: clamp(32px, 5vw, 56px);
  }
}

.chip-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr)); gap: 12px; }
.chip { background: var(--sand); padding: 32px; }
.chip p { font-size: 16px; line-height: 1.5; text-wrap: pretty; }

.panel { padding: 56px; }
.panel.aubergine { background: var(--aubergine); color: var(--cream); }
.panel.sand { background: var(--sand); }
.panel.forest { background: var(--forest); color: var(--cream); }
.panel.sand.trust { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 28px; }
.trust-copy { max-width: 400px; }

/* A large circle. This is the one shape on the site that is NOT from the five-silhouette
   vocabulary, see the shape section in README.md.
   border-radius rather than a clip-path so the circle survives on a bordered or focusable
   variant later; aspect-ratio must be 1 or "50%" describes an ellipse, not a circle.
   object-position is biased upward: the source is 5:6, so a square crop discards 20% of its
   height, and a centred crop would take half of that off the top of her head. */
.portrait-frame { position: relative; width: min(320px, 80%); aspect-ratio: 1; border-radius: 50%; overflow: hidden; flex: none; }
.portrait-frame img { width: 100%; height: 100%; object-fit: cover; object-position: 50% 25%; display: block; }
.portrait-frame .avatar-initials { display: none; }
.portrait-frame.monogram { background: var(--forest); }
.portrait-frame.monogram .avatar-initials { display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; color: var(--cream); font-family: var(--font-display); font-weight: 700; font-size: 40px; letter-spacing: .08em; }

.stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr)); gap: 12px; margin-top: 12px; }
.stat { padding: 32px; text-align: center; }
/* Fluid rather than a fixed 60px, so a long figure like "100%" never overflows a narrow tile. */
.stat-num { font-family: var(--font-display); font-weight: 600; font-size: clamp(34px, 3.8vw, 52px); line-height: 1.05; }
.stat p { font-size: 14px; margin-top: 8px; text-wrap: pretty; }
/* Chunky donut above each figure. 56 across with a 19px wall leaves an 18px hole -- .32,
   the same ratio as every other donut on the site.

   Scoped to the four About-page variants rather than to `.stat`, because the homepage tiles
   paint their own backgrounds and a Salem donut would vanish on the Salem tile. Those four
   class names currently match no rule at all, so the tiles render transparent -- the donuts
   are coloured by position rather than by class name so they do not imply otherwise.
   box-sizing is explicit: the universal selector does not reach pseudo-elements, so without
   it `width` would mean the hole and the ring would come out 94px across. */
:is(.stat.forest, .stat.leaf, .stat.aubergine, .stat.sand)::before {
  content: ""; display: block; box-sizing: border-box;
  width: 56px; height: 56px; margin: 0 auto 18px;
  border-radius: 50%; border: 19px solid var(--leaf);
}
:is(.stat.forest, .stat.leaf, .stat.aubergine, .stat.sand):nth-child(2)::before { border-color: var(--aubergine); }
:is(.stat.forest, .stat.leaf, .stat.aubergine, .stat.sand):nth-child(3)::before { border-color: var(--forest); }
:is(.stat.forest, .stat.leaf, .stat.aubergine, .stat.sand):nth-child(4)::before { border-color: var(--pine); }
/* Captions sit at full cream: a 14px line under a 60px figure already reads as
   secondary from scale alone, and dropping the opacity only cost contrast. */
/* Poster palette. The accent tile used to be coral -- light enough for ink copy. Salem is a
   mid green, so that tile flipped to cream; the tan tile stays dark-on-light. */
.stat.pine  { background: var(--pine);  color: var(--cream); } .stat.pine p  { color: #BDD3C6; }
/* Caption at full cream, not the dimmed tint the darker tiles use: Salem is the lightest
   dark ground on the site, and #CFE3D6 measured 3.93:1 there -- under AA for 14px. Cream
   is 4.66. */
.stat.coral { background: var(--coral); color: var(--cream); } .stat.coral p { color: var(--cream); }
.stat.blush { background: var(--blush); color: var(--pine);  } .stat.blush p { color: #1E4634; }
.stat.paper { background: var(--paper); color: var(--pine);  } .stat.paper p { color: var(--taupe); }

/* ---------- services band ---------- */

/* No margin-top of its own: this band used to be the only one that had solved its own
   clearance, which is why it was the one join that never looked welded. The shared band rule
   handles it now, and keeping this would leave it 112 where every other band is 96. */
.services-band { background: var(--sand); padding: clamp(64px, 9vw, 112px) 0; }
/* Explicit column counts, not auto-fit. auto-fit picks whatever number happens to fit, which
   lands on THREE at common widths, and an odd row leaves the last door's pull stranded on the
   outer edge of the case with nothing to pair with. Even counts only, so every pull meets
   another across a seam. (auto-fit also missed four by 4.5px here: 4x280 + gaps = 1124.5
   against 1120 of container.) */
.svc-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1.5px; margin-top: 40px; }
@media (min-width: 1080px) { .svc-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
@media (max-width: 640px)  { .svc-grid { grid-template-columns: minmax(0, 1fr); } }
/* 48px left and right, not 32. The pull sits at 14px and is 16px across, so at 32px padding
   the copy began 2px from it and read as crowded. 48 gives an 18px gutter.
   SYMMETRIC on purpose: the pull side alternates card to card, so padding only on the pull
   side would start each card's text at a different inset and the three columns would no
   longer line up with each other. */
.svc { background: var(--cream); padding: 32px 48px; }
/* Chunky donut per card. 76px across with a 26px stroke leaves a 24px hole -- .32 of the
   outer diameter, which is the proportion that makes a ring read as a solid object. Thinner
   strokes and bigger holes just look like circles with a line round them.
   Radome Tan is deliberately not in this rotation: it measures 1.35:1 on the Moonlight card
   and effectively disappears. */
.svc::before { content: ""; display: block; box-sizing: border-box; width: 76px; height: 76px; border-radius: 50%; border: 26px solid var(--leaf); margin-bottom: 26px; }
.svc:nth-child(2)::before { border-color: var(--aubergine); }
.svc:nth-child(3)::before { border-color: var(--forest); }
.svc:nth-child(4)::before { border-color: var(--plum); }
.svc h3 { font-weight: 700; font-size: 21px; line-height: 1.35; }
.svc > p { font-size: 16px; line-height: 1.5; color: var(--taupe); margin-top: 12px; }
.svc-links { display: flex; flex-direction: column; align-items: flex-start; gap: 10px; margin-top: 24px; font-size: 14px; font-weight: 420; }

/* ---------- process ---------- */

/* Narrow columns are what make these read as half moons rather than long lozenges. The cap is
   always a semicircle of radius height/2, so how "moon-like" a card looks is entirely the
   width-to-height ratio: at 300px columns the cards came out 365x227 and the curve was only
   31% of the width, a rectangle with a rounded end. At 210px they are 214x299, ratio 0.72,
   and the curve is 70% of the width. Widen this and the moon flattens back into a lozenge. */
/* Full-bleed blush field, so the green half moons sit on a colour rather than on the page. */
/* Two greens: the deepest one as the field, Romaine as the donut wall. The field has to be
   the DARK one -- the copy and the numerals sit in the hole, so the hole is what they read
   against, and pink numerals need a dark ground (pink on Romaine is about 1.5:1). */
.process-band { position: relative; background: var(--pine); padding: clamp(64px, 9vw, 112px) 0 clamp(56px, 7vw, 88px); }
/* The Salem column that used to rise behind these shapes is gone. It worked while the half
   circles were solid -- it peeked out from behind them. Now they are open donuts, so it
   showed straight THROUGH the holes and the copy in cards one and two landed on green while
   three to five sat on pink: two different contrasts for the same text. The field is one
   flat colour instead. */.process { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(210px, 100%), 1fr)); gap: 12px; }
.step { background: transparent; border: 12px solid var(--sand); color: var(--cream); padding: 20px; }
/* Sideways D: flat left edge, semicircular right end -- a half circle turned on its side,
   not an arch. gap 0 sets them touching, so each D's curve meets the next D's flat edge at a
   single tangent point on the centre line, leaving crescents of blush above and below it.

   999px rather than a measured radius: when radii overflow the box the browser scales every
   corner by ONE factor, so the end stays a true semicircle -- radius height/2 here, since the
   right edge is the binding constraint -- at any column width. A hand-picked px value would
   go elliptical as the columns flex.

   Right padding is much larger than left: the curve eats into the top-right and bottom-right
   of the text column, so the copy has to sit clear of it.

   Below 700px the grid drops to one or two columns; the cards go wide and short, the radius
   is capped by height, and the D flattens into a lozenge with a rounded end. There they keep
   the site's normal corner and their 12px gap instead. */
@media (min-width: 700px) {
  .process { gap: 0; }
  .step {
    border-radius: 0 999px 999px 0;
    /* A true half circle needs height to be exactly 2x width. Only at that ratio does the
       clamp resolve the radius to the full width, leaving no straight run between the two
       quarter-arcs -- at the D's 224x275 there were 87px of flat edge down the middle, 39%
       of the card, which is what still read as a rectangle with a rounded end. */
    aspect-ratio: 1 / 2;

    /* Half a thick donut. The fill becomes a heavy border and the interior is left open, so
       the pink field shows through as the hole. border-left is dropped because a donut cut in
       half has no wall across the cut -- keeping it would draw a closed capsule instead.
       aspect-ratio resolves against the border box here, so the OUTER edge stays an exact
       semicircle; and since the border is even on top/right/bottom, the hole comes out
       176x352 -- 1:2 as well, a second semicircle concentric with the first. */
    background: transparent;
    /* 90px -- a wall .40 of the card width, near double the 48 (.21) it started at.
       This is the ceiling while the copy lives inside the ring: a semicircular hole pinches
       hardest exactly where the first and last lines sit, so thickness trades directly against
       type size. Swept border x type against the circle equation -- 92 only clears if the body
       drops to 12px, and 96 fails outright. Chunkier than this means the copy leaves the hole. */
    border: 90px solid var(--sand);
    border-left: 0;

    /* The copy now sits in the hole, on pink rather than on pine, so every colour in here
       flips from light-on-dark to dark-on-light. */
    color: var(--cream);
    display: flex; flex-direction: column; justify-content: center;
    padding: 12px 12px 12px 6px;
  }
  /* `.process .step h3`, not `.step h3` -- the base rules for the numeral, heading and copy
     are defined LATER in this file at equal specificity, so an equal-specificity override
     here loses the cascade no matter that it sits in a media query. First pass at this left
     the numerals painting pink on pink, i.e. invisible. The descendant selector wins on
     specificity instead of position, and still only applies above 700px. */
  /* Type is what buys the wall its thickness -- the hole is only 134 wide now. */
  .process .step h3 { color: var(--cream); font-size: 15px; margin-top: 12px; }
  .process .step p { color: #BDD3C6; font-size: 13px; margin-top: 8px; }
  .process .step-num { color: var(--pink); font-size: 37px; }
}
.step-num { font-family: var(--font-display); font-weight: 600; font-size: 48px; line-height: 1; color: var(--pink); }
.step h3 { font-weight: 700; font-size: 18px; line-height: 1.3; margin-top: 18px; color: var(--cream); }
.step p { font-size: 14px; line-height: 1.5; color: #BDD3C6; margin-top: 10px; text-wrap: pretty; }
/* The pull quote and link line now sit on blush, not cream. */
.process-band .pull-quote { color: var(--cream); }
.process-band .related { border-top-color: rgba(192, 210, 173, .35); color: #BDD3C6; }
.process-band .related a { color: var(--sand); }
.pull-quote { font-size: 21px; line-height: 1.35; font-weight: 420; text-align: center; max-width: 640px; margin: 56px auto 0; text-wrap: pretty; }

/* ---------- dual CTA ---------- */

/* Whole donuts in the corner of each CTA panel -- a stroke about a third of the diameter, so
   the ring reads as a solid object rather than an outlined circle.

   These are FLOW items, not absolutely positioned. Two earlier attempts placed them by offset
   and distance-tested them clear of the copy, and both passed at 1440 and then collided once
   the panels reflowed -- the first ran under the body text, the second under the fineprint and
   the owners button. A flow item at the end of a flex column cannot overlap anything at any
   width, which is the only version of this that is actually safe.
   Neither panel is .noise, so ::after is free here. */
.cta-main::after, .cta-alt::after {
  content: ""; display: block; box-sizing: border-box; align-self: flex-end; flex: none;
  border-radius: 50%; background: transparent;
}
/* Stays the Fibra pink -- the one place it carries an area rather than text. It sits on the
   Rousseau panel, where it is decoration and answers to no contrast minimum. */
.cta-main::after { width: 96px; height: 96px; border: 33px solid var(--pink); }
.cta-alt::after  { width: 124px; height: 124px; border: 42px solid var(--forest); }
.cta-main { display: flex; flex-direction: column; align-items: flex-start; gap: 24px; }
.cta-main .btn { margin-top: 8px; }
.cta-alt { display: flex; flex-direction: column; align-items: flex-start; justify-content: space-between; gap: 24px; }
.cta-alt-q { font-size: 21px; line-height: 1.35; font-weight: 420; max-width: 340px; text-wrap: pretty; }

/* ---------- footer ---------- */

/* Aubergine, shared with the dark panels, dark stat tiles and the nav frost. The two
   hard-coded tints below are mixed from it, re-derive them if this colour changes. */
footer { background: var(--aubergine); color: var(--cream); }
footer a { color: var(--cream); }
footer a:hover { color: var(--tan); }
.foot-inner { padding-top: clamp(48px, 6vw, 72px); padding-bottom: 40px; }
.foot-cols { display: flex; flex-wrap: wrap; gap: 32px; }
.foot-brand { flex: 1.2 1 200px; }
.foot-contact { display: flex; flex-direction: column; align-items: flex-start; gap: 8px; margin-top: 18px; font-size: 14px; }
.foot-contact a { color: rgba(249,240,222,.8); }
.foot-col { flex: 1 1 150px; display: flex; flex-direction: column; align-items: flex-start; gap: 11px; font-size: 14px; }
.foot-head { font-weight: 600; font-size: 12px; letter-spacing: .16em; text-transform: uppercase; color: var(--pink); margin-bottom: 5px; }
/* min-width:0, a flex item defaults to min-width:auto, and the email input's
   intrinsic size (default `size` attribute + padding) held this column at 306px,
   overflowing the footer on 320px phones. */
.foot-news { flex: 1.5 1 280px; min-width: 0; }
.news-row { display: flex; margin-top: 14px; }
/* #8D7982, aubergine mixed 43% into cream. 3.58:1 against the footer, clearing the
   3:1 minimum for a non-text control boundary. */
.news-row input { flex: 1; min-width: 0; background: transparent; border: 1px solid #8D7982; border-right: 0; border-radius: 0; padding: 12px 20px; font-size: 16px; color: var(--cream); font-family: inherit; }
.news-row input::placeholder { color: rgba(249,240,222,.68); }
.news-row input:focus { border-color: var(--cream); outline: none; }
/* Tan, not plum: plum was the button colour back when the footer was wine, and it would
   disappear into a purple footer. Tan reads 7.93:1 against aubergine and is already the
   footer.s accent (heads, link hover). The label is ink at 9.05:1. */
.news-row button { background: var(--tan); color: var(--ink); border: 1px solid var(--ink); border-radius: 0; padding: 12px 24px; font-size: 13px; letter-spacing: .1em; text-transform: uppercase; font-weight: 480; font-family: inherit; cursor: pointer; }
.news-row button:hover { background: #E3B183; }
/* The shared footer focus ring is tan, which would sit invisibly beside a tan button. */
.news-row button:focus-visible { outline-color: var(--cream); }
#newsMsg { font-size: 13px; margin: 10px 4px 0; }
#newsMsg.ok { color: #C0D2AD; }
#newsMsg.err { color: var(--pink); }
/* #604454, aubergine lifted 18% toward cream; a deliberately barely-there 1.69:1 rule. */
.foot-legal { margin-top: 56px; padding-top: 24px; border-top: 1px solid #604454; font-size: 12px; letter-spacing: .08em; color: var(--tan); display: flex; justify-content: space-between; gap: 20px; flex-wrap: wrap; }

/* ---------- responsive / motion ---------- */

@media (max-width: 900px) {
  .nav-links { display: none; }
  .panel { padding: 40px 32px; }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after { animation-duration: .01s !important; animation-iteration-count: 1 !important; transition-duration: 0s !important; }
  .reveal { opacity: 1; transform: none; }
}

/* =======================================================================
   Interior pages
   ======================================================================= */

.sr-only { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
.skip-link { position: absolute; left: -9999px; top: 0; z-index: 100; background: var(--forest); color: var(--cream); padding: 12px 20px; }
.skip-link:focus { left: 8px; top: 8px; }

/* The current-page marker now lives with the retro nav cells further down: a translucent
   cream wash is invisible against a cell that is already filled. */

/* page hero, forest band the transparent nav sits over */
/* 150px, down from 190px: the top offset cleared the announce bar as well as the nav. */
/* Padding-top carries the 84px transparent nav on top of the real gap, so the number is not
   the space you see. Measured at 150px: 76px of visual air above the kicker against 57px below
   the lede, which sat the block low in the band. 134px brings the top gap to 60px and reads
   level. The 700px override below shifts by the same 16px so its +22px relationship holds. */
/* Equal air above the title and below the lede, at every width.

   The two were set independently, a flat 134 on top (156 under 700px) against a clamp on the
   bottom, and the mismatch was plain: 139 of green above the title against 56 below it.

   The obvious repair, adding the topbar's height to the top padding so the bar's own space does
   not count, is wrong here. The bar has NO background of its own: on this page the forest band
   runs up behind the wordmark to the top of the frame, so every pixel of that top padding is
   green the reader can see. It has to be measured like any other padding.

   Equal padding on both sides does NOT look equal here, which is the trap. The bar has no
   background, so the forest runs up behind the wordmark to the top of the frame, and the
   wordmark is then an object sitting inside the band's top padding. What the eye reads as the
   space above the title is the gap from the foot of that wordmark down to it, not from the edge
   of the band. Padded 84 and 84, that measured 25 above against 84 below.

   So the top carries the same air as the bottom PLUS the height the wordmark occupies. The
   wordmark's slab ends 72px down the viewport and the band starts at --page-edge plus the
   frame's 1.5px border, so it eats 58.5px of the top padding.

   The slab drops a cell size under 480px, which takes 4px off its foot, so --logo-drop follows
   it there. Without that the top runs 3px long on a phone: not visible, but the whole point of
   this rule is that the two gaps are equal, and a figure that is only nearly right invites the
   next person to trust it somewhere it is not. */
.page-hero {
  --hero-air: clamp(56px, 6vw, 88px);
  --logo-drop: 58.5px;
  background: var(--forest); color: var(--cream);
  padding: calc(var(--hero-air) + var(--logo-drop)) 0 var(--hero-air);
}
@media (max-width: 480px) { .page-hero { --logo-drop: 54.5px; } }
.page-hero .container { max-width: 1000px; }
/* The hero eyebrow. It was being written into the markup with no rule behind it, so it rendered
   at plain body size hard against the title. Set in the same mono the other eyebrows use, in
   Romaine because this hero is forest and Romaine is the palette's light green against it. */
.page-kicker {
  margin: 0 0 clamp(14px, 1.6vw, 22px);
  font-family: var(--font-mono); font-size: 11px; letter-spacing: .24em;
  text-transform: uppercase; color: var(--sand);
}
.page-title { font-family: var(--font-display); font-weight: 600; font-size: clamp(30px, 4.2vw, 55px); line-height: 1.06; letter-spacing: 0; max-width: 900px; text-wrap: balance; }
.page-lede { font-size: 18px; line-height: 1.5; font-weight: 420; color: #CBDCCE; max-width: 660px; margin-top: 24px; text-wrap: pretty; }
.page-hero .btn { margin-top: 32px; }

/* Breadcrumb, navigation, and the only thing allowed to sit above a page title. */
.crumbs { display: flex; align-items: center; gap: 10px; font-size: 14px; margin-bottom: 24px; color: rgba(249,240,222,.75); }
.crumbs a { color: var(--tan); }
.crumbs a:hover { color: var(--cream); }
.crumbs svg { width: 14px; height: 14px; flex: none; opacity: .8; }
.crumbs .here { color: rgba(249,240,222,.75); }

.prose { max-width: 720px; }
.prose p { font-size: 17px; line-height: 1.6; color: var(--taupe); margin-top: 20px; text-wrap: pretty; }
.prose p:first-child { margin-top: 0; }
.prose p strong { font-weight: 480; color: var(--ink); }
/* Editorial section break: a hairline above the heading, the way every spec sheet and
   catalogue page in the references separates one block from the next. Costs no markup and no
   new content, and it is what makes a run of prose read as a document rather than as a scroll.
   The rule sits ABOVE with the heading tucked under it, so the line belongs to the section it
   opens rather than closing the one before. */
.prose h2 { font-family: var(--font-display); font-weight: 600; font-size: clamp(22px, 2.5vw, 32px); line-height: 1.1; letter-spacing: 0; color: var(--forest); margin-top: 56px; padding-top: 24px; border-top: 1px solid var(--rule); text-wrap: balance; }
/* The first heading opens the page, so there is nothing above it to divide from. */
.prose h2:first-child { margin-top: 0; padding-top: 0; border-top: 0; }
.prose h3 { font-size: 21px; line-height: 1.3; margin-top: 32px; }
.prose ul { margin: 20px 0 0; padding-left: 0; list-style: none; }
.prose li { position: relative; font-size: 17px; line-height: 1.6; color: var(--taupe); padding-left: 26px; margin-top: 12px; text-wrap: pretty; }
.prose li::before { content: ""; position: absolute; box-sizing: border-box; left: 3px; top: 9px; width: 11px; height: 11px; background: transparent; border: 3.5px solid var(--leaf); border-radius: 50%; }
.prose li strong { font-weight: 480; color: var(--ink); }
.prose a { font-weight: 480; text-decoration: underline; text-decoration-color: var(--underline); text-decoration-thickness: 2px; text-underline-offset: 3px; }
.prose a:hover { text-decoration-color: currentColor; }

.section-band { background: var(--sand); padding: clamp(56px, 8vw, 96px) 0; }
/* The tiled moment on the services page, and only there. `.section-band` appears on eleven
   pages, so this is scoped by page rather than added to the component.

   The ground changes from Romaine to paper for it. That is the whole reason the homepage
   version failed: tan grout on a mid-tone green is a high-contrast grid over a large area and
   it vibrates. On a pale ground the same lines are quiet, which is why the quote band works.
   Text over the grout stays safe on both counts: forest 5.34, taupe 5.57. */
body[data-page="services"] .section-band {
  background-color: var(--paper);
  background-image:
    repeating-linear-gradient(to right,  var(--tan) 0 2px, transparent 2px 48px),
    repeating-linear-gradient(to bottom, var(--tan) 0 2px, transparent 2px 48px);
}
.band-forest { background: var(--forest); color: var(--cream); padding: clamp(56px, 8vw, 96px) 0; }
.band-forest .h-display, .band-forest h2 { color: var(--cream); }

/* ---------- the closing band, as a printed panel ----------

   Every page ends on this band and all nineteen carry the identical shape: display heading,
   lede, one button. So the retro goes on in CSS and the markup stays untouched.

   Two devices, both already in the site's vocabulary rather than invented for this:

   A bracket frame. Not a full box, which would read as a card and fight the cabinetry
   elsewhere; the sides are open, so it reads as crop marks around a piece of artwork. Drawn
   as two pseudo-elements, each a short strip with three of its four borders: the top one
   gives a rule with stubs turning down at both ends, the bottom one the same inverted. One
   element cannot do all four corners, and two is fewer than four.

   A comb tick above the heading, the footer hem shrunk to a caption mark. Three segments of
   2, 4 and 6px pitch, so the same fine-to-coarse graduation happens across 84px that happens
   across the page down there. No pitch under 2px, for the usual reason.

   The container gets its own padding so the brackets sit off the copy rather than on it. */
.band-forest > .container { position: relative; padding-top: 40px; padding-bottom: 40px; }
.band-forest .btn { margin-top: 34px; }
.band-forest > .container::before,
.band-forest > .container::after {
  content: ""; position: absolute; left: 0; right: 0; height: 20px;
  border: 1.5px solid rgba(249, 240, 222, .45);
  pointer-events: none;
}
.band-forest > .container::before { top: 0; border-bottom: 0; }
.band-forest > .container::after { bottom: 0; border-top: 0; }

.band-forest .h-display::before {
  content: ""; display: block; width: 84px; height: 14px; margin-bottom: 22px;
  background:
    repeating-linear-gradient(to bottom, var(--sand) 0 2px, transparent 2px 4px) 0 0 / 26px 100% no-repeat,
    repeating-linear-gradient(to bottom, var(--sand) 0 4px, transparent 4px 8px) 29px 0 / 26px 100% no-repeat,
    repeating-linear-gradient(to bottom, var(--sand) 0 7px, transparent 7px 14px) 58px 0 / 26px 100% no-repeat;
}
@media (max-width: 700px) {
  .band-forest > .container { padding-top: 30px; padding-bottom: 30px; }
  .band-forest > .container::before, .band-forest > .container::after { height: 14px; }
}
.band-forest p { color: #CBDCCE; }

/* numbered steps

   CURRENTLY UNUSED. Every numbered sequence moved to .pin-steps, the sticky-index treatment.
   Kept because it is a finished component and the donut echoes the service cards and the CTA;
   delete this block if it is still unreferenced next time anyone is in here. */
.steps { display: grid; gap: 12px; margin-top: 40px; }
.step-row { display: grid; grid-template-columns: 104px 1fr; gap: 24px; align-items: start; background: var(--sand); padding: 28px 32px; }
/* The numeral sits in a chunky donut. 104 across with a 32px wall leaves a 40px hole -- a .38
   ratio, matching the service-card and CTA donuts.
   Ring and digits are set separately rather than sharing `currentColor`: as one colour the
   digits sat on the hole at 3.28:1, which only passes because 30px counts as large text. The
   digits take Blackberry against the Romaine hole instead, and the ring keeps Salem.
   box-sizing is explicit: `* { box-sizing: border-box }` covers this element, but stating it
   here keeps the 104/32/40 arithmetic true if the rule is ever moved to a pseudo-element,
   where the universal selector would not reach it.
   The digits drop 50px -> 30px to clear the hole, but the element as a whole is far bigger
   than the bare numeral was. */
.step-row .n {
  box-sizing: border-box;
  display: flex; align-items: center; justify-content: center;
  width: 104px; height: 104px; border-radius: 50%;
  border: 32px solid var(--leaf); background: transparent;
  font-family: var(--font-display); font-weight: 700; font-size: 25px; line-height: 1;
  /* --plum, not the deep aubergine: it is the palette's lighter purple already (the primary
     button colour), so this reuses a token rather than inventing a shade. 7.47:1 on the
     Romaine hole, down from Blackberry's 9.04 but still well clear. */
  color: var(--plum);
}
.step-row h2, .step-row h3 { font-weight: 700; font-size: 20px; line-height: 1.3; }
.step-row p { font-size: 16px; line-height: 1.55; color: var(--taupe); margin-top: 8px; text-wrap: pretty; }
.band-forest .step-row { background: rgba(249,240,222,.07); }
.band-forest .step-row h2, .band-forest .step-row h3 { color: var(--cream); }
.band-forest .step-row p { color: #CBDCCE; }
.band-forest .step-row .n { border-color: var(--tan); color: var(--cream); }

/* generic card grid */
.card-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr)); gap: 12px; margin-top: 40px; }
.card-grid.two { grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr)); }
.card { background: var(--sand); padding: 32px; }
.card h3 { font-weight: 700; font-size: 21px; line-height: 1.35; }
.card p { font-size: 16px; line-height: 1.55; color: var(--taupe); margin-top: 12px; text-wrap: pretty; }
.card .card-links { display: flex; flex-direction: column; align-items: flex-start; gap: 10px; margin-top: 22px; font-size: 14px; font-weight: 420; }
.card.on-cream { background: var(--cream); }

/* service index rows */
.svc-list { display: grid; gap: 12px; margin-top: 40px; }
/* Indexed like a menu board: the letter is a fixed column so the names line up. */
/* Donuts in place of the letter indices. The letters were an index nothing referred to, and a
   ring ties these rows to the service cards on the home page, which carry the same shape.

   `.idx` is aria-hidden and its "(A)" was decorative, so font-size: 0 is enough to swallow the
   glyph; nothing in the accessibility tree changes and the markup stays as it is.

   36 across with a 12px wall leaves a 12px hole, a third of the diameter, which is the ratio
   every other donut on the site uses. box-sizing is explicit: the arithmetic only holds while
   `width` means the OUTER edge, and the universal selector would not reach this if the ring
   ever moved to a pseudo-element.

   Colour rotates on four and restarts per list, the same rotation the service cards use.
   Radome Tan is deliberately absent: it measures 1.35:1 on the Romaine row and vanishes. */
.svc-row .idx {
  box-sizing: border-box; display: block; margin: 0 auto; font-size: 0;
  width: 36px; height: 36px; border-radius: 50%;
  border: 12px solid var(--leaf);
}
.svc-list .svc-row:nth-child(4n+2) .idx { border-color: var(--aubergine-soft); }
.svc-list .svc-row:nth-child(4n+3) .idx { border-color: var(--forest); }
.svc-list .svc-row:nth-child(4n+4) .idx { border-color: var(--plum); }
.svc-row { display: grid; grid-template-columns: 42px minmax(0, 1fr) auto; gap: 24px; align-items: center; background: var(--sand); padding: 28px 32px; transition: transform .35s cubic-bezier(.2,.7,.2,1), background-color .35s ease; }
.svc-row:hover { transform: translateY(-3px); background: var(--sand-deep); text-decoration: none; }
.svc-row h2, .svc-row h3 { font-weight: 700; font-size: 22px; line-height: 1.3; }
.svc-row p { font-size: 16px; line-height: 1.5; color: var(--taupe); margin-top: 8px; text-wrap: pretty; }
.svc-row .go { display: block; width: 22px; height: 22px; color: var(--leaf); transition: transform .3s var(--ease-out); }
.svc-row:hover .go { transform: translateX(5px); }

/* portfolio grid */
.pf-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(320px, 100%), 1fr)); gap: 12px; margin-top: 40px; }

/* Case study index tiles.

   Deliberately NOT .pf-card: that component is a lightbox trigger wired to app.js, with a door
   opening transform and a data-shots contract. These go to a page instead, so they are plain
   anchors with the same extruded frame the rest of the site uses. */
.cs-index { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr));
  gap: clamp(26px, 3.2vw, 40px); margin-top: 36px; padding-bottom: 20px; }
.cs-tile { display: block; text-decoration: none; color: inherit; background: var(--paper);
  border: 2px solid var(--ink); box-shadow: -14px 14px 0 var(--ink);
  transition: transform .22s cubic-bezier(.3,.7,.3,1), box-shadow .22s cubic-bezier(.3,.7,.3,1); }
.cs-tile:hover { transform: translate(-4px, 4px); box-shadow: -7px 7px 0 var(--ink); }
.cs-tile:focus-visible { outline: none; box-shadow: -14px 14px 0 var(--ink), inset 0 0 0 3px var(--plum); }
.cs-tile-thumb { aspect-ratio: 4 / 3; overflow: hidden; background: var(--sand); }
.cs-tile-thumb img { width: 100%; height: 100%; object-fit: cover; display: block;
  transition: transform .6s cubic-bezier(.2,.7,.2,1); }
.cs-tile:hover .cs-tile-thumb img { transform: scale(1.05); }
.cs-tile-meta { padding: 14px 16px 17px; }
.cs-tile-name { margin: 0; font-family: var(--font-display); font-weight: 600; font-size: 19px;
  line-height: 1.15; color: var(--forest); }
.cs-tile-scope { display: flex; align-items: center; gap: 8px; margin: 9px 0 0;
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .14em; text-transform: uppercase; color: var(--taupe); }
@media (prefers-reduced-motion: reduce) {
  .cs-tile, .cs-tile-thumb img { transition: none; }
  .cs-tile:hover { transform: none; }
  .cs-tile:hover .cs-tile-thumb img { transform: none; }
}
/* flex column, not block. `.pf-card` is a <button> on the portfolio, and a button VERTICALLY
   CENTRES its content box by default. Cards in a row all stretch to the tallest, so any card
   whose title fits on one line had its whole stack pushed down by half the difference -- the
   thumbnails sat 12px lower than the card next to them with a two-line title, which threw the
   pulls out of line across the seam. A flex column stacks from the top and the extra height
   falls below the meta, where nothing is positioned against it. */
.pf-card { position: relative; display: flex; flex-direction: column; align-items: stretch; border: 0; padding: 0; background: var(--sand); cursor: pointer; text-align: left; font: inherit; color: inherit; overflow: hidden; }
/* Clicking a door opens it. The hinge is the edge OPPOSITE the pull, and the pull side
   alternates, so odd doors hinge on the left and even doors on the right.
   `perspective()` lives in the transform rather than on the grid: a perspective on the parent
   would share one vanishing point across every door at once, so doors far from the centre
   would shear instead of swing. Per-element, each door turns about its own hinge.
   z-index lifts the opening door above its neighbours so its leading edge is not clipped by
   the next door in the run. */
.pf-card { transition: transform .26s cubic-bezier(.3, .7, .3, 1); transform-origin: left center; }
.pf-card:nth-child(even) { transform-origin: right center; }
.pf-card.opening { z-index: 5; transform: perspective(1100px) rotateY(-24deg); }
.pf-card:nth-child(even).opening { transform: perspective(1100px) rotateY(24deg); }
@media (prefers-reduced-motion: reduce) { .pf-card { transition: none; } .pf-card.opening { transform: none; } }
.pf-thumb { position: relative; aspect-ratio: 4 / 3; overflow: hidden; background: var(--sand); }
.pf-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; transition: transform .6s cubic-bezier(.2,.7,.2,1); }
.pf-card:hover .pf-thumb img { transform: scale(1.05); }
/* The card footer was one flex line: title, scope, view count. The two labels are nowrap and
   the title is not, so a long scope label ("Exterior amenity" against "Interiors only") ate
   the row and squeezed the title to 68px of a 284px line, wrapping it to three lines and
   clipping the count. The title now takes its own row and the labels sit under it. */
.pf-meta { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 6px 16px; padding: 18px 20px 20px; }
.pf-meta h2, .pf-meta h3 { flex: 1 0 100%; font-weight: 700; font-size: 19px; line-height: 1.25; text-wrap: balance; }
.pf-meta span { font-size: 12px; letter-spacing: .12em; text-transform: uppercase; color: var(--taupe); white-space: nowrap; }

/* ---------- cabinetry ---------- */

/* Extended off the portfolio: a grid of boxes reads as a case of doors -- square corners, a
   hairline ink seam between them, and a small handle on each.

   The seam is an outset box-shadow on each box, meeting its neighbour's across a 1.5px gap.
   Two other approaches both fail here:
     - borders on the boxes double wherever two meet, and these grids are all auto-fit, so the
       column count changes with the viewport and the doubling moves around;
     - painting the GRID ink and letting it show through the gap fills EMPTY CELLS too. The
       services band is three cards, so at two columns the vacant fourth cell became a solid
       ink block. A shadow only draws where a box actually is.
   Each shadow reaches 1.5px outward and the gap is 1.5px, so neighbours meet exactly and the
   outer edge of the run gets the same line as a frame.

   Boxes stay opaque regardless: a translucent door would show the section behind it rather
   than reading as a door. That is why `.stat` takes a default background below, and why the
   forest step rows are flattened from a translucent tint to the colour it resolves to. */
.pf-grid, .svc-grid, .card-grid, .stats, .steps, .svc-list { gap: 1.5px; }
/* Image galleries break out of the shared seam and take their own edge, with the ground
   showing between. The GAP is the only thing deciding which reading you get: at 1.5px each
   cell's 1.5px shadow meets its neighbour's and the run is cabinetry; open it up and the
   shadows separate into individual plates, which is the menu reference.
   7px leaves ~4px of ground between two outlines -- enough to read as a gutter, tight enough
   that the grid still holds together as one object.
   Kept to the galleries: the stat tiles, service cards and step rows still read as doors in a
   case, and they should. */
.pf-grid { gap: 7px; }
/* Two across, not four. At four the renders were roughly 290px wide, which is a contact
   sheet: enough to tell one project from another and not enough to look at any of them.
   At two they run near 600px, which is the size the work was drawn to be seen at.

   The open animation is a side effect worth noting: .pf-card:nth-child(even) hinges from
   the right, and with an even column count that finally lines up, so cards in the left
   column swing open leftward and the right column rightward. At four it alternated
   against the grid. */
@media (min-width: 1000px) {
  body:is([data-page="portfolio"], [data-page="commercialportfolio"], [data-page="designer-portfolio"]) .pf-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}
@media (min-width: 640px) and (max-width: 999px) {
  body:is([data-page="portfolio"], [data-page="commercialportfolio"], [data-page="designer-portfolio"]) .pf-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

/* Service-page galleries opt out of the hard-edged plate treatment and take the site''s normal
   rounded box instead, matching the WORK strip: no outline, no shared seam, a 12px gutter and
   the standard corner. Only the two portfolio pages keep the ruled plates, because there the
   cell IS a door that opens and the edge is doing work. */
/* Service-page galleries as extruded plates, matching the work strip and the stat tiles.

   They used to be rounded, outline-less and captioned with a lettered chip -- (A), (B), (C)
   -- which was the only rounded thing left on the page and the only index on the site that
   counted something nobody refers to. Both are gone. What is left is the treatment every
   other block already has: square, an ink keyline, and a hard offset down and to the left.

   Gap and padding move together with the offset, for the same reason they do on the stat
   tiles: at the old 12px the shadow lands on the next plate instead of on paper, and without
   the padding whatever follows the grid sits on top of the shadow.

   Still scoped away from the two portfolio pages. There the cell is a door that opens, and it
   keeps its ruled-plate treatment and its knob. */
body:not([data-page="portfolio"]):not([data-page="commercialportfolio"]):not([data-page="designer-portfolio"]) .pf-grid { gap: 24px; }
/* Service-page renderings as mounted plates.

   Third pass. The first squared them off and threw a hard shadow, which read as a photograph
   with a block behind it. The second wrapped them in the poster frame, which read as a
   photograph with a border on it. Neither was the problem: a bare rendering is a bare
   rendering however it is edged.

   So this stops framing the picture and starts MOUNTING it, in the same vocabulary as the mail
   window and the CRT on how-it-works, which is the most period thing on the site: a paper
   shell, the image recessed behind an ink keyline, and a mono nameplate with a power light
   along the bottom carrying the project name.

   The name is the real gain. These galleries showed three unlabelled renderings; every one of
   them is a named project and the alt text already held the name, so the plate says it out
   loud instead of leaving it to a screen reader.

   The offset comes back here because a mount is an object and objects sit on things. It is
   the paper shell that carries it, not the photograph. */
body:not([data-page="portfolio"]):not([data-page="commercialportfolio"]):not([data-page="designer-portfolio"]) .pf-grid {
  gap: 32px; padding-bottom: 20px;
}
body:not([data-page="portfolio"]):not([data-page="commercialportfolio"]):not([data-page="designer-portfolio"]) .pf-card {
  background: var(--paper); border-radius: 0;
  border: 2px solid var(--ink);
  padding: 12px 12px 0;
  box-shadow: -16px 16px 0 var(--ink);
}
body:not([data-page="portfolio"]):not([data-page="commercialportfolio"]):not([data-page="designer-portfolio"]) .pf-thumb {
  border-radius: 0; box-shadow: inset 0 0 0 2px var(--ink);
}
.pf-plate {
  display: flex; align-items: center; gap: 9px;
  padding: 11px 2px;
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .18em; text-transform: uppercase;
  color: var(--taupe);
}
.pf-led { flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--leaf); box-shadow: 0 0 0 1.5px var(--ink), 0 0 6px var(--leaf); }
@media (max-width: 700px) {
  body:not([data-page="portfolio"]):not([data-page="commercialportfolio"]):not([data-page="designer-portfolio"]) .pf-grid {
    gap: 22px; padding-bottom: 12px;
  }
  body:not([data-page="portfolio"]):not([data-page="commercialportfolio"]):not([data-page="designer-portfolio"]) .pf-card {
    box-shadow: -10px 10px 0 var(--ink);
  }
}
.pf-card, .svc, .card, .stat, .step-row, .svc-row { box-shadow: 0 0 0 1.5px var(--ink); }

/* ---------- extruded stat tiles ----------

   The poster look: solid blocks each throwing a hard-edged shadow, no blur, so the shadow
   reads as a second flat shape rather than as light. These four tiles were already the right
   subject, a row of flat colour blocks with three greens and one tan odd-one-out, so they get
   it and nothing else does.

   Down and to the LEFT, not the poster's down-and-right. The buttons already cast that way,
   which puts the site's light up and to the right, and two light sources on one page reads as
   a mistake rather than as a reference.

   The gap has to grow with the offset or the shadow lands on the next tile instead of on
   paper, which is what turns the row from four blocks into one dark smear: 32 against an
   offset of 20 leaves 12px of paper showing. Below 700px the grid is a single column and the
   offset has to clear the container gutter instead, so both numbers come down.

   The 1.5px seam from the cabinetry rule above is kept as the first layer. Losing it would
   drop the hairline that every other block on the site carries. */
.stats { gap: 32px; }
.stats .stat { box-shadow: 0 0 0 1.5px var(--ink), -20px 20px 0 var(--ink); }

/* The same extrusion on the card and chip grids. These were welded together on 1.5px seams,
   reading as one cabinet bank; extruded they read as four separate blocks, which is what the
   content actually is -- four alternatives, not four drawers of one thing.

   Both selectors are two classes deep on purpose. The cabinetry rules further up set the
   seam gap and the hairline at one class, and a same-specificity override would then depend
   on source order; keyed to the grid, these win outright and stay put if the file is ever
   reordered. A `.card` outside a `.card-grid` keeps the flat cabinetry treatment. */
.card-grid, .chip-grid { gap: 32px; }
.card-grid .card, .chip-grid .chip { box-shadow: 0 0 0 1.5px var(--ink), -20px 20px 0 var(--ink); }

/* A cast shadow is painted outside the box and takes no space in layout, so whatever follows
   an extruded grid sits 20px too close to it -- the "See how the pricing model works" line was
   landing 4px off the shadow rather than 24px off the cards.

   PADDING, not margin. Margin was the obvious reach and it did nothing: the grid and the line
   after it are adjacent siblings in normal flow, so a 20px bottom margin collapsed against the
   line's own 24px top margin and the larger one simply won. Padding does not collapse, so the
   following margin now measures from the padded edge and the two add up.

   Set on the grids rather than on the elements that follow, so it holds wherever these grids
   are used and does not depend on which selector wins the cascade. */
.stats, .card-grid, .chip-grid { padding-bottom: 20px; }

@media (max-width: 700px) {
  .stats, .card-grid, .chip-grid { gap: 22px; padding-bottom: 12px; }
  .stats .stat,
  .card-grid .card, .chip-grid .chip { box-shadow: 0 0 0 1.5px var(--ink), -12px 12px 0 var(--ink); }
}
/* On the forest band the seam has to be light, not ink. */
.band-forest .step-row { box-shadow: 0 0 0 1.5px rgba(249, 240, 222, .32); }
.pf-card, .pf-thumb, .svc, .card, .stat, .step-row, .svc-row { border-radius: 0; }
.pf-card, .pf-thumb { background: var(--cream); }
/* `.stat` had no background at all, so on a seamed grid the whole block would go ink. Cream
   matches the page ground, so the tiles that already set their own colour are unaffected and
   the ones that do not look exactly as they did. */
.stat { background: var(--cream); }
/* #276351 is forest with cream at 7% over it, resolved. It had to stop being translucent or
   the seam colour would bleed up through the row. */
.band-forest .step-row { background: #276351; }

/* Doors do not float. `.lift` translates the box 4px on hover, which would tear a gap in the
   run of seams and show the ink behind; inside a case the door shifts colour instead. */
:is(.svc-grid, .card-grid, .stats, .steps, .svc-list) .lift:hover { transform: none; }
:is(.svc-grid, .card-grid, .stats, .steps) .lift:hover { filter: brightness(.97); }
.svc-row:hover { transform: none; }

/* Round finger pull, the kind recessed into a sliding cabinet door.

   The sides ALTERNATE. Sliding doors come in pairs sharing a track, and the two pulls flank
   the seam where those doors meet, which is what the reference cabinet shows. So odd doors
   take a pull on the right and even doors on the left: the pulls gather in pairs at every
   other seam, instead of marching down the run all at the same edge.

   14px keeps the pull inside the 32px padding gutter, so it never reaches the copy.
   currentColor lets one rule serve a cream door and a pine one. */
.svc, .stat { position: relative; }
/* Only `.svc` and `.stat` take a pull. `.card` and `.step-row` are wide boxes whose copy runs
   the full width, so the knob ends up against a line of text instead of sitting in clear
   gutter the way it does on the narrower tiles. */
.svc::after, .stat::after {
  content: ""; position: absolute; z-index: 2; right: 14px; top: 50%; transform: translateY(-50%);
  width: 16px; height: 16px; border-radius: 50%;
  /* A rim, like the portfolio pull's. The ring is currentColor at full strength and the fill
     a lighter mix of it, so the knob reads as a recessed pull on ANY door colour without
     needing a rule per surface. The plain background above is the fallback for browsers
     without color-mix; there the knob is simply solid. */
  background: currentColor;
  background: color-mix(in srgb, currentColor 38%, transparent);
  box-shadow: 0 0 0 1.5px currentColor;
}
.svc:nth-child(even)::after,
.stat:nth-child(even)::after { right: auto; left: 14px; }
/* No pulls once the grid is a single column.

   The whole device depends on the alternation above: sliding doors come in pairs sharing a
   track, and the two pulls flank the seam where those doors meet. Stack the doors one per row
   and there is no seam left for a pair to flank, so the knobs just zigzag left, right, left
   down the page and read as stray dots rather than as hardware.

   640 is where .svc-grid drops to one column. The stat tiles hold two a little longer, so they
   lose the pull slightly before they strictly have to, which is the cheaper of the two errors:
   a flourish missing early is invisible, a flourish that has stopped meaning anything is not. */
@media (max-width: 640px) {
  .svc::after, .stat::after { content: none; }
}
/* Over a photograph the handle needs its own separation, so this one keeps the ink-plus-ring
   treatment rather than currentColor.

   Knobs ONLY on the portfolio doors. Every other `.pf-grid` on the site is a service-page
   gallery of plain <figure> elements with cursor:default, so nothing opens there and a pull
   would advertise a door that is not one. Same reasoning that kept them off the WORK strip. */
/* The pulls, from the blue cabinet: a round disc set into the door face rather than a dot
   drawn on it. Three layers do that and all three are load-bearing:
     0 0 0 1.5px cream      separates the disc from whatever photograph is behind it
     inset 0 3px 4px dark   the near lip catching the shadow, which is what makes it a hole
     inset 0 -2px 3px light the far lip bouncing light back
   Light from above, consistent with the rest of the page.

   Inside the image at mid height, alternating right/left so consecutive cards PAIR across the
   vertical seam between them -- the arrangement asked for after trying both alternatives. It
   was briefly moved onto the horizontal seam between rows, on .pf-card with overflow visible;
   that is reverted, so .pf-card keeps clipping its own overflow and nothing hangs below the
   last row.

   With four columns the pairs fall on the 1|2 and 3|4 seams and the middle seam carries none.
   That is the alternation working, not a gap: nth-child alternates per CARD, and an even
   column count turns that into pairs. */
body:is([data-page="portfolio"], [data-page="commercialportfolio"], [data-page="designer-portfolio"]) .pf-thumb::after {
  content: ""; position: absolute; z-index: 2; right: 16px; top: 50%; transform: translateY(-50%);
  width: 22px; height: 22px; border-radius: 50%;
  /* The page's own cream. The card is a door that swings open on click and this is its knob,
     so it reads as hardware sitting on the image rather than as a coloured marker.

     The ring is a thin ink keyline rather than the cream halo it used to carry: a cream knob
     needs a dark edge to hold its shape against a pale render, and a cream halo around a cream
     fill would have left it dissolving into the lighter images entirely.

     The two insets are retuned for a light fill. At the old strengths, shading built for a dark
     plum knob turned the top of a cream one into a smudge. */
  background: var(--cream);
  box-shadow:
    0 0 0 1.5px rgba(20, 38, 29, .38),
    inset 0 3px 4px rgba(20, 38, 29, .26),
    inset 0 -2px 3px rgba(255, 255, 255, .85);
}
body:is([data-page="portfolio"], [data-page="commercialportfolio"], [data-page="designer-portfolio"]) .pf-card:nth-child(even) .pf-thumb::after { right: auto; left: 16px; }
/* ...and no knob at all once the grid is one card wide.

   Same reason the service tiles lose theirs: the alternation above is the whole point of the
   device, two knobs meeting either side of the seam between a pair of doors. One card per row
   leaves no seam, so they simply alternate left, right, left down the page and read as pink
   dots on the renderings rather than as hardware.

   639.98 is the exact complement of the rule above that pins these two pages to two columns
   from 640 up. Not a width derived from the base auto-fill track, which never applies here. */
@media (max-width: 639.98px) {
  body:is([data-page="portfolio"], [data-page="commercialportfolio"], [data-page="designer-portfolio"]) .pf-thumb::after { content: none; }
}

/* Portfolio only: an editorial inset. The seam hairlines stay and keep forming the grid, but
   the image no longer fills its cell edge to edge; it sits inside as a rounded panel with the
   cream showing around it, which is what turns a run of tiles into a ruled page. */
body:is([data-page="portfolio"], [data-page="commercialportfolio"], [data-page="designer-portfolio"]) .pf-card { padding: 16px 16px 0; }
body:is([data-page="portfolio"], [data-page="commercialportfolio"], [data-page="designer-portfolio"]) .pf-thumb { border-radius: 14px; }
body:is([data-page="portfolio"], [data-page="commercialportfolio"], [data-page="designer-portfolio"]) .pf-meta { padding: 14px 4px 18px; }
body:is([data-page="portfolio"], [data-page="commercialportfolio"], [data-page="designer-portfolio"]) .pf-grid { counter-reset: pf; }
body:is([data-page="portfolio"], [data-page="commercialportfolio"], [data-page="designer-portfolio"]) .pf-card { counter-increment: pf; }
/* Two rows, not three columns. At three-across the caption had room for `No. 05 | title |
   4 views` on one line; at four-across the cards are ~285px and a three-line title left the
   count squeezed into a sliver beside it, wrapping "4" away from "views". The count drops to
   its own row and right-aligns, which holds at any card width and any title length. */
body:is([data-page="portfolio"], [data-page="commercialportfolio"], [data-page="designer-portfolio"]) .pf-meta {
  display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: baseline; gap: 6px 12px;
}
body:is([data-page="portfolio"], [data-page="commercialportfolio"], [data-page="designer-portfolio"]) .pf-meta span {
  grid-column: 2; justify-self: end;
}
/* Scope goes in the empty cell under the number: the meta grid put "No. 0X" top-left, the
   title top-right and the view count bottom-right, leaving bottom-left unused.

   It answers the question the thumbnail cannot. A single exterior tells you nothing about
   whether the job included the inside, and that is exactly what somebody deciding whether to
   click needs to know. */
body:is([data-page="portfolio"], [data-page="commercialportfolio"], [data-page="designer-portfolio"]) .pf-meta .pf-scope {
  grid-column: 1; grid-row: 2; justify-self: start;
  font-family: var(--font-mono); font-size: 9px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--taupe); white-space: nowrap;
}
@media (max-width: 640px) {
  body:is([data-page="portfolio"], [data-page="commercialportfolio"], [data-page="designer-portfolio"]) .pf-meta .pf-scope { font-size: 8.5px; letter-spacing: .06em; }
}
/* Credit line, spanning the full width under the rest of the meta. Some of this work was
   modelled and rendered here from somebody else's design, and saying so is the point: on a
   site whose whole argument is white-label production, crediting the designer makes the claim
   more believable, not less. */
body:is([data-page="portfolio"], [data-page="commercialportfolio"], [data-page="designer-portfolio"]) .pf-meta .pf-credit {
  grid-column: 1 / -1; justify-self: start; margin-top: 2px;
  font-family: var(--font-mono); font-size: 9px; letter-spacing: .08em;
  text-transform: none; color: var(--taupe);
}
.pf-credit a { text-decoration-thickness: 1px; }
body:is([data-page="portfolio"], [data-page="commercialportfolio"], [data-page="designer-portfolio"]) .pf-meta::before {
  content: "No. " counter(pf, decimal-leading-zero);
  font-size: 11px; letter-spacing: .12em; text-transform: uppercase;
  color: var(--taupe); font-variant-numeric: tabular-nums;
}/* lightbox */
.lb { position: fixed; inset: 0; z-index: 200; background: rgba(18,26,20,.94); display: none; flex-direction: column; }
.lb[open], .lb.on { display: flex; }
.lb-top { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 18px clamp(16px, 3vw, 32px); color: var(--cream); flex: none; }
.lb-title { font-size: 18px; font-weight: 480; }
.lb-count { font-size: 13px; color: var(--tan); letter-spacing: .08em; }
.lb-close { background: transparent; border: 1px solid rgba(249,240,222,.5); color: var(--cream); border-radius: 40px; padding: 8px 18px; font: inherit; font-size: 13px; letter-spacing: .1em; text-transform: uppercase; cursor: pointer; }
.lb-close:hover { background: rgba(249,240,222,.14); }
.lb-stage { flex: 1; min-height: 0; display: flex; align-items: center; justify-content: center; gap: 12px; padding: 0 clamp(8px, 2vw, 24px) 24px; }
.lb-stage img { max-width: 100%; max-height: 100%; object-fit: contain; }
.lb-nav { display: flex; align-items: center; justify-content: center; background: rgba(249,240,222,.12); border: 0; color: var(--cream); width: 48px; height: 48px; border-radius: 50%; cursor: pointer; flex: none; transition: background-color .25s var(--ease-out); }
.lb-nav:hover { background: rgba(249,240,222,.25); }
.lb-nav svg { width: 20px; height: 20px; }

/* FAQ */
.faq-list { margin-top: 40px; border-top: 1px solid var(--rule); }
.faq-item { border-bottom: 1px solid var(--rule); }
.faq-q { width: 100%; display: flex; align-items: baseline; justify-content: space-between; gap: 20px; background: transparent; border: 0; padding: 24px 4px; font: inherit; font-size: 19px; font-weight: 480; color: var(--ink); text-align: left; cursor: pointer; }
.faq-q:hover { color: var(--plum); }
.faq-q .mark { width: 18px; height: 18px; color: var(--leaf); flex: none; transition: transform .3s var(--ease-out); }
.faq-item.open .faq-q .mark { transform: rotate(135deg); }
.faq-a { display: none; padding: 0 4px 26px; }
.faq-item.open .faq-a { display: block; }
.faq-a p { font-size: 17px; line-height: 1.6; color: var(--taupe); text-wrap: pretty; }
.faq-a p + p { margin-top: 14px; }

/* contact */
.contact-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr)); gap: clamp(32px, 5vw, 56px); align-items: start; }
.form-row { display: grid; gap: 8px; margin-top: 20px; }
.form-row label { font-size: 13px; letter-spacing: .1em; text-transform: uppercase; font-weight: 480; color: var(--taupe); }
.form-row input, .form-row textarea, .form-row select { background: var(--cream); border: 1px solid #C9B893; border-radius: 4px; padding: 13px 16px; font: inherit; font-size: 16px; color: var(--ink); width: 100%; }
.form-row input:focus, .form-row textarea:focus, .form-row select:focus { outline: 2px solid var(--leaf); outline-offset: 1px; border-color: var(--leaf); }
.form-row textarea { min-height: 150px; resize: vertical; }
.form-note { font-size: 14px; color: var(--taupe); margin-top: 18px; text-wrap: pretty; }
#formMsg { margin-top: 18px; font-size: 15px; padding: 14px 18px; border-radius: 4px; }
#formMsg.ok { background: #D8E5C6; color: #175844; }
#formMsg.err { background: #F7E2CC; color: var(--wine); }
/* Label/value rows on hairline rules, not stacked cards. Same device as the spec table on the
   sourcing page: small uppercase label on the left, value on the right, a rule between rows.
   The label column is fixed so the values align down the page -- an uneven left edge is what
   makes this pattern read as a list rather than as a specification. */
.fact-list { display: grid; margin-top: 28px; border-top: 1.5px solid var(--ink); }
.fact {
  display: grid; grid-template-columns: 148px minmax(0, 1fr); gap: 20px; align-items: baseline;
  background: transparent; padding: 15px 2px; border-bottom: 1px solid rgba(20, 38, 29, .3);
}
.fact dt { font-size: 12px; letter-spacing: .14em; text-transform: uppercase; color: var(--taupe); }
.fact dd { margin: 0; font-size: 16px; }
@media (max-width: 560px) { .fact { grid-template-columns: 1fr; gap: 5px; } }

/* quote / email sample */
/* ---------- the reply, as a mail window ----------

   Was a bordered card with a subject line on top, which read as a pull quote rather than as
   correspondence. It is a window now: title bar, addressed header fields, message body. The
   retro comes from the CHROME, not from a typeface doing an impression -- a pinstriped bar,
   square hardware, a beige-and-ink palette that is already in the brand.

   The frame borrows the page's existing light: 2px ink edge, no radius, and the same hard
   offset the stat tiles and the work plates throw, down and to the left. */
.reply-layout { display: grid; gap: clamp(28px, 3.4vw, 44px); margin-top: 32px; align-items: start; }
@media (min-width: 940px) { .reply-layout { grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr); } }

.mailwin { background: var(--cream); border: 2px solid var(--ink); margin-top: 32px; box-shadow: -14px 14px 0 rgba(20, 38, 29, .18); }
.reply-layout .mailwin { margin-top: 0; }

/* Pinstripe on 4px, not 2px. A one-pixel-on, one-pixel-off bar is exactly the width that
   aliases into a moire as the page scrolls, the same trap the record grooves fell into. */
.mail-bar {
  display: flex; align-items: center; gap: 12px;
  padding: 7px 10px; border-bottom: 2px solid var(--ink);
  background: repeating-linear-gradient(to bottom, var(--forest) 0 2px, var(--pine) 2px 4px);
}
.mail-title {
  flex: 1; text-align: center;
  font-family: var(--font-mono); font-size: 11px; letter-spacing: .22em; text-transform: uppercase;
  color: var(--cream);
  /* The label sits ON the stripes, so it carries its own ground rather than fighting them. */
  background: var(--pine); padding: 2px 12px; box-shadow: 0 0 0 1.5px var(--cream);
}
/* Window hardware: a close box on the left, a zoom box on the right, both square. */
.mail-close, .mail-zoom { flex: none; width: 13px; height: 13px; background: var(--cream); box-shadow: 0 0 0 1.5px var(--ink); }
.mail-zoom { box-shadow: 0 0 0 1.5px var(--ink), inset 0 4px 0 var(--ink); }

/* Header fields. Labels are a fixed column so the values line up as an addressed block, and
   they take the mono because that is what reads as a field name rather than as prose. */
.mail-fields { padding: clamp(14px, 2vw, 18px) clamp(18px, 3vw, 28px); border-bottom: 2px solid var(--ink); background: var(--paper); }
.mail-fields > div { display: grid; grid-template-columns: 74px minmax(0, 1fr); gap: 12px; padding: 5px 0; }
.mail-fields > div + div { border-top: 1px solid rgba(20, 38, 29, .18); }
.mail-fields span:first-child { font-family: var(--font-mono); font-size: 11px; letter-spacing: .16em; text-transform: uppercase; color: var(--taupe); align-self: center; }
.mail-fields span + span { font-size: 15px; color: var(--ink); word-break: break-word; }

.mail-body { padding: clamp(20px, 3vw, 30px) clamp(18px, 3vw, 28px); }
.mail-body p { font-size: 16px; line-height: 1.6; color: var(--ink); }
.mail-body p + p { margin-top: 16px; }
/* The client brief is a numbered list of eleven finishes, so it stays a real <ol>: the
   numbers are how the client wrote it and how the copy refers back to them (item five, item
   ten). Set in the mail body type rather than the browser default. */
.mail-body .brief-list { margin: 16px 0 0; padding-left: 22px; }
.mail-body .brief-list li { font-size: 15px; line-height: 1.55; color: var(--ink); padding-left: 4px; }
.mail-body .brief-list li + li { margin-top: 10px; }
.mail-body .brief-list + p { margin-top: 18px; }
.mail-body .brief-list li::marker { font-weight: 700; color: var(--taupe); }
/* Redactions are a run of small rings, not a solid bar. The bar was the heaviest mark on the
   page and it sat inside a sentence, so the eye stopped there rather than reading past it.

   One ring stands for one hidden digit, and --n in the markup says how many. The width is
   derived from that count rather than from a string of block glyphs, which is also what let
   the glyphs go: a screen reader used to announce five "full block" characters, and the span
   now says "redacted" once instead.

   12px ring with a 4px hole, so the hole is a third of the diameter, the same proportion as
   every other donut on the site. Pitch is 14 to leave 2px of paper between them. */
.mail-body .redact {
  display: inline-block; vertical-align: -1px; user-select: none;
  width: calc(var(--n, 3) * 14px); height: 12px;
  background-image: radial-gradient(circle at 6px 6px, transparent 0 2px, var(--ink) 2px 6px, transparent 6px);
  background-size: 14px 12px; background-repeat: repeat-x;
}
.reply-sig { font-style: italic; color: var(--taupe); }

/* ---------- magazine ----------

   A spread that turns on its spine, not a track that slides. Three parts: two static slots,
   and a leaf that sits over one of them and rotates 180 degrees about the centre line.

   The leaf needs `preserve-3d` on itself and `perspective` on an ANCESTOR, not on the leaf.
   Perspective applies to an element's children, so putting it on the leaf would flatten the
   very rotation it is meant to project. It lives on .flip-frame.

   Backface visibility is what makes one element read as a sheet of paper with two sides: the
   back face is pre-rotated 180deg and both faces hide their reverse, so exactly one is ever
   facing the reader.

   Below 800 a spread is two thumbnails rather than a spread, so it drops to a single page and
   the leaf covers the whole frame. */
.flip { margin-top: 32px; }
.flip-frame { perspective: 2400px; background: var(--paper); border: 2px solid var(--ink); padding: 14px; }
.flip-spread { position: relative; display: grid; grid-template-columns: 1fr 1fr; gap: 0; }
@media (max-width: 800px) { .flip-spread { grid-template-columns: 1fr; } }

.flip-slot img, .flip-face img { width: 100%; height: auto; display: block; background: #FFFFFF; }
.flip-slot { min-height: 1px; }
/* The gutter, which is what makes it read as an open magazine rather than two pages side by
   side. Three layers, all on one overlay:

     1. the spine, a single hard line on the centre
     2. the shading either side of it, wide and soft, because in a real spread the paper curves
        away into the binding and loses light over about a sixth of each page
     3. a touch of shade at the outer edges, which is the thickness of the block underneath

   The overlay sits ABOVE the turning leaf on purpose. In a real book the gutter shadow belongs
   to the binding, not to the page, so it should stay put while a page moves through it.
   pointer-events are off so it cannot swallow a click on the buttons beneath it. */
.flip-spread::after {
  content: ""; position: absolute; inset: 0; z-index: 3; pointer-events: none;
  background:
    linear-gradient(to right, transparent calc(50% - 1px), rgba(20, 38, 29, .5) calc(50% - 1px), rgba(20, 38, 29, .5) calc(50% + 1px), transparent calc(50% + 1px)),
    linear-gradient(to right, transparent 33%, rgba(20, 38, 29, .05) 43%, rgba(20, 38, 29, .19) 50%, rgba(20, 38, 29, .05) 57%, transparent 67%),
    linear-gradient(to right, rgba(20, 38, 29, .13), transparent 3%, transparent 97%, rgba(20, 38, 29, .13));
}
/* One page on a phone, so there is no gutter to draw: only the block edges survive. */
@media (max-width: 800px) {
  .flip-spread::after {
    background: linear-gradient(to right, rgba(20, 38, 29, .13), transparent 4%, transparent 96%, rgba(20, 38, 29, .13));
  }
}

.flip-leaf {
  position: absolute; top: 0; bottom: 0; left: 50%; width: 50%;
  transform-style: preserve-3d; transform-origin: left center;
  transition: transform .7s cubic-bezier(.4, .05, .2, 1);
  z-index: 2;
}
@media (max-width: 800px) { .flip-leaf { left: 0; width: 100%; } }
.flip-face {
  position: absolute; inset: 0;
  backface-visibility: hidden; -webkit-backface-visibility: hidden;
  background: var(--paper);
}
.flip-back { transform: rotateY(180deg); }
.flip-leaf[hidden] { display: none; }
@media (prefers-reduced-motion: reduce) { .flip-leaf { transition: none; } }

.flip-bar { display: flex; align-items: center; gap: 16px; padding: 12px 2px; }
.flip-count {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .18em; text-transform: uppercase;
  color: var(--taupe); font-variant-numeric: tabular-nums;
}
.flip-btn {
  font: inherit; font-size: 15px; line-height: 1; cursor: pointer;
  width: 40px; height: 40px; border: 0; border-radius: 999px;
  background: var(--plum); color: var(--cream);
  transition: background-color .2s var(--ease-out), transform .2s var(--ease-out);
}
.flip-btn:hover { background: var(--plum-dark); transform: translateY(-2px); }
.flip-btn[disabled] { background: var(--sand); color: var(--taupe); cursor: default; transform: none; }
/* ---------- draft strip ----------

   A real overflow-x container rather than an animated transform, for the same reason the work
   marquee is one: touch swipe, trackpad and arrow keys all come free and none of them fight
   each other. tabindex makes it reachable without a mouse. */
.strip { overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain; scrollbar-width: none; margin-top: 32px; }
.strip::-webkit-scrollbar { display: none; }
.strip:focus-visible { outline: 2px solid var(--leaf); outline-offset: 3px; }
/* The plate shadow falls down and to the LEFT (-10px, 10px), so at a 16px gap it landed on
   top of the previous frame and read as a dark bar slicing it. The gap has to clear the
   offset, and the track needs a left inset or the first frame has its shadow cut off by the
   scroller. Same rule the extruded grids elsewhere follow. */
.strip-track { display: flex; gap: 30px; width: max-content; padding: 2px 2px 20px 14px; }
.strip-fig { flex: none; margin: 0; width: clamp(240px, 34vw, 420px); }
/* The image used to bleed to the edge of its own keyline, so a wide draft read as a slice of
   something bigger rather than as a whole frame. It now sits on a paper mat inside the plate,
   which is the same construction as the sheet plates and stops the strip looking cropped. */
.strip-fig { background: var(--paper); box-shadow: 0 0 0 1.5px var(--ink), -10px 10px 0 var(--ink); padding: 10px; }
.strip-fig img { width: 100%; height: auto; display: block; }
/* One label per round, sitting on the rule above its strip, so five strips in a row still read
   as a sequence rather than as one long gallery. */
.strip-label {
  display: flex; align-items: center; gap: 9px;
  margin: 40px 0 0; padding-top: 14px; border-top: 1.5px solid var(--ink);
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--taupe);
}
.strip-label + .strip { margin-top: 16px; }
/* Frame count on a strip label. The Hillside study shows every round unsampled, so the
   number of frames is information rather than decoration: it is the size of the round. */
.strip-count { margin-left: 10px; opacity: .62; font-weight: 400; }

/* ---------- wallpaper sample rail ----------
   For the wall covering studies, which are the one thing on the site that exists as a physical
   sample in real life. A grid of them would be a grid of pictures; hung from a rail they read
   as what they are, a sample book you push along with a finger.

   The rail lives inside .strip-track, not on the scroller, so it travels WITH the samples. On
   the scroller it would have hung motionless while they slid underneath, which is the one thing
   a rail never does.

   Ring, wire, then the sheet. The stagger is nth-child rather than random so it repeats
   predictably at any count, and it is what stops nine tall panels reading as a bar chart. */
.wall-rail .strip-track { position: relative; padding-top: 34px; align-items: flex-start; }
.wall-rail .strip-track::before {
  content: ""; position: absolute; left: 0; right: 0; top: 15px; height: 1.5px; background: var(--ink);
}
.wall-rail .strip-fig { width: clamp(148px, 17vw, 208px); position: relative; padding-top: 20px; }
.wall-rail .strip-fig:nth-child(even) { padding-top: 52px; }
.wall-rail .strip-fig::before {
  content: ""; position: absolute; left: 50%; top: -19px; transform: translateX(-50%);
  width: 1.5px; height: 100%; background: var(--ink); opacity: .55;
}
.wall-rail .strip-fig::after {
  content: ""; position: absolute; left: 50%; top: -24px; transform: translateX(-50%);
  width: 11px; height: 11px; border-radius: 50%;
  background: var(--cream); box-shadow: 0 0 0 1.5px var(--ink);
}
/* The wire must not paint over the sheet it is holding. */
.wall-rail .strip-fig img { position: relative; z-index: 1; box-shadow: 0 0 0 1.5px var(--ink), -6px 6px 0 var(--ink); }

/* ---------- the reference board ----------
   CSS columns rather than grid, on purpose. A grid would give every pin the same box and crop
   the life out of them; columns let each keep its own proportion and pack under the one above,
   which is the masonry a pinboard actually is. The thing being shown IS a Pinterest board, so
   it should look like one.

   break-inside is what stops a pin splitting across a column, and the margin has to live on the
   figure rather than the column gap or the last pin in each column loses its footing. */
.pinboard { columns: 4 200px; column-gap: 12px; margin-top: 32px; }
.pinboard figure { break-inside: avoid; margin: 0 0 12px; }
.pinboard img {
  width: 100%; height: auto; display: block;
  box-shadow: 0 0 0 1.5px var(--ink);
}
@media (max-width: 640px) { .pinboard { columns: 2 130px; column-gap: 8px; } .pinboard figure { margin-bottom: 8px; } }
/* ---------- case study plates ----------

   Same mount as the drawing sheets and the service galleries, but sized for photographs
   rather than for line work: no white ground behind the image, because these are renderings
   and AI concepts, not plotted drawings.

   .cs-row is the three-across group. It shares the extruded-grid rules: the gap has to clear
   the offset, and the padding under it stops the next paragraph landing on the shadow. */
.cs-plate {
  margin: 32px 0 0; background: var(--paper);
  border: 2px solid var(--ink); padding: 12px 12px 0;
  box-shadow: -16px 16px 0 var(--ink);
}
.cs-plate img { width: 100%; height: auto; display: block; box-shadow: inset 0 0 0 2px var(--ink); }
.cs-plate figcaption {
  display: flex; align-items: center; gap: 9px; padding: 11px 2px;
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--taupe);
}
.cs-row {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
  gap: 32px; margin-top: 32px; padding-bottom: 20px;
}
.cs-row .cs-plate { margin: 0; }

/* ---------- the delivered work ----------
   The finished renderings are the reason somebody scrolled this far, and every case study was
   putting them in an auto-fit grid that landed three across and shrank each one to about 340px.
   A 1800px rendering of a room, shown at 340px, is a thumbnail of an argument.

   So: the first one takes the full measure, and the rest run in pairs. Same .cs-plate mount as
   everywhere else, just given room.

   The :last-child:nth-child(even) rule is what stops a stranded tile. Children are 1-indexed and
   the hero is child 1, so pairs start on the even indices; a last child on an even index has
   started a row nothing will join, and spans instead. That holds for any count rather than for
   the three counts these pages happen to have today. */
.cs-final {
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 32px; margin-top: 36px; padding-bottom: 20px;
}
.cs-final .cs-plate { margin: 0; }
.cs-final > :first-child { grid-column: 1 / -1; }
.cs-final > :last-child:nth-child(even) { grid-column: 1 / -1; }
@media (max-width: 720px) { .cs-final { grid-template-columns: 1fr; gap: 22px; } }

/* The swatch card from the paint-chip reference, and the one place on the site with real
   content for it: these are the colours actually specified for this project, named and
   numbered off the finish schedule. A punched disc rather than a printed dot, so the card
   reads as a chip with a hole in it the way the reference does. */
.cs-swatches { display: flex; flex-wrap: wrap; gap: 18px; margin-top: 36px; }
.cs-chip {
  flex: 1 1 180px; max-width: 240px; aspect-ratio: 3 / 4;
  background: var(--face);
  box-shadow: 0 0 0 1.5px var(--ink), -12px 12px 0 rgba(20, 38, 29, .16);
  display: flex; flex-direction: column; align-items: center; justify-content: flex-end;
  gap: 4px; padding: 0 12px 18px; position: relative;
}
.cs-chip::before {
  content: ""; position: absolute; left: 50%; top: 38%; transform: translate(-50%, -50%);
  width: 34%; aspect-ratio: 1; border-radius: 50%; background: var(--dot);
}
.cs-code, .cs-name {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--ink); mix-blend-mode: difference; filter: invert(1) grayscale(1) contrast(9);
}
.cs-name { opacity: .75; }

/* ---------- drawing sheets ----------
   One sheet per row, not a grid. These are landscape drawings whose whole value is being able
   to read a dimension string and a room label; three of them side by side would be three
   thumbnails of a drawing, which is worth nothing.

   Same mount as the service galleries and the CRT: paper shell, the sheet recessed behind an
   ink keyline, a mono nameplate under it. The sheet ground stays WHITE rather than the site's
   paper, because that is the paper the drawing was actually plotted on and tinting it would
   dull the linework. */
.sheets-head { margin-top: clamp(56px, 7vw, 88px); }
.sheets { display: grid; gap: clamp(28px, 3.4vw, 44px); margin-top: 32px; padding-bottom: 20px; }

/* Column counts for a sheet grid.

   These were inline `grid-template-columns:repeat(N,...)` on the page, which cannot respond to
   a breakpoint: a three column grid stayed three columns at 375px, leaving 74px per tile, and
   the captions overflowed the tile and pushed the document 6px sideways. As classes they can
   step down. minmax(0,1fr) throughout so a wide image cannot force a track open. */
.sheets.cols-2 { grid-template-columns: minmax(0, 1fr); }
.sheets.cols-3 { grid-template-columns: minmax(0, 1fr); }
.sheets.cols-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
@media (min-width: 560px) {
  .sheets.cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .sheets.cols-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (min-width: 860px) {
  .sheets.cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
}
@media (min-width: 1120px) {
  .sheets.cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
.sheet {
  margin: 0; background: var(--paper);
  border: 2px solid var(--ink); padding: 0;
  box-shadow: -16px 16px 0 var(--ink);
  overflow: hidden;
}
.sheet img {
  width: 100%; aspect-ratio: 4 / 3; height: auto; object-fit: cover; display: block;
  background: #FFFFFF; border-bottom: 2px solid var(--ink);
}
/* Drawings and documents are read, not glanced at, so they keep their whole frame, and they
   keep the plate they always had: a white ground behind a drawing is part of reading it. */
.sheets.doc .sheet img { aspect-ratio: auto; object-fit: contain; }
.sheet figcaption {
  display: flex; align-items: center; gap: 9px; padding: 13px 16px 15px;
  font-family: var(--font-mono); font-size: 10.5px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--taupe);
}
.sheet-led { flex: none; width: 7px; height: 7px; border-radius: 50%; background: var(--leaf); box-shadow: 0 0 0 1.5px var(--ink), 0 0 6px var(--leaf); }
@media (max-width: 700px) {
  .sheet { box-shadow: -10px 10px 0 var(--ink); }
  .sheets { padding-bottom: 12px; }
}
/* ---------- the furniture poster ----------

   Two halves, not a card with an image in it. Left is the print (paper ground, the piece, one
   caption line), right is the data (green ground, kicker, title, four rows). They meet on a
   shared edge with no gap -- the same rule the sourcing poster follows, since a gutter here
   would turn a poster into two panels.

   The medallion is a real photograph in a circle with the tan disc offset behind it, which is
   the reference's own move: a flat colour shape the subject is placed over. Three attempts at
   a DRAWN chair came before this (three-quarter tubular, profile line art, a row of primitive
   silhouettes) and none of them read as furniture at this size. */
.chair-poster { display: grid; margin-top: clamp(48px, 6vw, 80px); }
@media (min-width: 900px) { .chair-poster { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); } }

.cp-plate {
  background: var(--paper);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 28px; padding: clamp(32px, 4.5vw, 64px);
}
/* The disc is the ::before, offset up and left, so the photograph sits over it the way the
   subject sits over the flat shape in the print. */
.cp-art { width: min(100%, 460px); }
.cp-art svg { width: 100%; height: auto; display: block; }
.cp-sun { fill: var(--tan); }
.cp-ground { fill: var(--sand); }
.cp-strap { fill: var(--forest); }
.cp-strapline { stroke: var(--forest); }
.cp-cut { fill: var(--paper); }
.cp-tube { stroke: var(--ink); }
.cp-caption {
  margin: 0; font-family: var(--font-mono); font-size: 10px; letter-spacing: .16em;
  text-transform: uppercase; color: var(--taupe); text-align: center; max-width: 34ch;
  line-height: 1.6;
}

.cp-spec {
  background: var(--forest); color: var(--cream);
  display: flex; flex-direction: column; justify-content: center;
  gap: 18px; padding: clamp(32px, 4vw, 64px);
}
.cp-kicker { margin: 0; font-family: var(--font-mono); font-size: 11px; letter-spacing: .24em; text-transform: uppercase; color: var(--sand); }
.cp-title { font-family: var(--font-display); font-weight: 600; font-size: clamp(28px, 3.4vw, 46px); line-height: 1.04; color: var(--cream); }
.cp-body { margin: 0; font-size: 16px; line-height: 1.55; color: #CBDCCE; max-width: 46ch; text-wrap: pretty; }

/* A real <dl>, so the label/value pairing survives without the styling. Fixed label column so
   the values align into a column the way a parts list does. */
.cp-rows { margin: 6px 0 0; display: grid; gap: 0; }
.cp-rows > div { display: grid; grid-template-columns: 140px minmax(0, 1fr); gap: 16px; padding: 10px 0; border-top: 1px solid rgba(249, 240, 222, .28); }
.cp-rows > div:last-child { border-bottom: 1px solid rgba(249, 240, 222, .28); }
.cp-rows dt { font-family: var(--font-mono); font-size: 10px; letter-spacing: .16em; text-transform: uppercase; color: var(--sand); align-self: center; }
.cp-rows dd { margin: 0; font-size: 15px; color: var(--cream); }
.cp-more { margin: 4px 0 0; }
@media (max-width: 560px) {
  .cp-rows > div { grid-template-columns: minmax(0, 1fr); gap: 3px; }
}
/* ---------- the sourcing poster ----------

   Six flat panels in a 3x2, from the mid-century lamp print: blocks of colour, one object per
   block drawn as a silhouette, one block for the title and one for a line of copy.

   Two rules keep it a poster rather than a card grid. The panels TOUCH -- no gap, no gutter,
   no radius, no shadow -- because the colour blocks meeting edge to edge is what the reference
   is made of; a gap would turn it into six cards. And every panel declares exactly two
   colours, its ground and its `color`, with the SVG filling from currentColor, so the art can
   never drift out of step with the block it sits on.

   Full bleed, like the work strip: this is a poster, and a poster with page margins is a
   picture of a poster.

   The order alternates dark, light, dark across the top and light, dark, light... no: the
   TITLE sits third and the quote fifth, so the two text panels land on opposite diagonals and
   the four drawings sit around them rather than in a row. */
/* Four across, not three. The poster gained Materials and Textiles, and eight panels in a
   three-column grid leaves a hole in the last row. Four columns takes the two rows exactly. */
.spec-poster {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  margin-top: var(--pad-section);
}
@media (max-width: 900px) { .spec-poster { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.sp-panel {
  position: relative;
  aspect-ratio: 3 / 4;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 18px;
  padding: clamp(18px, 2.4vw, 32px);
  text-align: center;
}
.sp-panel svg { width: min(58%, 190px); height: auto; display: block; }
.sp-label {
  font-family: var(--font-mono); font-size: 11px; letter-spacing: .24em; text-transform: uppercase;
  opacity: .85;
}
/* Ground and art colour, one pair per panel. Nothing else in here sets a colour. */
.sp-pine       { background: var(--pine);       color: var(--tan); }
.sp-tan        { background: var(--tan);        color: var(--plum); }
.sp-plum       { background: var(--plum);       color: var(--cream); }
.sp-forest     { background: var(--forest);     color: var(--sand); }
.sp-sand       { background: var(--sand);       color: var(--ink); }
.sp-plum-deep  { background: var(--aubergine);  color: var(--tan); }

.sp-title { justify-content: flex-start; align-items: flex-start; text-align: left; gap: 10px; }
.sp-kicker { font-family: var(--font-mono); font-size: 11px; letter-spacing: .24em; text-transform: uppercase; opacity: .8; }
.sp-big { font-family: var(--font-display); font-weight: 600; font-size: clamp(28px, 3.4vw, 48px); line-height: 1.02; }

.sp-quote { align-items: flex-start; text-align: left; }
.sp-quote p { margin: 0; font-size: clamp(16px, 1.5vw, 21px); line-height: 1.35; font-weight: 500; text-wrap: pretty; }

/* Two across below 860, and the aspect squares up: at 3/4 a half-width panel gets tall enough
   to push the whole band past a phone screen on its own. */
@media (max-width: 860px) {
  .spec-poster { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .sp-panel { aspect-ratio: 1; }
}
/* ---------- media stage ----------

   The shell the video tour and the 360 walkthrough both sit in, built from the CRT's
   vocabulary so the two read as the same piece of equipment: paper shell, ink-keyed screen,
   a mono nameplate with a power light along the bottom.

   Anything dropped into .media-stage is positioned and sized by the one rule below, so a
   <video>, an <iframe> and Pannellum's generated container all fill it without their own CSS.
   Pannellum in particular needs a parent with real dimensions or it initialises to zero
   height, which is what the absolute inset gives it. */
.media-shell { margin-top: clamp(36px, 4.5vw, 56px); background: var(--paper); border: 2px solid var(--ink); padding: 14px 14px 0; box-shadow: -14px 14px 0 rgba(20, 38, 29, .18); }
.media-stage { position: relative; aspect-ratio: 16 / 9; overflow: hidden; background: var(--pine); box-shadow: inset 0 0 0 2px var(--ink); }
.media-stage > * { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; display: block; }
.media-stage > video { object-fit: cover; }
.media-bar { display: flex; align-items: center; gap: 10px; margin: 0; padding: 12px 2px; font-family: var(--font-mono); font-size: 10px; letter-spacing: .2em; color: var(--taupe); }
.media-led { flex: none; width: 8px; height: 8px; border-radius: 50%; background: var(--leaf); box-shadow: 0 0 0 1.5px var(--ink), 0 0 7px var(--leaf); }
.media-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 14px; }
/* Play triangle from borders, so the empty state costs no asset. */
.media-play { width: 0; height: 0; border-style: solid; border-width: 17px 0 17px 29px; border-color: transparent transparent transparent var(--sand); }
.media-caption { font-family: var(--font-mono); font-size: 11px; letter-spacing: .2em; text-transform: uppercase; color: var(--sand); }
/* Pannellum paints its own load screen and controls; only the ground needs correcting so it
   matches the stage rather than the library's default black. */
/* CloudPano loads in two steps and both of them land one level deeper than .media-stage > *,
   so each needs its own rule: the stage sizes the wrapper div, these size what appears inside
   it.

   Step one is a click-to-load splash carrying the tour's thumbnail, which the script writes
   with an inline `height: 500px`. Against a stage that is 604 tall at desktop that left a
   black band under it and made the embed look broken -- which it was not, it was waiting to
   be clicked. Inline styles need !important to beat, there is no other lever here.

   Step two is the iframe, injected only after the splash is clicked. That is CloudPano's own
   bandwidth-saving behaviour, not something to work around: the tour is several megabytes and
   most visitors will not open it. */
.media-stage iframe,
.media-stage [id^="cp-splash-screen"] {
  position: absolute; inset: 0;
  width: 100% !important; height: 100% !important;
  border: 0; display: block;
}
/* ---------- the walkthrough, as a CRT ----------

   Beige shell, dark screen, and a nameplate along the bottom. --screen-ar is the one number
   to change when a real video lands: 4/3 is the retro proportion, a Loom embed is 16/9 and
   will letterbox until this is switched.

   Whatever goes in the screen is positioned by this rule and nothing else, so an <iframe> and
   a <video> both drop in with no further CSS. */
.crt { --screen-ar: 4 / 3; margin: 0; background: var(--paper); border: 2px solid var(--ink); padding: 14px 14px 0; box-shadow: -14px 14px 0 rgba(20, 38, 29, .18); }
.crt-screen { position: relative; aspect-ratio: var(--screen-ar); overflow: hidden; background: var(--pine); box-shadow: inset 0 0 0 2px var(--ink); }
.crt-screen > iframe, .crt-screen > video { position: absolute; inset: 0; width: 100%; height: 100%; border: 0; object-fit: cover; display: block; }
/* Scanlines over whatever is underneath, so they stay once a video is in. 3px pitch for the
   same anti-moire reason as the title bar. */
.crt-screen::after {
  content: ""; position: absolute; inset: 0; pointer-events: none;
  background: repeating-linear-gradient(to bottom, rgba(249, 240, 222, .07) 0 1px, transparent 1px 3px);
}
.crt-plate { display: flex; align-items: center; gap: 10px; padding: 12px 2px; font-family: var(--font-mono); font-size: 10px; letter-spacing: .2em; color: var(--taupe); }
/* Power light. Salem rather than a red, because the palette has no red and a green LED is the
   more accurate one for the era anyway. */
.crt-led { width: 8px; height: 8px; border-radius: 50%; background: var(--leaf); box-shadow: 0 0 0 1.5px var(--ink), 0 0 7px var(--leaf); }
@keyframes crtBlink { 0%, 74% { opacity: 1; } 75%, 100% { opacity: .35; } }

/* misc */
/* Parts-list table. The vintage spec-sheet look belongs here specifically: a specification
   list is one of the actual deliverables, so the styling demonstrates the product rather than
   decorating the page.
   Radome Tan is the ground because it is the palette colour nearest manila card.
   Everything is uppercase on wide tracking with hairline rules, and the numbers take
   tabular-nums so the No. column stays a true column. */
.spec { width: 100%; max-width: 560px; margin-top: 28px; border-collapse: separate; border-spacing: 0;
  background: var(--sand); box-shadow: 0 0 0 1.5px var(--ink), -20px 20px 0 var(--ink); }
.spec th, .spec td { padding: 11px 16px; text-align: left; color: var(--ink);
  font-size: 13px; letter-spacing: .07em; text-transform: uppercase; }
.spec thead th { font-weight: 700; letter-spacing: .13em; border-bottom: 1.5px solid var(--ink); }
.spec tbody td { border-top: 1px solid rgba(20, 38, 29, .45); font-weight: 500; }
/* The rule down the No. column is what makes it read as a ruled form rather than a list. */
.spec th:first-child, .spec td:first-child {
  width: 62px; border-right: 1px solid rgba(20, 38, 29, .45);
  font-variant-numeric: tabular-nums; font-weight: 700;
}
.spec-note { margin-top: 34px; font-size: 12px; letter-spacing: .13em; text-transform: uppercase; color: var(--taupe); }

/* The table was written for a short numbered parts list: a 62px first column and 16px cell
   padding. The case-study billing tables put words in that first column instead, and with
   table-layout auto the minimum content width won, so the table ran 465px wide inside a 343px
   column and dragged the whole document into a horizontal scroll on a phone. Every page
   carrying one was affected.

   Fixed layout makes the declared widths binding, so the text wraps instead of pushing. */
@media (max-width: 640px) {
  /* Fixed layout fitted the table to the column but broke words down the middle (PACKAG / E),
     which is worse than the overflow it fixed. Instead the table keeps its real column widths
     and scrolls inside its own frame, so the page itself never moves sideways. `display:block`
     on the table makes it the scroll container; the rows still lay out as table rows inside it. */
  .spec { display: block; overflow-x: auto; -webkit-overflow-scrolling: touch;
    max-width: 100%; box-shadow: 0 0 0 1.5px var(--ink), -10px 10px 0 var(--ink); }
  .spec th, .spec td { padding: 10px 12px; font-size: 11.5px; letter-spacing: .05em; white-space: nowrap; }
  /* Pinned, so the row you are reading keeps its label while the charge column comes into
     view. Needs its own background or the scrolling cells show through underneath. */
  .spec th:first-child, .spec td:first-child { width: auto; position: sticky; left: 0; background: var(--sand);
    border-right: 1px solid rgba(20, 38, 29, .45); }
}

.pill-row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 28px; }
.pill { border: 1px solid #CBB99C; border-radius: 40px; padding: 9px 18px; font-size: 14px; color: var(--taupe); }
.band-forest .pill { border-color: rgba(249,240,222,.4); color: #CBDCCE; }
.related { margin-top: 0; padding-top: 24px; border-top: 1px solid var(--rule); font-size: 15px; color: var(--taupe); }
.related a { font-weight: 480; text-decoration: underline; text-decoration-color: var(--underline); text-decoration-thickness: 2px; text-underline-offset: 3px; }
.related a:hover { text-decoration-color: currentColor; }
/* On twelve pages the related line is alone in a bare <section class="container">, which has
   no vertical padding of its own. That put its rule hard against the colour band above, so it
   read as a stub hanging off the band rather than a divider introducing the links. Only the
   sole-occupant case is targeted: where .related sits inside a .section-pad next to other
   content, that section already supplies the space and must not gain more. */
section.container:not(.section-pad) > .related:only-child { margin-top: var(--pad-section); }
.cta-strip { display: flex; flex-wrap: wrap; align-items: center; gap: 16px; margin-top: 36px; }

/* mobile menu */
/* The menu button, shown at every width now that the drawer is the whole navigation rather
   than a phone fallback. The three bars stay and become the mark beside the word, which is why
   they are laid out in a nested column rather than being the button itself. */
/* Layout only. Its ground, border, radius and type come from the shared pill rule it shares
   with the nav links, so the two can never drift apart. */
.nav-toggle {
  display: flex; flex-direction: row; align-items: center; gap: 9px;
  cursor: pointer; width: auto; height: auto;
}
.nav-toggle .bars span { width: 18px; }
/* Ink, not cream: the button has a filled cream ground now, so cream bars on it are invisible. */
.nav-toggle .bars span { display: block; height: 2px; background: var(--ink); border-radius: 2px; transition: transform .3s ease, opacity .2s ease; }
.nav-toggle[aria-expanded="true"] .bars span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] .bars span:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] .bars span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }

/* svh for the same reason as the hero: with vh (the URL-bar-hidden height) the menu
   believes it has more room than is on screen, and its last items, including the CTA ,
   sit behind the URL bar with no way to scroll to them. overscroll-behavior stops a
   flick inside the menu from chaining into the page underneath. */
.mobile-menu { background: #123D2E; color: var(--cream); flex: 1 1 auto; min-height: 0; overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; }
.mm-inner { padding: 8px clamp(16px, 5vw, 32px) 32px; }
.mm-group { padding: 14px 0; border-bottom: 1px solid rgba(249,240,222,.16); display: flex; flex-direction: column; align-items: flex-start; gap: 2px; }
.mm-head { font-weight: 600; font-size: 12px; letter-spacing: .16em; text-transform: uppercase; color: var(--pink); margin-bottom: 4px; }
/* 44px minimum: these were 20px-tall inline links, well under a comfortable thumb target. */
.mobile-menu a { color: var(--cream); font-size: 17px; display: flex; align-items: center; min-height: 44px; width: 100%; }
.mobile-menu a:hover { color: var(--tan); }
/* The CTA is caught by the .mobile-menu a rule above, so re-centre its label. */
.mm-cta { margin-top: 24px; justify-content: center; }

/* ---------- graduated hem ----------

   Six columns of horizontal stripes whose pitch grows left to right, from a fine comb into
   two broad blocks. The interest is entirely in the RATE of change, so the two greens stay
   flat and nothing else is added: no shading, no third colour, no texture.

   Pitches are the Fibonacci run 3, 5, 8, 13, 21, 34. A plain doubling collapses too fast at
   this height (the last two columns come out identical, both a single band), where the
   Fibonacci step accelerates gently enough that all six columns stay distinct.

   Odd and even columns start on opposite colours, which offsets every other column by half a
   cycle. That is what keeps the band from reading as one continuous comb.

   Nothing narrower than 3px. A 1 or 2px stripe lands between device pixels and the comb
   moires as the page scrolls, the same trap the record grooves fell into.

   One band, once per page, immediately above the footer, and that is the whole budget for
   this pattern. The site already carries the tile motif in the quote band and on services,
   and a second field of pattern in the body would fight it. */
.hem { display: flex; height: 64px; background: var(--forest); }
.hem i { flex: 1; background: repeating-linear-gradient(to bottom, var(--sand) 0 var(--s), var(--forest) var(--s) calc(var(--s) * 2)); }
.hem i:nth-child(even) { background: repeating-linear-gradient(to bottom, var(--forest) 0 var(--s), var(--sand) var(--s) calc(var(--s) * 2)); }
.hem i:nth-child(1) { --s: 3px; }
.hem i:nth-child(2) { --s: 5px; }
.hem i:nth-child(3) { --s: 8px; }
.hem i:nth-child(4) { --s: 13px; }
.hem i:nth-child(5) { --s: 21px; }
.hem i:nth-child(6) { --s: 34px; }
@media (max-width: 700px) { .hem { height: 44px; } }
@media (max-width: 900px) {

  .btn-nav { display: none; }
}

@media (max-width: 700px) {
  .step-row { grid-template-columns: 1fr; gap: 10px; }
  .svc-row { grid-template-columns: 1fr; gap: 12px; }
  .svc-row .idx { margin: 0; }
  .lb-nav { width: 40px; height: 40px; }
}

/* ---------- draft placeholder ----------

   Only ever on a page carrying "draft: yes" in its metadata comment. It is deliberately ugly:
   a dashed box in the error colour, so a half-finished section can never be mistaken for a
   finished one in a screenshot or a review. Every one of these is a delete-me, and the page is
   done when the last one is gone. */
.cs-todo {
  margin: 24px 0 0;
  padding: 20px 22px;
  border: 2px dashed var(--rose);
  background: rgba(242, 110, 114, .06);
  font-family: var(--font-mono); font-size: 11px; line-height: 1.9;
  letter-spacing: .08em; text-transform: uppercase; color: var(--taupe);
}
.cs-todo b { color: var(--ink); letter-spacing: .12em; }

/* ---------- masthead hero ----------

   The studio name at full width between two rules, a dense line of small caps under it, then
   the work as a row of cards. Cream ground, ink rules, and colour carried entirely by the
   card grounds.

   It is an ordinary section, not the old pinned one. The photographic hero was two screens
   tall so the picture had somewhere to drift; with no picture that second screen was a
   hundred vertical percent of empty cream, so this unpins and gets out of the way. */
.hero-plain { height: auto; }
.hero-plain .hero-pin { position: static; height: auto; overflow: visible; background: var(--cream); }
/* No min-height. The photographic hero had to be a full screen so the picture had somewhere to
   drift; this one is a masthead, a line of type and a row of cards, and forcing it to 100vh
   just parks a block of empty cream between the last button and the work band. It is as tall
   as its contents and no taller. */
.hero-plain .hero-layout {
  position: static;
  padding: 118px 0 clamp(28px, 3.4vw, 48px);
}
.hero-plain .hero-content {
  animation: none; opacity: 1; transform: none;
  flex: none; align-items: stretch; justify-content: flex-start; text-align: left;
  width: min(1240px, 100% - 48px); margin: 0 auto; padding: 0;
}

/* The name, set to fill the measure. Sized off vw so it stays edge to edge at every width,
   which is the whole point of a masthead: it is a rule of type, not a heading that happens to
   be large. nowrap because it must never break, and the vw figure is tuned so it cannot. */
/* The measure the masthead is sized against. This exists only to be a query container: it wraps
   the word and nothing else, so the containment it carries cannot reach the hero's positioned
   children. */
.mast-fit { container-type: inline-size; }

.mast-word {
  margin: 0;
  /* Archivo, not the rounded display face. A masthead of this kind is a neutral grotesque set
     tight: the reference is Helvetica Bold, and Archivo is the site's own heading family and
     the closest thing to it already in the stylesheet, so this costs nothing to load. The
     rounded face read as a logo rather than as a rule of type at this size. */
  font-family: Archivo, "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-weight: 700;
  /* Measured, not guessed. This string sets 5.2469px wide per 1px of font-size in Archivo 700
     at this tracking, so the size that fills a container of width C is C / 5.2469, or 19.06% of
     it. 19 is used, which lands a whisker inside rather than exactly on the edge.

     Sized in cqi, off the CONTAINER, not in vw. It was vw, and that is why ASTRATTO hung off
     the right: 100vw counts the scrollbar, the content box does not, so the word was solved for
     a measure about 15px wider than it actually had. That reads as deliberate bleed on a wide
     screen and as a bug on a phone, and it was neither, it was the scrollbar. The vw line is
     kept underneath as the fallback for anything without container queries.

     The 236px cap is belt and braces: .hero-content clamps at 1240px, where 19cqi is 235.6. */
  font-size: min(calc(19.01vw - 14.1px), 236px);
  font-size: min(19cqi, 236px);
  line-height: .9; letter-spacing: -.03em;
  text-transform: uppercase; white-space: nowrap; color: var(--ink);
  border-top: 1.5px solid var(--ink); border-bottom: 1.5px solid var(--ink);
  padding: .09em 0 .05em;
  text-align: center;
}

/* The positioning line, and the h1. Set as a dense block of small caps rather than as a
   headline: the masthead above is already carrying the visual weight, and this has to be read
   rather than looked at. Centred and narrow so it sits as a caption to the name.

   The selector carries .hero-content because the base rule for the hero headline is
   .hero-content h1, which outranks a lone class. Written as .mast-line alone this whole block
   loses and the strapline renders at 78px. */
/* The strapline is a caption sitting between the masthead rule above it and the plate below,
   so both of its gaps come from ONE value. They were 20px and 47px, which read as the line
   being stuck to the rule above rather than sitting in its own space. */
.hero-content h1.mast-line {
  --mast-caption-gap: clamp(22px, 2.6vw, 34px);
  /* Full width, not 92ch. Each sentence gets a line of its own, and the longer of the two needs
     1062px to stay on one; 92ch capped this at 531 and folded it in half. The hero gives 1240 at
     1440 wide, so both sit on single lines down to about 1260, and wrap below that. */
  margin: var(--mast-caption-gap) auto 0; max-width: 100%;
  font-family: var(--font-mono); font-weight: 500;
  font-size: 10.5px; line-height: 1.85; letter-spacing: .12em;
  text-transform: uppercase; color: var(--taupe); text-align: center;
}
/* One sentence per block. As a single flowed paragraph the sentence break fell wherever the
   line happened to end, so the claim and the evidence for it ran together into one grey slab.
   Blocks put the break where the full stop is. The second sentence is long enough to still take
   two lines of its own, which is fine: what matters is that it starts on a line of its own. */
/* Every sentence is its own block, so the line breaks fall on the full stops rather than
   wherever the measure happens to run out. `> span` and not `span`, so only the sentences are
   blocks. */
.hero-content h1.mast-line > span { display: block; }
.hero-content h1.mast-line > span + span { margin-top: .7em; }

/* Two sets of sentences, not one set with clauses trimmed out of it.

   Trimming got the phone as far as two sentences over four lines, and no further: each sentence
   is its own block, so a block's line count is set by that sentence alone, and reaching three
   lines by trimming needs one of the two to land under 301px. The nearest honest trim came to
   309. Eight pixels. So the narrow screen gets its own three sentences instead, each written to
   sit on one line, and the wide screen keeps the long pair it was written for.

   The cost is that both sets are in the document and anything not running CSS reads all five
   sentences. They are whole grammatical sentences either way, so that reads as repetition
   rather than as the mangled half-sentences an interleaved version would have produced. */
.hero-content h1.mast-line > .mast-narrow { display: none; }

/* The deck. Four cards, four grounds. */
.deck {
  display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px;
  margin-top: clamp(28px, 3.4vw, 48px);
}
.deck-card {
  display: flex; flex-direction: column;
  min-height: 260px; padding: 18px 20px 0;
  border-radius: 16px; text-decoration: none;
  transition: transform .25s var(--ease-out), box-shadow .25s var(--ease-out);
}
.deck-card:hover, .deck-card:focus-visible {
  transform: translateY(-4px);
  box-shadow: 0 14px 30px rgba(20, 38, 29, .18);
}
.deck-tag {
  align-self: flex-start;
  font-family: var(--font-mono); font-size: 9px; letter-spacing: .18em; text-transform: uppercase;
  border: 1px solid currentColor; border-radius: 999px; padding: 5px 11px;
}
.deck-head {
  margin: 20px 0 0; padding-bottom: 20px;
  font-family: var(--font-display); font-weight: 600;
  font-size: clamp(15px, 1.28vw, 19px); line-height: 1.28; letter-spacing: 0;
  color: inherit; text-wrap: pretty;
}
/* The footer bar is pushed down by auto margin, and the hairline is the card's own colour at
   a third strength so it reads on every one of the four grounds without a per-card override. */
.deck-foot {
  margin-top: auto;
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 13px 0; border-top: 1px solid currentColor;
}
.deck-foot em {
  font-family: var(--font-mono); font-style: normal;
  font-size: 9px; letter-spacing: .14em; text-transform: uppercase; opacity: .8;
}
.deck-foot svg {
  width: 28px; height: 28px; padding: 6px; flex: none;
  border: 1px solid currentColor; border-radius: 50%;
}

.deck-cream  { background: var(--cream);  color: var(--ink);    box-shadow: inset 0 0 0 1.5px var(--ink); }
.deck-sand   { background: var(--sand);   color: var(--ink); }
.deck-forest { background: var(--forest); color: var(--cream); }
.deck-plum   { background: var(--plum);   color: var(--cream); }
.deck-cream:hover, .deck-cream:focus-visible { box-shadow: inset 0 0 0 1.5px var(--ink), 0 14px 30px rgba(20, 38, 29, .18); }

.mast-foot {
  display: flex; align-items: center; justify-content: center; gap: 22px;
  margin: clamp(26px, 3vw, 40px) 0 0;
}
.mast-all {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--plum); text-decoration: underline; text-underline-offset: 3px;
}

@media (max-width: 900px) {
  .deck { grid-template-columns: repeat(2, 1fr); gap: 10px; }
  .deck-card { min-height: 210px; padding: 15px 16px 0; }
  .hero-plain .hero-layout { padding-top: 100px; }
  .hero-content h1.mast-line { font-size: 9.5px; letter-spacing: .1em; line-height: 1.8; }
  /* Three short sentences instead of the wide pair. Renderings, drawings and sourcing are not
     named here: the services index sits directly underneath and names all nine, so spending a
     line of the strapline on three of them is repetition a phone cannot afford. */
  .hero-content h1.mast-line > .mast-wide { display: none; }
  .hero-content h1.mast-line > .mast-narrow { display: block; }
  /* The last hidden sentence is still a sibling, so the first visible one would take the .7em
     that separates sentences and open a gap above the block. */
  .hero-content h1.mast-line > .mast-wide + .mast-narrow { margin-top: 0; }

  /* Picture before words on a narrow screen. On a wide one the name, the line and the rendering
     all land in the first view together, so the line earns its place before the picture. On a
     phone they do not: the strapline pushes the only piece of actual work below the fold, and
     what a visitor scrolls past is four lines of grey small caps. The rendering goes first and
     the line reads underneath it.

     .hero-content is already a flex column, so this is order alone, no markup change: the
     heading stays where it is in the document and screen readers still meet the name, then the
     positioning line, then the image.

     The `> *` reset is load bearing. order defaults to 0, so naming only these three would put
     everything else on the page ahead of all of them. */
  .hero-plain .hero-content > * { order: 4; }
  .hero-plain .hero-content > .mast-fit { order: 1; }
  .hero-plain .hero-content > .hero-shot { order: 2; }
  .hero-plain .hero-content > .mast-line { order: 3; }
}
@media (max-width: 560px) {
  .deck { grid-template-columns: 1fr; }
  .deck-card { min-height: 0; }
  .mast-foot { flex-direction: column; gap: 14px; }
}

/* The nav sits transparent over the top of every hero and takes its cream text from the dark
   thing behind it. On the home page that thing is now cream, so the links were cream on cream
   and effectively invisible. Here the frost is on from the first pixel instead of arriving on
   scroll.

   animation: none is not optional. The scrolled nav is driven by a scroll-timeline keyframe
   with fill BOTH, so its 0% value paints over any background-color declared here unless the
   animation itself is cancelled. */
body[data-page="index"] .nav {
  animation: none;
  background-color: var(--nav-frost);
  backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
}

/* The R and A collide at the masthead's tight tracking: R's leg runs straight into A's left
   diagonal, and at 208px that reads as one glyph. letter-spacing applies AFTER each character,
   so putting a looser value on the R alone opens that one pair and leaves every other pair,
   and the overall word width, exactly as measured. Kerning one pair, not re-tracking the word. */
.mast-word .kern-ra { letter-spacing: -.006em; }

/* The photo strips are dragged now, not just scrolled. touch-action: pan-y hands horizontal
   gestures to the script while leaving vertical page scrolling to the browser, which is what
   stops a swipe along the strip from also dragging the page around underneath it.

   user-select and -webkit-user-drag off, because without them a drag across a photograph
   starts a native image drag or paints a selection instead of moving the strip. */
.marquee, .strip { touch-action: pan-y; cursor: grab; }
.marquee.is-dragging, .strip.is-dragging { cursor: grabbing; }
.marquee img, .strip img { user-select: none; -webkit-user-drag: none; }

/* ---------- the name ----------

   A dictionary entry, on the About page. The word is set in the masthead's face and weight so
   it reads as the same mark that runs across the top of the home page, just quieter, and the
   gloss under it is the mono tier used for every other label on the site.

   The gloss separators are drawn with ::before rather than typed into the markup, so the
   dividers never end up in copied text or read out by a screen reader as punctuation. */
/* One rule, under the entry, not a box around it. The section above already closes with a
   rule of its own, so a border on top of this block put two hairlines a short distance apart
   and made the entry look boxed in rather than ruled. */
.namesake {
  border-bottom: 1.5px solid var(--ink);
  padding: 0 0 clamp(12px, 1.4vw, 18px);
  margin-bottom: clamp(28px, 3vw, 44px);
}
.namesake-word {
  margin: 0;
  font-family: Archivo, "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-weight: 700;
  font-size: clamp(46px, 9vw, 132px);
  line-height: .92; letter-spacing: -.03em;
  text-transform: uppercase; color: var(--ink);
}
.namesake-gloss {
  display: flex; flex-wrap: wrap; align-items: baseline;
  margin: 14px 0 0;
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .18em;
  text-transform: uppercase; color: var(--taupe);
}
.namesake-gloss span + span::before { content: "·"; margin: 0 10px; color: var(--sand-line); }
/* The definition itself carries the weight of the section, so it gets the ink and not the taupe. */
.namesake-gloss span:last-child { color: var(--plum); }

/* .section-pad draws a rule above this section and then holds it 112px off the content. The
   entry draws its own rule underneath. Left at the default, those two rules sit a long way
   apart with the word floating between them; pulled in, the pair reads as one ruled entry with
   the word set into it, which is the whole point of the treatment. */
.namesake-sec { padding-top: clamp(18px, 2vw, 28px); }

/* ---------- news columns ----------

   The naming section set as newsprint: a heavy rule across the top, a kicker over a hairline,
   hairlines between the columns, justified text and a drop cap.

   Justified text is only safe here because the columns are wide enough for it and hyphens are
   on. Justification without hyphenation in a narrow measure opens rivers of white space, which
   is the exact thing that makes a fake newspaper look fake. */
.notes {
  display: grid; grid-template-columns: repeat(3, 1fr);
  border-top: 3px solid var(--ink);
  margin-top: clamp(26px, 3vw, 40px);
}
.note {
  padding: 16px clamp(16px, 1.6vw, 24px) 0;
  display: flex; flex-direction: column;
}
.note + .note { border-left: 1px solid var(--rule); }
.note:first-child { padding-left: 0; }
.note:last-child { padding-right: 0; }

.note-tag {
  font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .2em;
  text-transform: uppercase; color: var(--plum);
  padding-bottom: 9px; margin-bottom: 14px;
  border-bottom: 1px solid var(--rule);
}
.note-head {
  font-family: Archivo, "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-weight: 700; font-size: clamp(17px, 1.5vw, 21px);
  line-height: 1.12; letter-spacing: -.015em;
  margin: 0 0 12px; color: var(--ink); text-wrap: balance;
}
.note p {
  margin: 0; font-size: 14px; line-height: 1.62; color: var(--taupe);
  text-align: justify; hyphens: auto; -webkit-hyphens: auto;
}
/* The drop cap is the retro tell. Three lines deep, in the accent, with the right side bearing
   trimmed so the text sets against the letter rather than around a box. */
.note p::first-letter {
  float: left; font-family: Archivo, Helvetica, Arial, sans-serif; font-weight: 700;
  font-size: 3.05em; line-height: .84; padding: 4px 7px 0 0; color: var(--plum);
}
.note-foot {
  margin-top: auto; padding: 14px 0 16px;
  font-family: var(--font-mono); font-size: 9px; letter-spacing: .16em;
  text-transform: uppercase; color: var(--sand-line);
}

@media (max-width: 860px) {
  /* Column rules become horizontal ones. Three newspaper columns on a phone is one word per
     line, and justified text at that measure is unreadable. */
  .notes { grid-template-columns: 1fr; }
  .note { padding: 16px 0 0; }
  .note + .note { border-left: 0; border-top: 1px solid var(--rule); }
  .note p { text-align: left; }
}
/* A strip that is followed by copy needs a gap under it. .strip carries a top margin only,
   which is right where strips stack one after another on the case study, but on the direction
   session the last one runs straight into the next heading.

   Scoped to `.strip + .prose` rather than added to .strip as a bottom margin, so the case
   study's five stacked strips keep their existing rhythm untouched. */
.strip + .prose { margin-top: clamp(40px, 4.5vw, 68px); }

/* ---------- pinned steps ----------

   The index numeral holds while its own step scrolls past it, then the next number takes over.

   `position: sticky` on the numeral, with the STEP as its containing block. That is the whole
   mechanism: a sticky element can only travel inside its own parent, so each numeral is
   released the moment its step ends and the following one arrives underneath it. Nothing is
   listening to scroll.

   top is 116px because the nav is fixed 12px down and 84px tall, so anything smaller pins the
   numeral underneath it. If the nav height changes, this number has to change with it. */
.pin-steps { margin-top: clamp(32px, 4vw, 52px); border-top: 1.5px solid var(--ink); }
.pin-step {
  display: grid; grid-template-columns: minmax(150px, 20%) 1fr;
  gap: clamp(20px, 4vw, 64px); align-items: start;
  padding: clamp(34px, 4.4vw, 64px) 0;
  border-bottom: 1px solid var(--rule);
  /* A sticky numeral can only travel the height of its own step minus its own height. These
     steps are a paragraph each, so without a floor the numeral had about 140px of travel and
     the pin was over before the eye registered it. A 42vh field gives each step roughly half a
     screen, which is what makes the hold read as a hold and the handoff read as a handoff.
     Measured, not guessed: at 950px tall this takes the pin from ~140px of travel to ~330px. */
  min-height: clamp(280px, 42vh, 440px);
}
.pin-n {
  position: sticky; top: 116px; margin: 0;
  font-family: Archivo, "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-weight: 700; font-size: clamp(46px, 7vw, 112px);
  line-height: .8; letter-spacing: -.045em; color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.pin-kicker {
  margin: 0 0 10px;
  font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .2em;
  text-transform: uppercase; color: var(--plum);
}
.pin-body h3 {
  margin: 0; font-weight: 700; font-size: clamp(19px, 1.9vw, 26px); line-height: 1.2;
  color: var(--ink); text-wrap: balance;
}
.pin-body > p:last-child {
  margin: 14px 0 0; max-width: 62ch;
  font-size: 16px; line-height: 1.6; color: var(--taupe); text-wrap: pretty;
}

@media (max-width: 760px) {
  /* One column, and the numeral stops pinning. Stacked, a sticky numeral would sit on top of
     its own paragraph rather than beside it. */
  .pin-step { grid-template-columns: 1fr; gap: 12px; }
  .pin-n { position: static; font-size: 44px; }
}

/* ---------- ledger ----------

   From the AAWU(M) reference: a label column, a hairline grid, and the number given the whole
   width of the page with a small annotation pushed to the right margin.

   The vertical rule is a border-left on the NUMERAL cell, not a line drawn down the container.
   Drawn on the container it would overshoot the first and last rows; on the cell it starts and
   stops exactly where the rows do, which is what makes the grid read as ruled paper.

   The numeral is Archivo at 700, the same face and weight as the ASTRATTO masthead, so the two
   big-type moments on the site are recognisably the same voice. */
.ledger { margin-top: clamp(28px, 3.4vw, 44px); border-bottom: 1px solid var(--rule); }
/* That margin exists to clear a heading above the ledger. When the ledger IS the first
   thing in its section it stacks on the section padding instead, and the two together
   opened a gap taller than the first row. */
.ledger:first-child { margin-top: 0; }
.ledger-row {
  display: grid;
  grid-template-columns: minmax(150px, 18%) 1fr minmax(130px, 17%);
  align-items: start;
  border-top: 1px solid var(--rule);
}
.ledger-label {
  margin: 0; padding: 20px 20px 20px 0;
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .16em;
  text-transform: uppercase; color: var(--taupe);
}
.ledger-num {
  margin: 0; padding: 6px 24px 14px;
  border-left: 1px solid var(--rule);
  font-family: Archivo, "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-weight: 700; font-size: clamp(52px, 9vw, 132px);
  line-height: .96; letter-spacing: -.045em; color: var(--ink);
  font-variant-numeric: tabular-nums;
}
/* The annotation sits on the same baseline as the label, not the numeral: it is a footnote to
   the row, and centring it against a 132px figure would make it look like a caption to the
   number alone. The dot is the reference's right-margin marker. */
.ledger-note {
  margin: 0; padding: 20px 0;
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--taupe);
}
.ledger-note::after {
  content: ""; flex: none; align-self: center;
  width: 7px; height: 7px; border-radius: 50%; background: var(--plum);
}

@media (max-width: 760px) {
  /* Label above, number below, annotation under that. The three-column grid needs width to
     read, and squeezed narrow the numeral would set at about four characters per line. */
  .ledger-row { grid-template-columns: 1fr; }
  .ledger-label { padding: 16px 0 0; }
  .ledger-num { border-left: 0; padding: 2px 0 6px; font-size: clamp(44px, 15vw, 84px); }
  .ledger-note { padding: 0 0 16px; }
}

/* ---------- catalogue leaf ----------

   From the MoMA booklet reference: a right aligned title stack over a rule, the artefact on a
   diagonally clipped field, then a small bold label sitting over the body copy.

   The diagonal field the reference uses was tried and removed: under a stack of pages it read
   as a second object competing with them rather than as a ground. What carries the reference
   here is the title stack and the label-over-copy foot, not the wedge. */
.cat-leaf {
  --leaf-pad: clamp(18px, 2.4vw, 34px);
  margin-top: 32px;
  background: var(--paper);
  border: 1.5px solid var(--ink);
  padding: var(--leaf-pad);
  /* No extrusion. The site's other panels throw a hard ink shadow, but this one is a page: it
     lies flat, and lifting it off the paper fought the thing it is trying to look like. */
}

/* The title stack is right aligned and the rule runs the full width, which is what makes it
   read as a masthead on a page rather than as a heading in a box. */
.cat-leaf-head {
  text-align: right;
  padding-bottom: 14px; margin-bottom: var(--leaf-pad);
  border-bottom: 1.5px solid var(--ink);
}
.cat-leaf-title {
  margin: 0;
  font-family: Archivo, "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-weight: 700; font-size: clamp(20px, 2.1vw, 30px);
  letter-spacing: .01em; text-transform: uppercase; color: var(--ink); line-height: 1;
}
/* The subtitle is the same face at a lighter grade, not a different family. On the reference
   the two lines are obviously one voice at two weights, and that is what stops the stack
   reading as a heading plus a caption. */
.cat-leaf-sub {
  margin: 6px 0 0;
  font-family: Archivo, "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-weight: 400; font-size: clamp(16px, 1.7vw, 23px);
  letter-spacing: 0; color: var(--ink); line-height: 1.15;
}
.cat-leaf-cap {
  margin: 9px 0 0;
  font-family: var(--font-mono); font-size: 9px; letter-spacing: .16em;
  text-transform: uppercase; color: var(--taupe);
}

/* No coloured field behind the spread. The page block below already carries the depth, and a
   tinted wedge under it read as a second thing competing with the pages rather than as a
   ground. The stage is now only spacing: enough room on the right and bottom for the stack to
   lean out without touching the plate's border. */
.cat-leaf-stage { padding: 0; }
.cat-leaf-stage .flip { margin-top: 0; }
.cat-leaf-stage .flip-bar { padding-bottom: 0; }

/* A keyline and nothing else. The stacked sheets that were here read as ragged lines down the
   outside edge rather than as a page block, so the depth came off and the outline stayed. */
.cat-leaf-stage .flip-frame {
  background: #FFFFFF;
  border: 1.5px solid var(--ink);
  padding: 10px;
}

/* Label and copy side by side, the label in the narrow column, as the reference sets it. */
.cat-leaf-foot {
  display: grid; grid-template-columns: minmax(150px, 22%) 1fr;
  gap: clamp(16px, 3vw, 44px);
  margin-top: var(--leaf-pad);
}
.cat-leaf-label {
  margin: 0; font-weight: 700; font-size: 12px; line-height: 1.45; color: var(--ink);
}
.cat-leaf-foot > p:last-child {
  margin: 0; max-width: 62ch;
  font-size: 13px; line-height: 1.6; color: var(--taupe);
}

@media (max-width: 700px) {
  .cat-leaf-head { text-align: left; }
  .cat-leaf-foot { grid-template-columns: 1fr; gap: 10px; }
}
/* ---------- shopfront elevation ----------

   A drawing, not a picture. One weight of line, one colour, no shading.

   Everything in the SVG strokes and fills with currentColor, so the whole thing recolours from
   the single `color` declaration here. vector-effect: non-scaling-stroke keeps every line the
   same weight at every size, which is the point of a line drawing: without it the strokes
   thicken and thin with the viewport and it stops reading as drafted. */
.elev-plate {
  margin: clamp(40px, 5vw, 72px) auto 0;
  max-width: 620px;
  background: var(--paper);
  border: 1.5px solid var(--ink);
  padding: clamp(20px, 3vw, 40px) clamp(16px, 2.4vw, 34px) clamp(14px, 2vw, 22px);
}
.elev-draw {
  display: block; width: 100%; height: auto;
  color: var(--plum);
}
.elev-draw [stroke] { vector-effect: non-scaling-stroke; }

/* SVG text does not inherit the page's font stack, so each label states its own. */
.elev-word {
  font-family: Archivo, "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-weight: 700; font-size: 74px; letter-spacing: -.02em;
}
/* Knocked out of the filled door sign, so it takes the plate colour rather than the ink. */
.elev-door {
  font-family: Archivo, "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-weight: 700; font-size: 17px; letter-spacing: .04em;
  fill: var(--paper);
}
.elev-line, .elev-tagtext text {
  font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  fill: currentColor;
}
.elev-line { font-size: 12px; letter-spacing: .12em; }
.elev-tagtext text { font-size: 11px; letter-spacing: .08em; }

/* The exterior variant: the same drawing language inverted onto the forest ground, because a
   line drawing reads as a DRAWING when the line is the only thing on the field. On paper the
   plum line competes with the paper's warmth; on forest, a cream line is the only mark there.

   Everything else is inherited, including non-scaling-stroke, so the hairline holds its weight
   at any width. */
.elev-plate.is-dark {
  background: var(--forest);
  border-color: var(--forest);
  max-width: 720px;
}
.elev-plate.is-dark .elev-draw { color: var(--cream); }
.elev-plate.is-dark figcaption {
  border-top-color: rgba(249, 240, 222, .28);
  color: rgba(249, 240, 222, .72);
}
.elev-plate.is-dark .sheet-led { background: var(--tan); }
/* The slat field is one <path> repeated by CSS rather than 30 hand-written lines: the count and
   the rhythm are then a single number to change, and the markup stays readable. */
.elev-slats line { stroke: currentColor; stroke-width: 1; }

.elev-plate figcaption {
  display: flex; align-items: center; gap: 9px;
  margin-top: clamp(14px, 2vw, 22px); padding-top: 12px;
  border-top: 1px solid var(--rule);
  font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .16em;
  text-transform: uppercase; color: var(--taupe);
}

/* ---------- services menu ----------

   From the bar-card reference: a bordered card, a centred head over a rule, two columns split
   by a vertical rule, each block labelled down its left edge, and a note across the foot.

   The reference is a PRICE list. This one carries no prices, because the studio does not
   publish rates and the pricing page says so in as many words. The right hand column gives the
   DELIVERABLE instead, which is the other thing anybody scanning a menu wants and which is
   true.

   The rotated block labels are writing-mode, not transform. A transform leaves the label's
   layout box horizontal, so the grid column would size to the wrong axis and the label would
   sit on top of the list rather than beside it. */
/* The card carries no top margin of its own because on /services it is the first thing in its
   section. On /process it follows a paragraph, where it sat hard against the text. Scoped to
   the not-first case so the services page is untouched. */
/* Sibling-keyed, not :not(:first-child): on /services the blade sign occupies the
   first-child slot, so the only card on the page was reading as the second one and
   stacking 40px on top of the section padding. */
.menu-card + .menu-card { margin-top: clamp(24px, 3vw, 40px); }
.menu-card {
  max-width: 860px; margin: 0 auto;
  background: var(--sand);
  border: 1.5px solid var(--ink);
  border-radius: 14px;
  padding: clamp(20px, 3vw, 40px);
  color: var(--ink);
}
.menu-head { text-align: center; padding-bottom: clamp(16px, 2.4vw, 28px); }
.menu-title {
  margin: 0;
  font-family: Archivo, "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-weight: 700; font-size: clamp(26px, 3.6vw, 44px);
  letter-spacing: .22em; text-transform: uppercase; line-height: 1;
}
.menu-sub {
  margin: 12px 0 0;
  font-size: 13px; letter-spacing: .04em; color: var(--taupe);
}

.menu-cols {
  display: grid; grid-template-columns: 1fr 1fr;
  border-top: 1.5px solid var(--ink);
}
/* The dividing rule belongs to the right column, so it runs exactly the height of the taller
   of the two and stops there, the way a printed rule does. */
.menu-stack { border-left: 1.5px solid var(--ink); display: grid; align-content: start; }
.menu-stack .menu-block + .menu-block { border-top: 1.5px solid var(--ink); }

.menu-block {
  display: grid; grid-template-columns: 34px 1fr;
  gap: clamp(10px, 1.4vw, 18px);
  padding: clamp(18px, 2.4vw, 30px) clamp(14px, 1.8vw, 24px);
}
.menu-label {
  writing-mode: vertical-rl; transform: rotate(180deg);
  margin: 0; justify-self: center;
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .24em;
  text-transform: uppercase; color: var(--ink);
}
.menu-block ul { list-style: none; margin: 0; padding: 0; display: grid; gap: clamp(14px, 1.8vw, 22px); }
/* Name and deliverable on one line, description under it, which is the reference's item shape
   with the price column doing a different job. */
.menu-block li {
  display: grid; grid-template-columns: 1fr auto; column-gap: 14px; align-items: baseline;
}
.menu-block li a { grid-column: 1; display: block; color: inherit; text-decoration: none; }
.menu-name {
  display: block; font-weight: 700; font-size: 12.5px; letter-spacing: .1em;
  text-transform: uppercase; line-height: 1.3;
}
.menu-desc {
  display: block; margin-top: 5px; max-width: 40ch;
  font-size: 12px; line-height: 1.5; color: var(--taupe);
}
.menu-block li a:hover .menu-name { text-decoration: underline; text-underline-offset: 3px; }
.menu-out {
  grid-column: 2; grid-row: 1;
  font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--plum); white-space: nowrap;
}

.menu-foot {
  margin: 0; padding-top: clamp(16px, 2.4vw, 26px);
  border-top: 1.5px solid var(--ink);
  text-align: center; font-family: var(--font-mono); font-size: 10px;
  letter-spacing: .14em; text-transform: uppercase; line-height: 2; color: var(--taupe);
}

@media (max-width: 760px) {
  /* One column, and the labels lie down. A vertical label in a 34px gutter beside a 40 character
     description is taller than the list it is labelling. */
  .menu-cols { grid-template-columns: 1fr; }
  .menu-stack { border-left: 0; border-top: 1.5px solid var(--ink); }
  .menu-block { grid-template-columns: 1fr; gap: 12px; }
  .menu-label { writing-mode: horizontal-tb; transform: none; justify-self: start; }
}

/* ---------- work filter ----------

   A row per facet, chips inside it. Laid out as a grid with a fixed label column so the three
   rows' chips all start on the same vertical line, which is what makes it scan as a set of
   choices rather than three unrelated lines of buttons. */
/* The filter is the first thing in its section, so two spacings were stacking: section-pad's
   full top space AND the filter's own top margin, up to 148px of empty cream between the hero
   and the first row of chips. The filter drops its margin and the section comes in.

   :has() rather than a class on the page, so it applies wherever a filter is the lead element
   and nowhere else. */
.workfilter {
  margin: 0 0 clamp(8px, 1.4vw, 16px);
  border-top: 1.5px solid var(--ink);
}
.section-pad:has(> .workfilter) {
  padding-top: var(--pad-continuous);
  /* The sign hangs 72px down from its section by default, which left it floating below the
     filter it sits beside. Tying --sign-top to the same value as the section's own top padding
     puts the top of the sign on the filter's first rule. The two clamps have to stay equal. */
  --sign-top: var(--pad-continuous);
}

.wf-row {
  display: grid; grid-template-columns: 92px 1fr;
  gap: 14px; align-items: baseline;
  padding: 13px 0;
  border-bottom: 1px solid var(--rule);
}
.wf-label {
  margin: 0;
  font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .18em;
  text-transform: uppercase; color: var(--taupe);
}
.wf-chips { display: flex; flex-wrap: wrap; gap: 7px; }
.wf-chip {
  font: inherit; cursor: pointer;
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .12em; text-transform: uppercase;
  color: var(--ink); background: transparent;
  border: 1px solid var(--rule); border-radius: 999px;
  padding: 6px 13px;
  transition: background-color .18s var(--ease-out), color .18s var(--ease-out),
              border-color .18s var(--ease-out);
}
.wf-chip:hover { border-color: var(--ink); }
/* The selected state is a fill, not a border change. A chosen filter has to be obvious at a
   glance across three rows, and a heavier outline is not. */
.wf-chip.on { background: var(--plum); border-color: var(--plum); color: var(--cream); }

/* The chips are on/off controls, so they are drawn as switches: a track on the left of each
   chip, and a knob that travels to the far side when the filter comes on.

   The fill stays. Across three rows the fill is what makes a selection readable at a glance,
   and on its own it only says "highlighted"; the knob is what says "switched on". The two
   are doing different jobs, so both are kept.

   Both parts inherit currentColor, so the switch recolours itself with the chip: ink on the
   cream chip, cream on the plum one, with no second set of colours to keep in step. */
.wf-chip { position: relative; padding-left: 36px; }
.wf-chip::before,
.wf-chip::after { content: ""; position: absolute; top: 50%; pointer-events: none; }
.wf-chip::before {
  left: 9px; width: 20px; height: 10px; margin-top: -5px;
  border: 1.2px solid currentColor; border-radius: 999px; opacity: .45;
  transition: opacity .18s var(--ease-out), background-color .18s var(--ease-out);
}
.wf-chip::after {
  left: 11px; width: 6px; height: 6px; margin-top: -3px;
  border-radius: 50%; background: currentColor; opacity: .55;
  transition: left .24s var(--ease-out), opacity .18s var(--ease-out);
}
.wf-chip:hover::before, .wf-chip:hover::after { opacity: .8; }
.wf-chip.on::before { opacity: 1; background: rgba(249, 240, 222, .2); }
.wf-chip.on::after,
.wf-chip.on:hover::after { left: 21px; opacity: 1; }
/* The travel is the whole point of the control, so it is the one thing that still reads with
   motion off: the knob lands in the on position, it just does not slide there. */
@media (prefers-reduced-motion: reduce) {
  .wf-chip::before, .wf-chip::after { transition: none; }
}

.wf-foot {
  display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
  margin: 14px 0 0;
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--taupe);
}
.wf-clear {
  font: inherit; cursor: pointer; color: var(--plum); background: none; border: 0; padding: 0;
  text-decoration: underline; text-underline-offset: 3px;
}
.wf-cross { margin-left: auto; }
.wf-cross a { color: var(--plum); text-decoration: underline; text-underline-offset: 3px; }

/* A filtered-out card is hidden with the HTML attribute, so it leaves the layout entirely and
   the grid closes up. Opacity would leave holes in it. */
.pf-grid > [hidden] { display: none; }

@media (max-width: 620px) {
  .wf-row { grid-template-columns: 1fr; gap: 8px; }
  .wf-cross { margin-left: 0; width: 100%; }
}
/* ---------- drawer ----------

   The full menu as a panel on the right: big type, a hairline between every row, groups that
   expand in place with a count and a plus.

   It supersedes the old .mobile-menu and is available at EVERY width, not only on a phone. The
   bar shows four destinations; this shows all twenty-three, which is the only reason to have
   it. Its trigger keeps the id navToggle so the existing open/close and scroll-lock script
   drives it unchanged. */
.drawer {
  position: fixed; z-index: 80;
  top: var(--page-edge); right: var(--page-edge); bottom: var(--page-edge);
  width: min(560px, calc(100% - var(--page-edge) * 2));
  background: var(--cream); color: var(--ink);
  border: 1.5px solid var(--ink);
  padding: clamp(16px, 2.2vw, 28px);
  overflow-y: auto;
  /* Contain, so a flick that reaches the end of the drawer does not start scrolling the page
     behind it. The script also locks the body, but this stops the rubber-band before it. */
  overscroll-behavior: contain;
  box-shadow: -30px 0 70px rgba(20, 38, 29, .3);
}
.drawer[hidden] { display: none; }

.drawer-top {
  display: flex; align-items: center; justify-content: space-between; gap: 16px;
  padding-bottom: clamp(28px, 4vw, 56px);
}
.drawer-close {
  font: inherit; cursor: pointer;
  display: flex; align-items: center; gap: 7px;
  font-family: var(--font-mono); font-size: 11px; letter-spacing: .12em; text-transform: uppercase;
  color: var(--ink); background: var(--sand); border: 1.5px solid var(--ink);
  padding: 7px 12px;
}
.drawer-close:hover { background: var(--ink); color: var(--cream); }
.drawer-start {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--plum); text-decoration: underline; text-underline-offset: 3px;
}

.drawer-row { border-top: 1.5px solid var(--ink); }
.drawer-row:last-child { border-bottom: 1.5px solid var(--ink); }

/* One rule for the plain rows and the group triggers, so a link and a button that look
   identical actually are identical.

   BOTH forms of the plain row are listed. A plain destination is <a class="drawer-row">, which
   IS the row; a group is <div class="drawer-row"> holding a trigger. `.drawer-row > a` only
   matches the second, so the plain links were falling through unstyled and setting inline at
   body size, three to a line. */
a.drawer-row,
.drawer-row > a,
.drawer-head > .drawer-name {
  display: flex; align-items: center; gap: 6px; width: 100%;
  padding: clamp(10px, 1.4vw, 16px) 0;
  font-family: Archivo, "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-weight: 700; font-size: clamp(23px, 3vw, 36px); line-height: 1.1; letter-spacing: -.02em;
  color: var(--ink); text-decoration: none;
  background: none; border: 0; text-align: left; cursor: pointer;
}
a.drawer-row:hover, .drawer-row > a:hover, .drawer-head > .drawer-name:hover { color: var(--plum); }

/* A group row is a destination AND an expander: the name is a link in the same big type as
   the plain rows, and the count with its plus sits to the right as a separate control. Before
   this the whole row was one button, so the label had to be a category rather than a page and
   the real page was a tap further in. */
.drawer-head { display: flex; align-items: center; gap: 14px; }
.drawer-head > .drawer-name { flex: 1 1 auto; }
.drawer-trigger {
  flex: none; display: flex; align-items: center; gap: 8px;
  padding: clamp(10px, 1.4vw, 16px) 0 clamp(10px, 1.4vw, 16px) 10px;
  font: inherit; font-family: var(--font-mono); font-size: 12px; letter-spacing: .08em;
  color: var(--taupe); background: none; border: 0; cursor: pointer;
}
.drawer-trigger:hover { color: var(--plum); }
.drawer-trigger sup { top: 0; font-size: 1em; font-weight: 400; }

/* The plus and minus are drawn from two bars, and open just hides the upright one. A glyph
   would sit on a different baseline in every font and jump as it changed. */
.drawer-mark {
  position: relative; flex: none;
  width: 16px; height: 16px;
}
.drawer-mark::before, .drawer-mark::after {
  content: ""; position: absolute; inset: 0; margin: auto;
  background: currentColor;
}
.drawer-mark::before { width: 100%; height: 2px; }
.drawer-mark::after  { width: 2px; height: 100%; transition: opacity .2s var(--ease-out); }
.drawer-trigger[aria-expanded="true"] .drawer-mark::after { opacity: 0; }

.drawer-panel {
  display: grid; grid-template-columns: 1fr 1fr;
  gap: 6px clamp(16px, 3vw, 40px);
  padding: 4px 0 clamp(18px, 2.4vw, 28px);
}
.drawer-panel[hidden] { display: none; }
.drawer-panel a {
  font-size: 15px; line-height: 1.5; color: var(--taupe); text-decoration: none;
}
.drawer-panel a:hover { color: var(--ink); }

/* The trigger is a labelled button now, not three bars, because it is no longer a phone-only
   hamburger. The bars stay as the mark beside the word. */
.nav-toggle .bars { display: flex; flex-direction: column; gap: 4px; }
.nav-toggle em {
  font-style: normal;
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink);
}
.nav-toggle:hover { background: var(--ink); }
.nav-toggle:hover .bars span { background: var(--cream); }
.nav-toggle:hover em { color: var(--cream); }
@media (max-width: 560px) {
  .drawer-panel { grid-template-columns: 1fr; }
}

/* ---------- form plate ----------

   The contact form as a ruled card, from the reference: a heavy border, the label sitting ON
   the rule above its own field rather than floating beside it, cells divided by hairlines, and
   a solid bar for the send.

   The rules come from borders on the CELLS, not lines drawn on the plate. That is what makes
   the grid close up correctly when a row goes full width, and it is why a cell can be moved
   between one and two columns without any line being redrawn.

   Inputs are borderless and transparent. The cell already draws the box; an input with its own
   border inside a ruled cell is two boxes, which is the mistake that makes a form like this
   look like a form pretending to be a card. */
.formplate {
  margin-top: 26px;
  background: var(--paper);
  border: 6px solid var(--ink);
}
.fp-head {
  display: flex; align-items: center; justify-content: space-between; gap: 20px;
  padding: clamp(14px, 1.8vw, 22px) clamp(14px, 1.8vw, 22px) clamp(20px, 3vw, 34px);
}
.fp-tag {
  background: var(--ink); color: var(--cream);
  padding: 8px 14px;
  font-family: var(--font-mono); font-size: 11px; letter-spacing: .18em; text-transform: uppercase;
}
.fp-title {
  font-family: Archivo, "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-weight: 700; font-size: clamp(30px, 4.4vw, 56px); line-height: .9;
  letter-spacing: -.03em; text-transform: uppercase; color: var(--ink);
}

.fp-grid { display: grid; grid-template-columns: 1fr 1fr; }
.fp-cell {
  display: flex; flex-direction: column;
  border-top: 1.5px solid var(--ink);
  padding: 8px clamp(12px, 1.6vw, 18px) 12px;
  min-width: 0;
}
/* The vertical rule belongs to the right-hand cell of a pair, so it appears only where two
   cells actually sit side by side and never dangles under a full-width one. */
.fp-cell:nth-child(2n) { border-left: 1.5px solid var(--ink); }
.fp-wide { grid-column: 1 / -1; border-left: 0; }

.fp-cell label {
  font-family: var(--font-mono); font-size: 11px; font-weight: 700;
  letter-spacing: .1em; color: var(--ink);
  padding-bottom: 6px;
}
.fp-cell input,
.fp-cell select,
.fp-cell textarea {
  font: inherit; font-size: 15px; color: var(--ink);
  background: transparent; border: 0; padding: 4px 0; width: 100%;
  border-radius: 0;
}
.fp-cell textarea { min-height: 116px; resize: vertical; line-height: 1.5; }
.fp-cell input:focus, .fp-cell select:focus, .fp-cell textarea:focus {
  outline: 2px solid var(--plum); outline-offset: 3px;
}
.fp-cell ::placeholder { color: var(--taupe); opacity: .75; }

.fp-send {
  display: flex; align-items: center; justify-content: center; gap: 14px;
  width: 100%; border: 0; border-top: 1.5px solid var(--ink);
  background: var(--ink); color: var(--cream);
  padding: clamp(16px, 2.2vw, 24px);
  font-family: var(--font-mono); font-size: 13px; letter-spacing: .24em;
  text-transform: uppercase; cursor: pointer;
  transition: background-color .2s var(--ease-out);
}
.fp-send:hover { background: var(--plum); }

@media (max-width: 560px) {
  .fp-grid { grid-template-columns: 1fr; }
  .fp-cell:nth-child(2n) { border-left: 0; }
}

/* With the inline links gone the bar has three children, and space-between stranded the call
   to action in the middle of an empty bar. Pushing it right groups it with the menu button, so
   the bar reads as wordmark on the left and controls on the right. */
.nav .btn-nav { margin-left: auto; }
.nav { gap: 14px; }

/* ---------- nav pills ----------

   The four shortcut links and the Menu button, set as the same pill the work filter uses.

   Same shape as the work filter's chips: a pill on a hairline. Filled cream rather than left
   transparent, because on the page those chips sit on cream while here the bar is aubergine,
   and an outline-only pill on a dark ground is a different object entirely.

   The Menu button shares the rule. It is the fifth control in the same row and had no business
   looking like a sixth idea. */
.nav-links { margin-inline: auto; gap: 7px; }
.nav-links a,
.nav-toggle {
  border-radius: 999px;
  border: 1px solid var(--sand-line);
  background: var(--cream);
  color: var(--ink);
  padding: 8px 15px;
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .12em;
  text-transform: uppercase; font-weight: 500;
  transition: background-color .2s var(--ease-out), color .2s var(--ease-out),
              border-color .2s var(--ease-out);
}
/* Hover pulls the cell to the dark end of the same palette rather than lifting or underlining
   it, so the row keeps its printed, un-animated character. */
.nav-links a:hover, .nav-links a:focus-visible,
.nav-toggle:hover, .nav-toggle:focus-visible { background: var(--forest); border-color: var(--forest); color: var(--cream); }

/* The current page's cell is filled in the accent. The old marker was a translucent cream wash,
   which is invisible against a cell that is already filled. */
body[data-page="services"] .nav-links [data-nav="services"],
body[data-page="virtual-interior-design"] .nav-links [data-nav="services"],
body[data-page="virtual-exterior-design"] .nav-links [data-nav="services"],
body[data-page="3d-photorealistic-renderings"] .nav-links [data-nav="services"],
body[data-page="2d-floor-plans"] .nav-links [data-nav="services"],
body[data-page="2d-elevations-millwork"] .nav-links [data-nav="services"],
body[data-page="sourcing"] .nav-links [data-nav="services"],
body[data-page="video-tour"] .nav-links [data-nav="services"],
body[data-page="vr-walkthrough"] .nav-links [data-nav="services"],
body[data-page="direction-session"] .nav-links [data-nav="services"],
body[data-page="portfolio"] .nav-links [data-nav="portfolio"],
body[data-page="commercialportfolio"] .nav-links [data-nav="portfolio"],
body[data-page="case-studies"] .nav-links [data-nav="portfolio"],
body[data-page="tada-case-study"] .nav-links [data-nav="portfolio"],
body[data-page="accuracy-pass"] .nav-links [data-nav="portfolio"],
body[data-page="how-it-works"] .nav-links [data-nav="how-it-works"],
body[data-page="pricing"] .nav-links [data-nav="how-it-works"],
body[data-page="faq"] .nav-links [data-nav="how-it-works"],
body[data-page="information"] .nav-links [data-nav="information"] {
  background: var(--plum); color: var(--cream);
}

@media (max-width: 1100px) {
  /* The shortcuts go and the Menu button carries the whole navigation. Four cells, a call to
     action and a menu button do not fit a narrow bar, and the drawer already holds all of it. */
  .nav-links { display: none; }
}

/* ---------- roster ----------

   The team as a ruled list: name, role, tenure. Three columns on a hairline, the same grid
   language as the ledger and the work filter, so the About page reads as one document rather
   than three devices.

   Not portrait cards. There are no portraits of the team, and a card carrying an initial in a
   circle is a worse answer than a name on a line. */
.roster {
  margin-top: var(--pad-continuous);
  border-bottom: 1px solid var(--rule);
}
/* A fixed portrait column, whether or not the row has one. The column holds its width so every
   name starts on the same line: sizing it to content would step the two photo-less rows left
   and turn a list into a ragged edge. */
.roster-row {
  display: grid;
  grid-template-columns: 64px minmax(150px, 24%) 1fr minmax(140px, 20%);
  gap: 6px clamp(14px, 2.4vw, 34px);
  align-items: center;
  padding: clamp(12px, 1.6vw, 18px) 0;
  border-top: 1px solid var(--rule);
}
/* The name owns column two whether a portrait was supplied or not, so a missing photo leaves a
   gap rather than shunting the whole row across. */
.roster-name { grid-column: 2; }
.roster-face {
  grid-column: 1; grid-row: 1;
  width: 64px; height: 80px; object-fit: cover; display: block;
  border: 1.5px solid var(--ink);
}
.roster-name {
  margin: 0; grid-column: 2;
  font-family: Archivo, "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-weight: 700; font-size: clamp(17px, 1.7vw, 22px); line-height: 1.2;
  letter-spacing: -.01em; color: var(--ink);
}
.roster-role { margin: 0; font-size: 14px; line-height: 1.5; color: var(--taupe); }
/* Tenure is the column that makes the section mean something, so it takes the accent rather
   than the taupe the role sits in. */
.roster-since {
  margin: 0; text-align: right;
  font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--plum);
}
@media (max-width: 700px) {
  /* Portrait beside the name, everything else stacked under it. */
  .roster-row { grid-template-columns: 56px 1fr; gap: 4px 14px; }
  .roster-face { width: 56px; height: 70px; grid-row: 1 / span 3; }
  .roster-name, .roster-role, .roster-since { grid-column: 2; }
  .roster-since { text-align: left; }
}

/* Optical centring for the extruded cards.

   They are already centred to the pixel: 120px of band on each side, measured. But every card
   throws its hard shadow 20px to the LEFT, so the visual block is 20px wider on that side and
   the whole group reads as sitting left of centre.

   Shifting by half the overhang puts the block's optical centre on the band's centre. It is a
   transform rather than a margin so nothing about the layout or the grid changes: this is a
   correction to what the eye sees, not to where the boxes are. */
.section-band > .container > .card-grid,
.section-band > .container > .card { transform: translateX(10px); }

/* ---------- build-up sequence ----------
   Frames stacked in one 16:9 box and cross-faded, rather than a carousel that slides. The
   room is the SAME room in every frame, so anything that moves the picture sideways fights
   what the sequence is showing: sliding says "next photo", dissolving says "same place,
   later". Nothing but opacity animates.

   The box is sized by aspect-ratio, not by the images, so it never collapses while the later
   frames are still loading and the page does not jump under the reader. */
.buildup { margin: clamp(24px, 3vw, 40px) 0 0; }
.bu-stage {
  position: relative; aspect-ratio: 16 / 9; overflow: hidden;
  background: var(--sand); border: 1.5px solid var(--ink);
}
/* Frames STACK, they do not cross-fade.

   Cross-fading looked wrong and the reason is arithmetic: halfway through, the outgoing frame
   and the incoming one are both at partial opacity, so the ground shows through the pair and
   the whole picture washes pale for a moment. Two half-transparent images never add back up to
   one opaque one.

   Every frame here is the same room plus more objects, so nothing needs to fade OUT. Each
   frame stays fully opaque and the next one fades in ON TOP of it. There is always a solid
   image underneath, the picture never lightens, and what the eye sees is objects arriving in a
   room that is otherwise perfectly still.

   Stacking order is DOM order: absolutely positioned siblings paint later-over-earlier on
   their own, so no z-index is needed and the markup order IS the build order. */
.bu-frame {
  position: absolute; inset: 0;
  width: 100%; height: 100%; object-fit: cover; display: block;
  opacity: 0; transition: opacity .24s var(--ease-out);
}
.bu-frame.is-on { opacity: 1; }
/* Looping back to the empty room has to be instant. Fading four stacked layers out at once
   would undress the room in slow motion, which is the sequence running backwards. */
.buildup.is-resetting .bu-frame { transition: none; }

.bu-bar {
  display: flex; align-items: center; gap: clamp(12px, 2vw, 20px);
  margin-top: 14px;
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--taupe);
}
.bu-play {
  flex: none; width: 30px; height: 30px; padding: 0; cursor: pointer;
  background: transparent; border: 1px solid var(--rule); border-radius: 50%;
  display: grid; place-items: center; color: var(--ink);
  transition: border-color .18s var(--ease-out), background-color .18s var(--ease-out);
}
.bu-play:hover { border-color: var(--ink); background: var(--sand); }
/* Two bars for pause; the same two bars become a triangle for play by collapsing one of them
   and clipping the other, so there is no icon swap and nothing to keep in sync. */
.bu-play-icon { width: 9px; height: 10px; border-left: 3px solid currentColor; border-right: 3px solid currentColor; }
.bu-play[aria-label^="Play"] .bu-play-icon {
  width: 0; height: 0; border-left: 9px solid currentColor; border-right: 0;
  border-top: 6px solid transparent; border-bottom: 6px solid transparent;
  margin-left: 3px;
}
.bu-dots { display: flex; gap: 7px; }
.bu-dot {
  width: 9px; height: 9px; padding: 0; cursor: pointer;
  border: 1px solid var(--rule); border-radius: 50%; background: transparent;
  transition: background-color .2s var(--ease-out), border-color .2s var(--ease-out);
}
.bu-dot:hover { border-color: var(--ink); }
.bu-dot.is-on { background: var(--plum); border-color: var(--plum); }
.bu-step { margin-left: auto; color: var(--ink); }

/* Motion off: no auto-advance and no dissolve. The sequence still works, because the dots are
   real buttons and stepping through it by hand shows exactly the same thing. */
@media (prefers-reduced-motion: reduce) {
  .bu-frame { transition: none; }
}
@media (max-width: 620px) {
  .bu-step { display: none; }
}
/* ---------- portfolio switch ----------
   Two cells, one keyline, no gap between them. Drawn as one object rather than two buttons,
   because the point is that these are the two HALVES of the work: a pair of separate pills
   would read as two unrelated links that happen to sit together.

   The counts are load-bearing, not decoration. "Commercial" alone is a label; "Commercial,
   6 projects" tells you whether it is worth the click before you spend it. */
.pfswitch {
  display: grid; grid-template-columns: 1fr 1fr;
  margin: 0 0 clamp(14px, 2vw, 22px);
  border: 1.5px solid var(--ink);
}
.pfswitch-opt {
  display: flex; flex-direction: column; gap: 5px;
  padding: clamp(12px, 1.6vw, 18px) clamp(14px, 2vw, 22px);
  color: var(--ink); text-decoration: none;
  transition: background-color .18s var(--ease-out), color .18s var(--ease-out);
}
.pfswitch-opt + .pfswitch-opt { border-left: 1.5px solid var(--ink); }
.pfswitch-name {
  font-family: var(--font-display); font-weight: 600;
  font-size: clamp(15px, 1.7vw, 21px); line-height: 1.1; letter-spacing: 0;
}
.pfswitch-n {
  font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .16em;
  text-transform: uppercase; color: var(--taupe);
}
/* The side you are NOT on gets the arrow, so the half that is a destination looks like one and
   the half you are standing on does not. */
.pfswitch-opt:not(.is-current) .pfswitch-name::after { content: " \2192"; }
a.pfswitch-opt:hover { background: var(--sand); color: var(--ink); text-decoration: none; }
.pfswitch-opt.is-current { background: var(--plum); color: var(--cream); }
.pfswitch-opt.is-current .pfswitch-n { color: rgba(249, 240, 222, .72); }
@media (max-width: 560px) {
  .pfswitch-n { font-size: 9px; letter-spacing: .12em; }
}
/* ---------- hero palette plate ----------
   One image, gridded, with some cells replaced by solid colour and labelled. The tiles are not
   decoration: they are the studio's actual palette, with the real names and the real hex values
   that are in this stylesheet. A swatch card that invents paint codes is a prop; this one can be
   checked against the site it sits on.

   The grid is the same one in both layers, so a tile lands exactly on the photo's cell edges and
   the hairlines run unbroken across image and swatch alike. That alignment IS the effect. */
/* The plain hero image that replaced the gridded palette plate. Same frame, same border, same
   16:9 box, so the page's rhythm is unchanged: only the grid of service tiles came off. The
   plate rules below are kept because the component is PARKED, not deleted, at
   _build/parked-palette-plate.md. */
.hero-shot {
  position: relative; margin: clamp(34px, 4vw, 56px) 0 0;
  border: 1.5px solid var(--ink); overflow: hidden;
  aspect-ratio: 16 / 9;
}
.hero-shot > img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: block;
}
.hero-content h1.mast-line + .hero-shot { margin-top: clamp(22px, 2.6vw, 34px); }

/* What we do, said plainly under the picture. These are the nine links the tiles used to carry,
   so taking the grid off does not cost the homepage its route into the service pages.
   A wrapped flex row, not a grid: the names are very different lengths and a grid would strand
   one of them alone on a line at most widths. */
.hero-services {
  display: flex; flex-wrap: wrap; align-items: baseline; justify-content: center; text-align: center;
  gap: 10px clamp(14px, 1.8vw, 24px);
  margin-top: clamp(18px, 2.2vw, 26px);
}
.hero-services a {
  font-family: var(--font-mono); font-size: 11px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--ink); text-decoration: none;
  padding-bottom: 2px; border-bottom: 1.5px solid transparent;
  transition: border-color .18s var(--ease-out), color .18s var(--ease-out);
}
.hero-services a:hover, .hero-services a:focus-visible {
  color: var(--plum); border-bottom-color: var(--pink); text-decoration: none;
}
/* The separator is drawn, not typed, so it is never read aloud or copied with the text.
   It sits AFTER each item rather than before the next one. On a `::before` the dot lands at the
   START of a wrapped line, where it reads as a bullet rather than a separator; trailing it
   instead puts it at the end of the line, which reads as "this continues". */
.hero-services a:not(:last-child) { position: relative; }
.hero-services a:not(:last-child)::after {
  content: ""; position: absolute; right: calc(clamp(14px, 1.8vw, 24px) / -2 - 1px);
  top: .4em; width: 3px; height: 3px; border-radius: 50%; background: var(--taupe);
}

/* On a phone the same nine links are an index, not a line of running text.

   Wrapped inline at 375 they broke across five ragged lines with a separator dot stranded at
   the end of each, which reads as a typo rather than as "this continues", and the tap targets
   were 11px of type packed edge to edge. Two columns with a rule under each row: nothing wraps,
   nothing is stranded, and every row is a real target.

   Elevations and Millwork is the one label too long for half the measure, so it takes the whole
   width. Named by href rather than by :nth-child so that renaming or reordering the list cannot
   silently point this at the wrong item. */
@media (max-width: 720px) {
  .hero-services {
    display: grid; grid-template-columns: 1fr 1fr;
    gap: 0 clamp(14px, 4vw, 22px);
    margin-top: clamp(20px, 5vw, 28px);
    border-top: 1px solid var(--rule);
    text-align: left;
  }
  .hero-services a {
    font-size: 9.5px; letter-spacing: .1em;
    padding: 12px 0 11px;
    border-bottom: 1px solid var(--rule);
  }
  .hero-services a[href="/2d-elevations-millwork"] { grid-column: 1 / -1; }
  .hero-services a:not(:last-child)::after { display: none; }
}

.palette-plate {
  position: relative; margin: clamp(34px, 4vw, 56px) 0 0;
  border: 1.5px solid var(--ink); overflow: hidden;
  aspect-ratio: 16 / 9;
}
.palette-plate > img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; display: block;
}
.hero-content h1.mast-line + .palette-plate { margin-top: clamp(22px, 2.6vw, 34px); }
.pp-grid {
  position: absolute; inset: 0;
  display: grid; grid-template-columns: repeat(8, 1fr); grid-template-rows: repeat(5, 1fr);
}
/* The rules sit on the CELLS, not on the tiles, so they carry across the photo too. */
.pp-cell { border-right: 1px solid rgba(249, 240, 222, .30); border-bottom: 1px solid rgba(249, 240, 222, .30); }
.pp-tile {
  display: flex; flex-direction: column; justify-content: flex-end;
  gap: 2px; padding: clamp(7px, .9vw, 13px);
  background: var(--c); color: var(--t);
  font-family: var(--font-mono); letter-spacing: .07em; text-transform: uppercase;
  border-right: 1px solid rgba(249, 240, 222, .30); border-bottom: 1px solid rgba(249, 240, 222, .30);
}
.pp-name { font-size: clamp(8px, .82vw, 11px); font-weight: 500; line-height: 1.25; }
.pp-hex  { font-size: clamp(7.5px, .74vw, 10px); opacity: .74; letter-spacing: .12em; }
/* The tiles are links, so they need a state. The face stays put and the label moves: recolouring
   a tile would break the palette the plate is built out of. */
a.pp-tile { text-decoration: none; transition: box-shadow .18s var(--ease-out); }
a.pp-tile .pp-name { transition: transform .18s var(--ease-out); }
a.pp-tile:hover { text-decoration: none; box-shadow: inset 0 0 0 2px var(--t); }
a.pp-tile:hover .pp-name { transform: translateX(3px); }
a.pp-tile:focus-visible { outline: 3px solid var(--ink); outline-offset: -3px; }
.palette-plate figcaption {
  position: absolute; left: 0; right: 0; bottom: 0;
  display: flex; align-items: center; gap: 9px;
  padding: 9px clamp(10px, 1.4vw, 16px);
  background: var(--ink); color: rgba(249, 240, 222, .8);
  font-family: var(--font-mono); font-size: 9px; letter-spacing: .16em; text-transform: uppercase;
}
/* Below the fold of a phone the labels stop being readable before the tiles stop being pretty,
   so the grid coarsens rather than shrinking the type past legibility. */
@media (max-width: 640px) {
  .pp-grid { grid-template-columns: repeat(4, 1fr); grid-template-rows: repeat(5, 1fr); }
  .pp-tile[data-wide] { grid-column: span 2; }
}
/* ---------- /process spacing ----------
   This page is one continuous explanation of one process: every section carries on from the one
   before it, so every boundary takes the smaller gap and each section sits with the same air
   above it as below.

   Left as a page-level rule rather than a change to what .pad-snug means, because elsewhere the
   asymmetry is doing a job. On /information the team section is deliberately tight on top and
   full underneath: the small gap says "still the same subject", the big one says "a new subject
   starts here". Flattening that globally would delete the signal. */
body[data-page="process"] .section-pad {
  padding-top: var(--pad-continuous);
  padding-bottom: var(--pad-continuous);
}
/* Example links. Every stage and every deliverable on /process points at a real one, because
   "two rounds of changes" and "a sourcing guide" are abstractions until somebody has seen the
   actual artefact. Set small and in mono so they read as a side door out of the text rather
   than competing with it. */
.step-eg { margin-top: 12px; }
.step-eg a {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .12em;
  text-transform: uppercase;
}
.menu-eg {
  display: block; margin-top: 4px; font-style: normal;
  font-family: var(--font-mono); font-size: 9px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--plum);
}
/* The steps carry a top margin to clear a heading above them, which is the case on
   /how-it-works. On /process they ARE the first thing in their section, so that margin landed
   on top of the section padding and the block opened ~90px down while closing at 42.

   Third time this exact shape has bitten: the ledger, the band card grids, and now this. The
   rule is always the same, a margin meant to clear a sibling stacking on a parent's padding
   when the sibling is not there. */
.pin-steps:first-child { margin-top: 0; }
/* ---------- project estimator ----------
   Built out of the filter chips and the ruled contact plate that already exist, so it reads as
   part of the site rather than as a widget bolted on.

   The figure is a RANGE and is set large, because the range is the answer. A single number set
   this big would read as a quote however the copy is worded. */
.est { border: 1.5px solid var(--ink); background: var(--paper); }
.est-row {
  display: grid; grid-template-columns: 130px 1fr;
  gap: 10px clamp(14px, 2.2vw, 28px); align-items: baseline;
  padding: clamp(13px, 1.7vw, 18px) clamp(14px, 2vw, 24px);
  border-bottom: 1px solid var(--rule);
}
/* `display: grid` above outranks the user agent's `[hidden] { display: none }`, which is a
   class-versus-element fight the class always wins. Without this the rows the estimator
   hides for the chosen sizing method stay on screen, and the form asks for a facade count
   on an interior only brief. */
.est-row[hidden] { display: none; }
.est-label {
  margin: 0; font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .18em;
  text-transform: uppercase; color: var(--taupe);
}
.est-opts { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
.est-chip, .est-num {
  display: inline-flex; align-items: center; gap: 8px; cursor: pointer;
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .1em; text-transform: uppercase;
  border: 1px solid var(--rule); border-radius: 999px; padding: 7px 14px;
  transition: background-color .18s var(--ease-out), border-color .18s var(--ease-out), color .18s var(--ease-out);
}
.est-chip input { position: absolute; opacity: 0; pointer-events: none; }
.est-chip:hover { border-color: var(--ink); }
/* :has() lets the whole chip carry the state, so the hit target is the chip and not a 13px box */
.est-chip:has(input:checked) { background: var(--plum); border-color: var(--plum); color: var(--cream); }
.est-chip:has(input:focus-visible) { outline: 2px solid var(--ink); outline-offset: 2px; }
.est-num { border-radius: 999px; }
.est-num input {
  width: 4.5ch; border: 0; background: transparent; color: inherit;
  font: inherit; font-size: 13px; text-align: center; padding: 0;
}
.est-num input[name="sqft"] { width: 6.5ch; }
/* Native spinners cannot be styled in any browser, so they are removed and drawn properly.
   Firefox needs the appearance reset, WebKit needs the pseudo-elements. */
.est-num input[type="number"] { -moz-appearance: textfield; appearance: textfield; }
.est-num input[type="number"]::-webkit-outer-spin-button,
.est-num input[type="number"]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

.est-spin { display: inline-grid; grid-template-rows: 1fr 1fr; gap: 1px; margin: 0 2px 0 -4px; }
.est-spin button {
  width: 17px; height: 11px; padding: 0; margin: 0; border: 0; border-radius: 3px;
  background: transparent; color: var(--taupe); cursor: pointer;
  display: grid; place-items: center;
  transition: color .15s var(--ease-out), background-color .15s var(--ease-out);
}
.est-spin button:hover { color: var(--paper); background: var(--ink); }
.est-spin button:active { background: var(--pine); }
.est-spin svg { width: 8px; height: 5px; display: block; }
/* One chevron drawn once, flipped for the down button. */
.est-spin .est-dn svg { transform: rotate(180deg); }
/* The pill's own right padding is now doing nothing between the chevrons and the caption. */
.est-num { padding-right: 12px; }
.est-num > label { cursor: pointer; }
/* Two stacked chevrons inside a 39px pill are about 11px each, which is a fine mouse target
   and a bad thumb target. On a touch device they are dropped entirely: tapping the field
   raises a number pad (the inputs carry inputmode="numeric"), which beats tapping an 11px
   chevron six times. */
@media (pointer: coarse) {
  .est-spin { display: none; }
  .est-num input[type="number"] { width: 5ch; }
  .est-num input[name="sqft"] { width: 7ch; }
}
.est-num input:focus-visible { outline: none; }
.est-num:has(input:focus-visible) { outline: 2px solid var(--ink); outline-offset: 2px; }

/* The area hint sits between rows, so it borrows the row's gutters and rule to stay on the
   same grid rather than floating in the middle of the form. */
/* The area note is a real .est-row, not a paragraph laid across the form. It sits in the same
   two column grid as every control, so its text starts on the same line as the chips and its
   label reads like the labels above it. Written as a loose full width paragraph it ignored the
   grid entirely and looked unfinished. */
.est-note-text {
  margin: 0; font-size: 13.5px; line-height: 1.55; color: var(--taupe); max-width: 62ch;
}

.est-out {
  display: block; padding: clamp(20px, 2.8vw, 32px) clamp(14px, 2vw, 24px);
  background: var(--forest); color: var(--cream); border-bottom: 1px solid var(--rule);
}
.est-out-label {
  margin: 0 0 8px; font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .18em;
  text-transform: uppercase; color: var(--sand);
}
.est-figure {
  margin: 0; font-family: var(--font-display); font-weight: 600;
  font-size: clamp(24px, 3.6vw, 46px); line-height: 1.05; letter-spacing: -.01em;
  font-variant-numeric: tabular-nums;
}
.est-note {
  margin: 10px 0 0; font-size: 13px; line-height: 1.5; color: rgba(249, 240, 222, .8);
}
/* The out of range caution. It lives inside the dark output box because it qualifies the
   number directly above it, and a note placed outside the box gets read as page furniture
   and skipped. Coral rule rather than coral type: coral copy on this green fails contrast. */
.est-warn {
  margin: 12px 0 0; padding-left: 12px; border-left: 2px solid var(--rose);
  font-size: 12.5px; line-height: 1.5; color: rgba(249, 240, 222, .72);
}
.est-warn[hidden] { display: none; }
/* The two timings are different questions and the form was letting them blur: the 10 to 14 day
   window is the INITIAL LOOK, which is what the rush fee buys down, while a full project runs
   about a month. Given its own line rather than folded into the note, which is already long
   enough to be skimmed past. */
.est-time {
  margin: 14px 0 0; padding-top: 12px;
  border-top: 1px solid rgba(249, 240, 222, .26);
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .1em;
  text-transform: uppercase; line-height: 1.6; color: rgba(249, 240, 222, .82);
}
.est-time:empty { display: none; }
.est-fine {
  margin: 0; padding: clamp(12px, 1.6vw, 16px) clamp(14px, 2vw, 24px);
  font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--taupe);
}
@media (max-width: 620px) {
  .est-row { grid-template-columns: 1fr; }
}
/* ---------- detail study ----------
   A render with numbered hotspots over it; each one swaps a tight crop cut from the 4K
   master into the panel alongside. Built for /chapter-house-case-study but generic: the
   script drives every [data-dstudy] on a page.

   The plate is deliberately the larger column. The point of the page is that the wide shot
   is where detail hides, so the wide shot has to stay legible while you work through it. */
.dstudy {
  display: grid; grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr);
  gap: clamp(18px, 2.2vw, 32px); align-items: start;
}
.dplate { position: relative; margin: 0; line-height: 0; border: 1.5px solid var(--ink); }
.dplate img { width: 100%; height: auto; display: block; }

/* The cream halo is load bearing, not decoration: these markers sit on photographs that run
   from near white marble to near black steel, and an ink ring alone vanishes on both ends. */
.dspot {
  position: absolute; transform: translate(-50%, -50%);
  width: 34px; height: 34px; padding: 0; border-radius: 50%;
  border: 1.5px solid var(--ink); background: var(--paper); color: var(--ink);
  font-family: var(--font-mono); font-size: 12px; font-weight: 700; line-height: 1;
  display: grid; place-items: center; cursor: pointer;
  box-shadow: 0 0 0 3px rgba(249, 240, 222, .5);
  transition: background .18s ease, color .18s ease, transform .18s ease;
}
.dspot:hover { background: var(--sand); }
.dspot.is-on {
  background: var(--plum); border-color: var(--plum); color: var(--cream);
  transform: translate(-50%, -50%) scale(1.14);
}
.dspot:focus-visible { outline: 2px solid var(--ink); outline-offset: 3px; }

.dpanel { border: 1.5px solid var(--ink); background: var(--paper); position: sticky; top: 104px; }
/* Fixed 4:3 box because every crop is generated 4:3. Without it the panel jumps height on
   each swap and the whole page reflows under the reader. */
.dzoom { aspect-ratio: 4 / 3; overflow: hidden; border-bottom: 1.5px solid var(--ink); background: var(--sand); }
.dzoom img { width: 100%; height: 100%; object-fit: cover; display: block; }
.dcap { padding: clamp(14px, 1.8vw, 22px); }
.dcap-num {
  margin: 0 0 8px; font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .18em;
  text-transform: uppercase; color: var(--taupe);
}
.dcap h3 { margin: 0 0 10px; }
.dcap p:last-child { margin: 0; font-size: 15px; line-height: 1.6; }

.dhint {
  font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .18em;
  text-transform: uppercase; color: var(--taupe);
}

@media (max-width: 900px) {
  .dstudy { grid-template-columns: 1fr; }
  /* Sticky is wrong once the panel sits below the plate: it would pin the caption over the
     next section as you scroll away from the render it belongs to. */
  .dpanel { position: static; }
  .dspot { width: 30px; height: 30px; font-size: 11px; }
}

/* ---------- numbered chapter heads, spec plates ----------
   From the Laxer/Obys references Michelle sent: a section number, a stacked display title with
   a colour bar slicing through one line of it, rows that end in a dot, and a split meta row.

   Taken as STRUCTURE, not as a skin. The references are Helvetica-brutalist on grey with hot
   pink; running that literally would replace the identity this site already has. So the devices
   arrive in Comfortaa, ink on cream, and the bar is `--rose`, which is the coral already used
   on pressed buttons and is the complement of the forest green the site is built on.

   Ink on rose measures 5.4:1, so the bar is safe for display type and for body type. */
.numhead { margin-bottom: clamp(22px, 3vw, 38px); }
.numhead-n {
  margin: 0 0 12px; font-family: var(--font-mono); font-size: 12px;
  letter-spacing: .2em; color: var(--taupe);
}
.numhead-t {
  margin: 0; font-family: var(--font-display); font-weight: 600;
  font-size: clamp(30px, 5.2vw, 60px); line-height: 1.02; letter-spacing: -.02em;
}
.nh-line { display: block; }
/* box-decoration-break keeps the padding on BOTH ends when a barred line wraps; without it a
   wrapped highlight loses its left inset on the second line and the bar looks broken. */
.numhead-t .bar {
  background: var(--pink); color: var(--ink);
  padding: .02em .3em; margin-left: -.3em;
  box-decoration-break: clone; -webkit-box-decoration-break: clone;
}
/* On the sage band the number would sit at 2.6:1 against sand. Ink instead. */
.section-band .numhead-n { color: var(--ink); opacity: .62; }

/* The spec plate: a single deliverable, itemised. The dot at the end of each row is the
   reference's own device and it earns its place here, because it gives a ragged list a hard
   right edge to line up against. */
.specplate {
  border: 1.5px solid var(--ink); background: var(--paper);
  padding: clamp(20px, 2.6vw, 32px);
}
.spec-label {
  margin: 0 0 10px; font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .18em;
  text-transform: uppercase; color: var(--taupe);
}
.spec-title {
  margin: 0; font-family: var(--font-display); font-weight: 600;
  font-size: clamp(20px, 2.6vw, 30px); line-height: 1.12;
}
.spec-sub { margin: 8px 0 0; font-size: 15px; color: var(--taupe); }
.spec-sub b { font-weight: 500; color: var(--ink); border-bottom: 1.5px solid var(--rose); }

.spec-cols {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
  gap: clamp(20px, 3vw, 40px); margin-top: clamp(22px, 2.8vw, 32px);
}
.spec-inc {
  margin: 0 0 4px; font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .18em;
  text-transform: uppercase; color: var(--taupe);
}
.speclist { list-style: none; margin: 0; padding: 0; border-top: 1.5px solid var(--ink); }
.speclist li {
  display: flex; align-items: baseline; gap: 14px;
  padding: 10px 0; border-bottom: 1px solid var(--rule);
  font-family: var(--font-mono); font-size: 11px; letter-spacing: .08em;
  text-transform: uppercase; line-height: 1.4;
}
.speclist li::after {
  content: ""; flex: none; margin-left: auto;
  width: 7px; height: 7px; border-radius: 50%; background: var(--ink);
  transform: translateY(-1px);
}
/* The exclusions read in rose so the two lists cannot be skimmed as one. */
.speclist.is-not li::after { background: var(--rose); }

.specmeta {
  display: grid; grid-template-columns: 1fr 1fr; margin: clamp(22px, 2.8vw, 32px) 0 0;
  border-top: 1.5px solid var(--ink);
}
.specmeta > div { padding: 16px 0 0; }
.specmeta > div + div { border-left: 1.5px solid var(--ink); padding-left: clamp(16px, 2vw, 26px); }
.specmeta dt {
  display: flex; align-items: center; gap: 8px;
  font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .18em;
  text-transform: uppercase; color: var(--taupe);
}
.specmeta dt::before {
  content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--rose); flex: none;
}
.specmeta dd {
  margin: 8px 0 0; font-family: var(--font-display); font-weight: 600;
  font-size: clamp(19px, 2.4vw, 27px); line-height: 1.1;
}

@media (max-width: 560px) {
  .specmeta { grid-template-columns: 1fr; }
  .specmeta > div + div {
    border-left: 0; padding-left: 0; margin-top: 16px; border-top: 1px solid var(--rule);
  }
}

/* ---------- the contact title block ----------
   From the reference Michelle picked: the whole page is a ruled grid of cells with hairline
   borders and no gaps, a giant headline sitting in its own cell, a photograph filling another,
   and the contact details as labelled rows.

   It suits this studio better than it suited the reference, because a ruled sheet with a title
   block IS an architectural drawing. The site already speaks this language: the BRIEF plate is
   a grid of ruled cells, and the blade sign in the gutter is the rotated edge strip from the
   reference, already built.

   The form keeps its own markup untouched. Inside the sheet it drops its 6px frame, because the
   sheet is now the frame; nesting one border inside another was the thing making it read as a
   widget dropped onto a page. */
/* NOTE: named .tblock, not .sheet. `.sheet` is already a drawing-figure component used on
   five other pages, and reusing the name silently overrode its 2px border everywhere. */
.tblock { border: 1.5px solid var(--ink); background: var(--paper); }
.tblock + .tblock { margin-top: clamp(20px, 2.6vw, 34px); }
.tb-row { display: grid; }
.tb-row + .tb-row { border-top: 1.5px solid var(--ink); }
.tb-row.two { grid-template-columns: minmax(0, 1.02fr) minmax(0, 1fr); }
/* Label column then value, as the reference lays its contact rows out. */
.tb-row.kv { grid-template-columns: minmax(130px, 230px) minmax(0, 1fr); }
.tb-cell { padding: clamp(16px, 2.2vw, 28px); min-width: 0; }
.tb-cell + .tb-cell { border-left: 1.5px solid var(--ink); }

/* Archivo, matching the ASTRATTO masthead and the BRIEF title in the plate two rows below it.
   The stylesheet already recorded the reasoning up at .mast-word: "the rounded face read as a
   logo rather than as a rule of type at this size". A ruled sheet wants a neutral grotesque set
   tight, and Contact being rounded while BRIEF underneath it was a grotesque is the tell.

   THE ROUNDED VERSION IS NOT LOST. Add `is-rounded` to the h2 and it comes straight back,
   identical to what it was. Michelle liked it and may want it again. */
.tb-title {
  margin: 0; font-family: Archivo, "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-weight: 700; text-transform: uppercase;
  font-size: clamp(38px, 7.4vw, 92px); line-height: .9; letter-spacing: -.035em;
}
.tb-title.is-rounded {
  font-family: var(--font-display); font-weight: 600; text-transform: none;
  font-size: clamp(44px, 9vw, 108px); letter-spacing: -.035em;
}
/* The reference sets a small registered mark against the headline. Here it is the studio's
   own initials, which is a truer version of the same gesture. */
/* The word drops down the cell, the mark does not follow. That works because the mark is taken
   out of the text flow and pinned: `top` on an absolutely positioned box is measured from the
   containing block's PADDING EDGE, so growing the cell's padding-top moves the word and leaves
   the mark exactly where it was. Left aligned, as it was. */
.tb-cell.is-title { position: relative; padding-top: clamp(30px, 4vw, 54px); }
/* Centred horizontally. Safe to do only because the mark is absolute: inline it would count as
   part of the line and centring would drag it in off the corner. */
.tb-title { text-align: center; }
.tb-mark { white-space: nowrap;
  position: absolute; top: clamp(16px, 2.2vw, 28px); right: clamp(16px, 2.2vw, 28px);
  font-family: var(--font-mono); font-size: clamp(9px, .9vw, 12px);
  letter-spacing: .16em; color: var(--taupe); text-transform: uppercase;
}
.tb-k {
  margin: 0 0 10px; font-family: var(--font-mono); font-size: 9.5px; letter-spacing: .18em;
  text-transform: uppercase; color: var(--taupe);
}
.tb-cell p:not(.tb-k):not(.tb-lede) { margin: 0; font-size: 16px; line-height: 1.55; }
.tb-lede { margin: 0; font-size: clamp(16px, 1.5vw, 19px); line-height: 1.5; max-width: 46ch; }
.tb-v { font-size: 16px; line-height: 1.5; }

/* A photograph fills its cell edge to edge, the way the reference does it. */
.tb-cell.is-media { padding: 0; position: relative; min-height: 260px; }
.tb-cell.is-media img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; object-position: 50% 22%; display: block;
}

/* Inside the sheet the plate stops drawing its own frame and its head loses the padding that
   was holding it away from a border that is no longer there. */
.tblock .formplate { border: 0; margin: 0; background: transparent; }
.tblock .fp-head { padding-bottom: clamp(16px, 2.4vw, 26px); }
.tblock .fp-cell:first-child, .tblock .fp-cell:nth-child(2) { border-top: 0; }

/* Was padding: 0, so the note ran straight into the border on both sides. */
.form-note { margin: 0; padding: clamp(12px, 1.6vw, 16px) clamp(14px, 1.8vw, 22px); }
#formMsg { margin: 0; padding: 0 clamp(14px, 1.8vw, 22px) 12px; }
#formMsg[hidden] { display: none; }

@media (max-width: 760px) {
  .tb-row.two, .tb-row.kv { grid-template-columns: 1fr; }
  .tb-cell + .tb-cell { border-left: 0; border-top: 1.5px solid var(--ink); }
  .tb-row.kv .tb-cell:first-child { padding-bottom: 0; border-bottom: 0; }
  .tb-row.kv .tb-cell + .tb-cell { border-top: 0; padding-top: 8px; }
  .tb-cell.is-media { min-height: 220px; }
}

/* ---------- the receipt ----------
   A running summary of what the visitor has picked, from the reference Michelle sent: ruled
   rows, mono type, label left and value right, a total on its own rule at the foot.

   It floats and it can be dragged, because she asked for it to be movable "in case its
   annoying to people", and it collapses to a small pill so it is never in the way.

   NO PER LINE PRICES, on purpose. The whole page insists the output is a bracket and not a
   quote, and her own rate cards say clients get one flat fixed fee. Itemised dollar amounts
   here would publish the rate card and contradict the page in the same breath. The receipt
   answers "did I pick what I meant to", and the range answers "what will it cost". */
.receipt-dock {
  position: fixed; right: 20px; bottom: 20px; z-index: 60;
  display: flex; flex-direction: column; align-items: flex-end; gap: 10px;
  max-width: min(340px, calc(100vw - 32px));
}
.receipt-open {
  display: inline-flex; align-items: center; gap: 9px;
  padding: 10px 16px; border: 1.5px solid var(--ink); border-radius: 999px;
  background: var(--paper); color: var(--ink); cursor: pointer;
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .16em; text-transform: uppercase;
  box-shadow: -5px 5px 0 var(--ink);
  transition: background-color .18s var(--ease-out), color .18s var(--ease-out);
}
.receipt-open:hover { background: var(--ink); color: var(--paper); }
.receipt-open::before {
  content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--rose); flex: none;
}
.receipt-open[hidden] { display: none; }

.receipt {
  width: min(340px, calc(100vw - 32px));
  background: var(--paper); border: 1.5px solid var(--ink);
  box-shadow: -8px 8px 0 var(--ink);
  max-height: min(70vh, 560px); display: flex; flex-direction: column;
}
.receipt[hidden] { display: none; }
/* It arrives rather than appearing, because it now opens itself when somebody starts picking
   and an unannounced panel snapping into the corner reads as an error. `hidden` toggles
   display, which cannot be transitioned, so the motion rides on a class added a frame later. */
.receipt { opacity: 0; transform: translateY(8px); transition: opacity .22s var(--ease-out), transform .22s var(--ease-out); }
.receipt.is-in { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  .receipt { transition: none; opacity: 1; transform: none; }
}

/* The whole head is the drag handle, so `cursor: grab` belongs on it and not on the panel. */
.receipt-head {
  display: flex; align-items: center; justify-content: space-between; gap: 12px;
  padding: 12px 14px; border-bottom: 1.5px solid var(--ink);
  cursor: grab; touch-action: none; user-select: none;
}
.receipt-head:active { cursor: grabbing; }
.receipt-title {
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .18em;
  text-transform: uppercase; color: var(--ink);
}
.receipt-close {
  border: 0; background: transparent; padding: 2px 4px; cursor: pointer; color: var(--taupe);
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .14em; text-transform: uppercase;
}
.receipt-close:hover { color: var(--ink); }

.receipt-list { margin: 0; padding: 0 14px; overflow-y: auto; flex: 1 1 auto; }
.receipt-row {
  display: grid; grid-template-columns: 92px 1fr; gap: 12px; align-items: baseline;
  padding: 10px 0; border-bottom: 1px solid var(--rule);
}
.receipt-row dt {
  font-family: var(--font-mono); font-size: 9px; letter-spacing: .16em;
  text-transform: uppercase; color: var(--taupe);
}
.receipt-row dd { margin: 0; font-size: 13.5px; line-height: 1.45; }
/* Stacked values get the reference's bullet, which is what stops three add ons reading as one
   run-on sentence. */
.receipt-row dd span { display: block; position: relative; padding-left: 14px; }
.receipt-row dd span::before {
  content: ""; position: absolute; left: 0; top: .55em;
  width: 5px; height: 5px; border-radius: 50%; background: var(--ink);
}
.receipt-empty { padding: 18px 0; margin: 0; font-size: 13.5px; color: var(--taupe); }

.receipt-total {
  display: flex; align-items: baseline; justify-content: space-between; gap: 12px;
  padding: 14px; border-top: 1.5px solid var(--ink); background: var(--forest); color: var(--cream);
}
.receipt-total span {
  font-family: var(--font-mono); font-size: 9px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--sand);
}
.receipt-total strong {
  font-family: var(--font-display); font-weight: 600; font-size: 17px;
  font-variant-numeric: tabular-nums;
}
.receipt-fine {
  margin: 0; padding: 10px 14px; border-top: 1px solid var(--rule);
  font-family: var(--font-mono); font-size: 9px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--taupe);
}

/* A floating panel on a phone covers the thing it is describing, and there is nowhere to drag
   it to. The pill stays, the panel opens against the bottom edge full width. */
@media (max-width: 640px) {
  .receipt-dock { left: 12px; right: 12px; bottom: 12px; max-width: none; align-items: stretch; }
  .receipt { width: auto; box-shadow: -5px 5px 0 var(--ink); max-height: 60vh; }
  .receipt-head { cursor: default; }
}
@media (prefers-reduced-motion: reduce) {
  .receipt-open { transition: none; }
}

/* ---------- pressed state, last on purpose ----------
   Every button presses into the SAME pair, whatever it looks like at rest: the face drops to
   deep green and the disc goes coral. One gesture across the site rather than a per-button
   effect.

   Coral is on the disc and not on the copy deliberately. Coral type on this green measures
   about 4.4:1, under the threshold for text this size; the disc carries no such constraint and
   is the louder position anyway.

   POSITION IS LOAD-BEARING. Components paint their own faces with selectors like
   `.work-chips .btn:nth-child(1)`, which is (0,3,0), exactly the same weight as the doubled
   class below. Specificity cannot break that tie, so source order does, and this block has to
   stay at the end of the file. Adding component rules after it will silently un-press them.

   .is-pressed rides along with :active because these buttons navigate; see app.js. */
.btn:active:active, .btn.is-pressed.is-pressed {
  background: var(--pine);
  color: var(--cream);
  --dot: var(--rose);
}
.btn-outline:active:active, .btn-outline.is-pressed.is-pressed { box-shadow: inset 0 0 0 1.5px var(--pine); }
/* ---------- panorama scene strip ----------

   The room picker under the 360 walkthrough. Two tours of one project, so it groups by tour
   with a mono caption per group rather than running all thirteen rooms together.

   Buttons rather than links: they swap the scene inside the viewer, they do not navigate. The
   thumbnails are 2:1 because the sources are equirectangular, and a whole panorama squeezed
   into a 16:9 crop reads as a smear; at 2:1 it at least reads as "the whole room, flattened",
   which is what it is. */
.pano-strip { margin-top: clamp(28px, 3vw, 40px); display: flex; flex-direction: column; gap: 28px; }
.pano-group-head {
  margin: 0 0 12px; font-family: var(--font-mono); font-size: 10px; letter-spacing: .2em;
  text-transform: uppercase; color: var(--taupe);
}
.pano-row { display: grid; grid-template-columns: repeat(auto-fill, minmax(148px, 1fr)); gap: 12px; }
.pano-thumb {
  appearance: none; padding: 0; cursor: pointer; text-align: left; font: inherit;
  background: var(--paper); border: 2px solid var(--ink); color: var(--ink);
  display: flex; flex-direction: column; overflow: hidden;
  transition: transform .18s ease, box-shadow .18s ease;
}
.pano-thumb img { display: block; width: 100%; height: auto; aspect-ratio: 2 / 1; object-fit: cover; }
.pano-thumb span {
  padding: 7px 9px; font-family: var(--font-mono); font-size: 10px; letter-spacing: .12em;
  text-transform: uppercase; border-top: 2px solid var(--ink);
}
.pano-thumb:hover, .pano-thumb:focus-visible { transform: translate(-2px, -2px); box-shadow: 4px 4px 0 rgba(20, 38, 29, .3); }
/* The scene you are already in. Ink ground with cream type measures 12.6:1, so the current
   room is obvious without relying on the thin border alone. */
.pano-thumb[aria-current="true"] { background: var(--ink); color: var(--cream); }
.pano-thumb[aria-current="true"] span { border-top-color: var(--cream); }
@media (prefers-reduced-motion: reduce) { .pano-thumb { transition: none; } .pano-thumb:hover { transform: none; } }

/* Pannellum's own chrome, keyed to the site. The library ships a black title box and blue-grey
   controls; left alone they read as a third-party widget dropped into the page.
   The title is hidden in the normal view because .media-bar already names the room right under
   the stage, but it comes back in fullscreen, where the bar is off screen and it is the only
   thing telling you which room you are in. */
.media-stage .pnlm-title-box { display: none; }
.media-stage:fullscreen .pnlm-title-box,
.media-stage:-webkit-full-screen .pnlm-title-box {
  display: block; font-family: var(--font-mono); font-size: 11px; letter-spacing: .16em;
  text-transform: uppercase; background: var(--ink); color: var(--cream);
}
.media-stage .pnlm-controls {
  background: var(--paper); border: 2px solid var(--ink); border-radius: 0;
  box-shadow: 3px 3px 0 rgba(20, 38, 29, .3);
}
.media-stage .pnlm-control { border-color: var(--ink); background: var(--paper); }
.media-stage .pnlm-control:hover { background: var(--sand); }
.media-stage .pnlm-load-box, .media-stage .pnlm-lbox { background: var(--pine); }
.media-stage .pnlm-container { background: var(--pine); }

/* ---------- walkthrough floor markers ----------

   The donut CloudPano used: a translucent ring lying flat on the floor rather than a billboard
   arrow standing up in the air. Passing cssClass on the hotspot replaces Pannellum's sprite
   classes entirely, so nothing of the library's own look survives here.

   The marker itself is a pseudo-element, not the hotspot div, because Pannellum owns that
   div's `transform` for positioning and writing our own would fight it. The div is left at
   zero size so the library's centring (-offsetWidth/2) resolves to the exact floor point, and
   the ring is then centred on that point by its own offsets.

   rotateX lays it down. The floors in these renders are near-white travertine, so a plain
   white ring would disappear: the ink outline on both edges of the stroke is what keeps it
   readable, and it is doing the same job as CloudPano's dark rim. */
.pano-dot { position: absolute; width: 0; height: 0; perspective: 340px; }
.pano-dot::before {
  content: ''; position: absolute; left: -51px; top: -51px; width: 102px; height: 102px;
  border-radius: 50%;
  /* A doughnut, not a washer. A border plus a box-shadow ring gives four hard edges; this is
     one radial gradient whose stops fade in and out, so the ring has no edge anywhere. The
     stops are the dough: nothing until 38%, rising to full through the body of the ring, gone
     again by 88%. The hole stays open so the floor reads through it. */
  background: radial-gradient(closest-side,
    rgba(255, 255, 255, 0)    30%,
    rgba(255, 255, 255, .10)  40%,
    rgba(255, 255, 255, .40)  52%,
    rgba(255, 255, 255, .56)  62%,
    rgba(255, 255, 255, .40)  70%,
    rgba(255, 255, 255, .13)  80%,
    rgba(255, 255, 255, 0)    90%);
  /* drop-shadow follows the gradient's alpha rather than the box, so the glow is the ring's
     own shape blurred outward, not a rectangle. Two passes: a tight one for the edge and a
     wide faint one for the bloom. Kept low, since a glow this size reads as a light source in
     the room and a bright one would look like a lens flare rather than a marker. */
  filter: drop-shadow(0 0 7px rgba(255, 255, 255, .55)) drop-shadow(0 0 16px rgba(255, 255, 255, .3));
  transform: rotateX(66deg);
  transition: transform .22s ease, opacity .22s ease;
}
.pano-dot:hover::before, .pano-dot:focus-visible::before {
  transform: rotateX(66deg) scale(1.13);
  opacity: 1;
  filter: drop-shadow(0 0 9px rgba(255, 255, 255, .75)) drop-shadow(0 0 22px rgba(255, 255, 255, .45)) brightness(1.35);
}
/* Pannellum still supplies the tooltip span and positions it above the marker; only its look
   is ours. It names the room the marker leads to, which the arrow never did. */
.pano-dot span {
  visibility: hidden; position: absolute;
  background: var(--ink); color: var(--cream);
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .16em; text-transform: uppercase;
  padding: 6px 10px; white-space: nowrap; border-radius: 0;
}
.pano-dot:hover span { visibility: visible; }
/* Pannellum draws the tooltip's pointer as a border triangle in its own near-black; recolour
   it or it sits a shade off the ink chip above it. */
.pano-dot span::after { border-top-color: var(--ink) !important; }
@media (prefers-reduced-motion: reduce) {
  .pano-dot::before { transition: none; }
  .pano-dot:hover::before { transform: rotateX(66deg); }
}

/* ---------- the homepage hero, as a dissolve ----------

   Two stacked images that cross-fade. No sliding: the outgoing picture stays put and the
   incoming one comes up through it, which is why the transition is on opacity alone and why
   both layers sit in the same absolute box.

   The drag is a compromise between the two things asked for. A real carousel would slide the
   image with the finger, but sliding is exactly what a dissolve is not; so the picture takes
   a small damped nudge in the direction of the drag, enough to feel like it is being pushed,
   and the change itself is still a dissolve when the finger lifts. */
.hero-show { touch-action: pan-y; cursor: grab; -webkit-user-select: none; user-select: none; }
/* Without this the browser's own image drag-and-drop starts on mousedown, complete with a
   ghost thumbnail, and the carousel never sees the movement. */
.hero-slide { -webkit-user-drag: none; -khtml-user-drag: none; user-drag: none; }
.hero-show.is-dragging { cursor: grabbing; }
.hero-slide {
  opacity: 0;
  transition: opacity 1400ms cubic-bezier(.4, 0, .3, 1);
  will-change: opacity, transform;
}
.hero-slide.is-on { opacity: 1; }
/* The neighbouring picture during a slide or a drag: fully opaque but translated off the
   stage, where .hero-shot's overflow:hidden clips it. Without this it would be invisible at
   opacity 0 and the slide would look like the outgoing image leaving over an empty box. */
.hero-slide.is-peek { opacity: 1; }
/* While a finger or mouse is down the pictures must track it exactly, so no transition at
   all: the script writes transform on every pointermove. The slide's own easing is applied
   inline by the script when the drag is released. */
.hero-show.is-dragging .hero-slide { transition: none; }

.hero-arrow {
  position: absolute; top: 50%; transform: translateY(-50%);
  z-index: 2; width: 42px; height: 42px; padding: 0;
  display: grid; place-items: center;
  background: rgba(251, 246, 234, .82); color: var(--ink);
  border: 1.5px solid var(--ink); border-radius: 50%;
  cursor: pointer; opacity: 0;
  transition: opacity .2s ease, background-color .2s ease;
}
.hero-arrow svg { width: 20px; height: 20px; }
.hero-prev { left: 14px; }
.hero-next { right: 14px; }
/* Visible at rest, not only on hover. Hover-only controls are why a carousel can look
   like a static picture until you happen to move the cursor over it; a quiet permanent pair
   says "there are more of these" the moment the page loads. */
.hero-arrow { opacity: .62; }
.hero-show:hover .hero-arrow, .hero-arrow:focus-visible { opacity: 1; }
.hero-arrow:hover { background: var(--sand); }
/* Touch has no hover, so the arrows would never appear; there the swipe is the control. */
@media (hover: none) { .hero-arrow { display: none; } }

.hero-count {
  position: absolute; right: 12px; bottom: 10px; z-index: 2; margin: 0;
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .18em;
  color: var(--cream); text-shadow: 0 1px 6px rgba(20, 38, 29, .75);
  font-variant-numeric: tabular-nums;
}

@media (prefers-reduced-motion: reduce) {
  .hero-slide { transition: opacity 1ms; }
}

/* ---------- drawer shortcuts ----------

   The four bar destinations, repeated at the top of the drawer as direct links. They are also
   in the list below, but each sits inside a collapsed group, and on a phone the bar's copies
   are hidden entirely, so without these there is no one-tap route to Portfolio at all.

   Deliberately NOT styled as .drawer-row. The rows below are 23 to 36px display type and these
   would read as the four most important pages on the site if they matched. Pills at label size
   read as what they are: shortcuts into a list that starts underneath. */
.drawer-quick {
  display: flex; flex-wrap: wrap; gap: 8px;
  margin: 0 0 clamp(22px, 3vw, 34px);
}
.drawer-quick a {
  font-family: var(--font-mono); font-size: 11px; letter-spacing: .12em; text-transform: uppercase;
  color: var(--ink); background: var(--sand);
  border: 1.5px solid var(--ink); border-radius: 40px;
  padding: 9px 16px; text-decoration: none;
  transition: background-color .18s ease, color .18s ease;
}
.drawer-quick a:hover, .drawer-quick a:focus-visible {
  background: var(--ink); color: var(--cream); text-decoration: none;
}
/* On a touch screen these are the only route to these four pages, so they get a proper
   thumb-sized target rather than the 33px a label-sized pill lands on by default. */
@media (pointer: coarse) { .drawer-quick a { padding: 13px 18px; } }
@media (prefers-reduced-motion: reduce) { .drawer-quick a { transition: none; } }

/* ---------- the hero filmstrip ----------

   Built only while a drag is in progress. A window of slides either side of the current one,
   laid out edge to edge with a visible seam, so you can see the next few coming and can cross
   more than one picture in a single gesture. At rest the hero is back to the two cross-fading
   layers and this does not exist, which is why forty-nine images never sit in the DOM at once.

   Below the arrows and the counter (z-index 2), which stay usable throughout. */
.hero-strip { position: absolute; inset: 0; z-index: 1; will-change: transform; }
/* Full stage width, written by the script. Nothing peeks at rest because the strip does not
   exist at rest: it is built when a horizontal drag starts and removed when it ends. */
.hero-tile {
  position: absolute; top: 0; height: 100%;
  object-fit: cover; display: block;
  -webkit-user-drag: none; user-drag: none;
  background: var(--pine);
}
/* The strip follows the pointer with no easing of its own; the script applies the easing
   inline once the drag is released and it animates to the picture it landed on. */
.hero-show.is-dragging .hero-strip { transition: none; }

/* Position through the run, along the bottom edge. With forty-nine pictures a row of dots
   would be unreadable, so it is one continuous bar: the same information, and it doubles as a
   quiet hint that the hero is a sequence rather than a photograph. */
.hero-prog {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 2;
  height: 3px; background: rgba(20, 38, 29, .28);
}
.hero-prog > span {
  display: block; height: 100%; width: 0;
  background: var(--sand);
  transition: width .45s cubic-bezier(.22, .61, .36, 1);
}
@media (prefers-reduced-motion: reduce) { .hero-prog > span { transition: none; } }

/* ---------- what we source ----------

   The six categories, in the card component the rest of the site uses. This replaced a
   full-bleed grid of flat colour panels with icon silhouettes, which was the only thing on the
   site built that way and read as another brand's page.

   The drawings survived the move: they inherit their colour through currentColor, so a card
   only declares its own ink and the artwork follows, exactly as the panels did. */
/* Pinned to a count that DIVIDES six, never auto-fit. Auto-fit put five across and stranded
   Textiles alone on a second row, which is the same incomplete-last-row fault the service grid
   had. Three and two both divide six exactly. */
.spec-cards { grid-template-columns: repeat(3, minmax(0, 1fr)); }
@media (max-width: 860px) { .spec-cards { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
.spec-card { display: flex; flex-direction: column; align-items: flex-start; gap: 18px; padding: 26px; }
.spec-card h3 { font-size: 18px; }
.spec-ico { display: block; color: var(--pine); }
.spec-ico svg { width: 46px; height: auto; display: block; }
@media (max-width: 700px) { .spec-card { padding: 20px; gap: 14px; } .spec-ico svg { width: 38px; } }

/* ---------- the page opener ----------

   Every case study used to hand the reader a green title block and then several hundred
   words before showing a single thing. The work is the argument, so the work goes first.

   Full bleed, like the work strip and the poster: this is meant to land, and a lead image
   with page margins is a picture of a lead image. It butts straight onto the forest hero
   with no gap, so the two read as one opening rather than as a header followed by a photo.

   HEIGHT is viewport-relative rather than a fixed ratio. A 16:9 source shown whole is
   810px tall on a laptop, which puts the first sentence below the fold; a hard 21:9 crop
   throws away a quarter of a composition that was framed on purpose. Clamping the height
   and letting object-fit crop keeps it cinematic on a wide screen and keeps it a picture
   of a room on a phone.

   The caption carries the container measure itself rather than needing a wrapper element,
   because it is the only part of the figure that respects the page margin. Same mono as
   the drawing sheets and the case study plates. */
.page-opener { margin: 0; }
.page-opener img {
  display: block; width: 100%;
  height: clamp(240px, 54vh, 580px);
  object-fit: cover; object-position: center;
  background: var(--ink);
}
.page-opener figcaption {
  display: flex; align-items: center; gap: 9px;
  max-width: 1200px; margin: 0 auto;
  padding: 13px clamp(20px, 4vw, 40px) 0;
  font-family: var(--font-mono); font-size: 10px; letter-spacing: .16em; text-transform: uppercase;
  color: var(--taupe);
}
/* A short landscape window would otherwise give the image most of the screen. */
@media (max-height: 620px) { .page-opener img { height: clamp(200px, 46vh, 400px); } }

/* ---------- open a hosted tour ----------

   Used where a tour lives somewhere else and has to be opened rather than embedded. The
   information markers inside these tours are ordinary links set to open in a new tab, and a
   cross origin iframe blocks exactly that, so embedding them quietly breaks the thing they
   are for. */
.tour-open {
  display: flex; align-items: stretch; gap: 0; margin-top: var(--pad-continuous);
  border: 2px solid var(--ink); background: var(--paper);
  box-shadow: -16px 16px 0 var(--ink); text-decoration: none; color: inherit;
  transition: transform .18s var(--ease-out), box-shadow .18s var(--ease-out);
}
.tour-open:hover { transform: translate(-3px, 3px); box-shadow: -13px 13px 0 var(--ink); }
.tour-shot { position: relative; flex: 0 0 44%; overflow: hidden; background: var(--ink); }
.tour-shot img { width: 100%; height: 100%; object-fit: cover; display: block; min-height: 200px; }
.tour-play {
  position: absolute; left: 50%; top: 50%; width: 54px; height: 54px; margin: -27px 0 0 -27px;
  border-radius: 50%; background: var(--paper); box-shadow: 0 0 0 2px var(--ink);
}
.tour-play::after {
  content: ""; position: absolute; left: 21px; top: 17px;
  border-left: 17px solid var(--ink); border-top: 10px solid transparent; border-bottom: 10px solid transparent;
}
.tour-line { display: flex; flex-direction: column; justify-content: center; gap: 8px; padding: clamp(18px, 3vw, 32px); }
.tour-line strong { font-size: 19px; line-height: 1.25; }
.tour-line span { font-size: 14px; line-height: 1.5; color: var(--taupe); max-width: 46ch; }
@media (max-width: 640px) {
  .tour-open { display: block; box-shadow: -10px 10px 0 var(--ink); }
  .tour-shot { display: block; }
  .tour-shot img { min-height: 190px; }
}

/* ---------- style quiz ----------

   Spacing is the whole job here. Each stage is one block with generous air above and below,
   because the eye should be on two pictures and nothing else. The old version set margins on
   the pieces and the stages ran into each other. */
/* the page opened on a wall of text, so it now opens on the pictures instead */
/* The quiz pins are a mix of portrait and landscape, and the site strip lets pictures keep
   their own height, which left ragged white space inside the frames. Here they crop to one
   shape, and the frames are smaller because this is a taster rather than a project gallery. */
/* The stage is a grid with a 44px gap, and the site adds its own top margin to .prose after
   a strip, which made the space below a strip more than twice the space above it. The rhythm
   is set here in one place: 44 between blocks, and a label belongs to its strip so it is
   pulled down to sit against it. */
/* An empty block still takes a row in a grid, and every row it takes adds another gap. The
   result has four blocks that only fill on some routes, so on the quick one they were stacking
   up two hundred pixels of nothing before the mood board. */
#quiz .quiz-stage > .prose:empty,
#quiz .quiz-stage > .quiz-grid:empty,
#quiz .quiz-stage > .quiz-styles:empty,
#quiz .quiz-stage > .quiz-reads:empty { display: none; }

#quiz .prose { margin-top: 0; }
#quiz .strip { margin: 0; }
/* pulled back by exactly the stage gap, then given 14px of its own, so the label sits against
   its strip at every width rather than only at the one I happened to measure */
#quiz .strip-label { margin: 0 0 calc(14px - clamp(28px, 4vw, 44px)); }
#quiz .strip-track { padding-bottom: 12px; }

#quiz .strip-fig { width: clamp(150px, 18vw, 224px); }
#quiz .strip-fig img { aspect-ratio: 3 / 4; object-fit: cover; height: auto; }
#quiz .strip-label { margin-top: 4px; }

/* A still band across the top. It does not pin and it does not move: it scrolls away with
   everything else, and the page opens on pictures rather than on a paragraph. */
.quiz-band { overflow: hidden; height: clamp(220px, 40vh, 420px); border-bottom: 2px solid var(--ink); background: var(--sand); }
.quiz-band img { display: block; width: 100%; height: 145%; object-fit: cover; will-change: transform; }
@media (prefers-reduced-motion: reduce) { .quiz-band img { height: 100%; transform: none; } }

.quiz-check { display: grid; gap: 12px; justify-items: start; margin-top: 8px; }
.quiz-check-q { margin: 0; font-family: var(--font-display); font-weight: 600; font-size: clamp(18px, 2.1vw, 23px); }
.quiz-fix { display: grid; gap: 14px; justify-items: start; width: 100%; }
.quiz-fixtext {
  width: 100%; max-width: 62ch; border: 1.5px solid var(--ink); background: var(--paper);
  color: inherit; font: inherit; font-size: 15px; line-height: 1.5; padding: 12px 14px; resize: vertical;
}
.quiz-fixtext:focus-visible { outline: 2px solid var(--forest); outline-offset: 2px; }

.quiz-stage { display: grid; gap: clamp(28px, 4vw, 44px); }
/* .quiz-stage sets display, which beats the browser default for [hidden]. Without this the
   start block, the pairs and the result all sit on the page at once. */
.quiz-stage[hidden] { display: none; }
.quiz-tile-note {
  position: absolute; left: 0; right: 0; bottom: 0; padding: 20px 10px 0; border-bottom: 8px solid rgba(20,38,29,.82);
  background: linear-gradient(to top, rgba(20,38,29,.82), rgba(20,38,29,0));
  color: #fff; font-family: var(--font-mono); font-size: 10px; letter-spacing: .1em;
  text-transform: uppercase; text-align: left; line-height: 1.4; pointer-events: none;
  display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
}
.quiz-stage > .prose + .quiz-actions { margin-top: 4px; }

.quiz-bar { display: flex; align-items: center; gap: 18px; }
.quiz-back { display: inline-flex; align-items: center; gap: 6px; line-height: 1; border: 1.5px solid var(--ink); background: transparent; color: inherit; font: inherit; font-size: 12px; padding: 8px 12px; border-radius: 2px; cursor: pointer; }
.quiz-back[disabled] { opacity: .32; cursor: default; }
.quiz-prog { flex: 1; height: 6px; background: rgba(20,38,29,.14); border-radius: 3px; overflow: hidden; }
.quiz-prog span { display: block; height: 100%; width: 0; background: var(--forest); transition: width .25s var(--ease-out); }
.quiz-num { margin: 0; font-family: var(--font-mono); font-size: 11px; letter-spacing: .16em; color: var(--taupe); white-space: nowrap; }

/* The pairs had no words on them at all, which left people judging which picture was better
   rather than which room they wanted to be in. The prompt is the instruction, so it is set as
   a heading; and "I like both" and "I like neither" are real answers, not a faint escape
   hatch, so they are buttons rather than a small underlined link. */
.quiz-prompt { margin: 8px 0 22px; text-align: center; font-size: clamp(19px, 2.3vw, 25px); line-height: 1.2; }
.quiz-foot { display: flex; flex-wrap: wrap; justify-content: center; gap: 12px; margin-top: 22px; }
.quiz-alt {
  border: 1.5px solid var(--ink); background: transparent; color: inherit;
  font: inherit; font-size: 14px; padding: 9px 16px; border-radius: 999px; cursor: pointer;
  transition: transform .13s var(--ease-out), box-shadow .13s var(--ease-out);
}
.quiz-alt:hover, .quiz-alt:focus-visible { transform: translate(-2px, 2px); box-shadow: -4px 4px 0 var(--ink); }
/* The arrow glyph carries its ink to the right of its box, so a button that measures as
   centred reads as pushed right. Pulled back by the width of that bearing. */
.quiz-arrow { display: inline-block; line-height: 1; position: relative; top: -1px; }

.quiz-room { margin: 0; text-align: center; font-family: var(--font-mono); font-size: 11px; letter-spacing: .22em; text-transform: uppercase; color: var(--taupe); }

.quiz-pair { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(16px, 2.4vw, 30px); }
.quiz-pick { position: relative; padding: 0; border: 2px solid var(--ink); background: var(--paper); cursor: pointer; overflow: hidden; box-shadow: -10px 10px 0 var(--ink); transition: transform .16s var(--ease-out), box-shadow .16s var(--ease-out); }
/* Height, not ratio: both pictures have to sit on one screen together or it stops being a
   comparison and becomes two separate looks with a scroll in between. */
.quiz-pick img { width: 100%; height: clamp(260px, 56vh, 620px); object-fit: cover; display: block; }
.quiz-pick:hover, .quiz-pick:focus-visible { transform: translate(-3px, 3px); box-shadow: -7px 7px 0 var(--ink); }
.quiz-key { position: absolute; left: 12px; top: 12px; width: 24px; height: 24px; border-radius: 50%; background: var(--paper); box-shadow: 0 0 0 1.5px var(--ink); font-family: var(--font-mono); font-size: 11px; line-height: 24px; text-align: center; }
.quiz-foot { text-align: center; }
.quiz-skip {
  border: 1.5px solid var(--ink); background: transparent; color: inherit;
  font: inherit; font-size: 14px; padding: 9px 16px; border-radius: 999px; cursor: pointer;
  justify-self: start;
  transition: transform .13s var(--ease-out), box-shadow .13s var(--ease-out);
}
.quiz-skip:hover, .quiz-skip:focus-visible { transform: translate(-2px, 2px); box-shadow: -4px 4px 0 var(--ink); }
.quiz-life.is-avoid li::marker { color: var(--plum); }
.quiz-life.is-avoid li { color: var(--ink); }

.quiz-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(220px, 100%), 1fr)); gap: 14px; }
/* Text needs air before a wall of pictures, but only where the two sit inside one block.
   As a blanket rule it also fired between grid children that already had the stage gap, which
   made one space on the quick route half as big again as every other space on the page. */
[data-quiz-common] .quiz-grid, [data-quiz-chosen] .quiz-grid { margin-top: 28px; }
[data-quiz-common] .quiz-sub { margin-bottom: 12px; }
.quiz-tile { position: relative; padding: 0; border: 2px solid var(--ink); background: var(--paper); cursor: pointer; overflow: hidden; }
.quiz-tile img { width: 100%; aspect-ratio: 3 / 4; object-fit: cover; display: block; transition: opacity .16s var(--ease-out); }
.quiz-tile[aria-pressed="true"] { box-shadow: -8px 8px 0 var(--plum); }
.quiz-tile[aria-pressed="true"] img { opacity: .82; }
.quiz-tick { position: absolute; right: 10px; top: 10px; width: 26px; height: 26px; border-radius: 50%; background: var(--paper); box-shadow: 0 0 0 1.5px var(--ink); opacity: 0; transition: opacity .16s var(--ease-out); }
.quiz-tick::after { content: ""; position: absolute; left: 8px; top: 7px; width: 8px; height: 13px; border: solid var(--plum); border-width: 0 2.5px 2.5px 0; transform: rotate(45deg); }
.quiz-tile[aria-pressed="true"] .quiz-tick { opacity: 1; }
.quiz-grid-small { grid-template-columns: repeat(auto-fill, minmax(min(170px, 100%), 1fr)); }
.quiz-chosen { margin: 0; border: 2px solid var(--ink); background: var(--paper); }
.quiz-chosen img { width: 100%; aspect-ratio: 3 / 4; object-fit: cover; display: block; border-bottom: 2px solid var(--ink); }
/* the descriptions run to a dozen words now, so the caption is clamped and the full text is
   left on the image alt where a screen reader still gets all of it */
.quiz-chosen figcaption {
  padding: 10px 12px 0; margin-bottom: 10px; font-family: var(--font-mono); font-size: 10px; letter-spacing: .12em;
  text-transform: uppercase; color: var(--taupe); line-height: 1.5;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}

/* ---------- the written result ----------

   The three nearest styles sit at the top as cards, because a name is what a client repeats
   back to somebody else. The prose under them is the part that gets used in a meeting; the
   axis rails are folded away in a details block, since they are evidence rather than answer. */
/* ---------- the written questions ----------

   Picture on one side, lettered answers on the other. The options are the whole point, so they
   are set as real reading text rather than as squeezed radio labels. */
/* ---------- the quick route ----------

   A wall of words rather than a questionnaire. Chips are sized as real reading text, grouped
   with a quiet label, and the pressed state uses the same offset shadow the rest of the site
   uses for a chosen thing so it reads as picked rather than as merely hovered. */
.quiz-chips { display: grid; gap: 22px; }
.quiz-chipgroup { display: grid; gap: 10px; }
.quiz-chiprow { display: flex; flex-wrap: wrap; gap: 9px; }
.quiz-chip {
  border: 1.5px solid var(--ink); background: var(--paper); color: inherit;
  font: inherit; font-size: 14px; line-height: 1.2; padding: 9px 14px; border-radius: 999px;
  cursor: pointer; transition: transform .13s var(--ease-out), box-shadow .13s var(--ease-out), background .13s var(--ease-out);
}
.quiz-chip:hover, .quiz-chip:focus-visible { transform: translate(-2px, 2px); box-shadow: -4px 4px 0 var(--ink); }
.quiz-chip[aria-pressed="true"] { background: var(--forest); color: var(--paper); border-color: var(--forest); box-shadow: -4px 4px 0 var(--plum); }
.quiz-chip[aria-pressed="true"]:hover { transform: translate(-2px, 2px); box-shadow: -2px 2px 0 var(--plum); }
/* The easel beside the chips. It stays put while the list scrolls under it, so the picture is
   always in view of whichever word the pointer is on. Below tablet width it turns into a short
   strip pinned under the nav, picture and caption side by side, so it costs one row not a screen. */
.quiz-quickwrap { display: grid; grid-template-columns: minmax(0, 1fr) clamp(220px, 26vw, 320px); gap: clamp(24px, 4vw, 48px); align-items: start; }
.quiz-peek { margin: 0; position: sticky; top: 112px; border: 2px solid var(--ink); background: var(--paper); box-shadow: -10px 10px 0 var(--ink); }
.quiz-peek img { display: block; width: 100%; aspect-ratio: 3 / 4; object-fit: cover; background: var(--sand); }
.quiz-peek figcaption { display: grid; gap: 2px; padding: 10px 12px 12px; font-size: 13px; line-height: 1.35; }
.quiz-peek b { font-weight: 600; }
.quiz-peek span { color: var(--taupe); }
@media (max-width: 760px) {
  .quiz-quickwrap { grid-template-columns: 1fr; }
  .quiz-peek { order: -1; top: 108px; z-index: 2; display: grid; grid-template-columns: 96px 1fr; align-items: center; box-shadow: -6px 6px 0 var(--ink); }
  .quiz-peek img { aspect-ratio: 1; }
}


.quiz-ask { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: clamp(20px, 3vw, 40px); align-items: start; }
.quiz-ask-fig { margin: 0; border: 2px solid var(--ink); background: var(--paper); box-shadow: -10px 10px 0 var(--ink); }
.quiz-ask-fig img { display: block; width: 100%; height: clamp(240px, 52vh, 560px); object-fit: cover; }
.quiz-ask-fig[hidden] { display: none; }
/* No picture beside the question (the object rounds, the patterns): the answers take the full width */
.quiz-ask:has(.quiz-ask-fig[hidden]) { grid-template-columns: 1fr; }
.quiz-ask-body { display: grid; gap: 16px; }
.quiz-ask-q { margin: 0; font-size: clamp(20px, 2.6vw, 28px); line-height: 1.2; text-wrap: balance; }
.quiz-opts { display: grid; gap: 10px; }
/* five objects side by side: the picture is the question, so it gets the room and the label
   underneath only confirms what you are looking at */
.quiz-opts.is-grid { grid-template-columns: repeat(auto-fit, minmax(min(130px, 45%), 1fr)); gap: 12px; }
.quiz-objopt {
  display: grid; gap: 8px; padding: 0 0 10px; text-align: center; cursor: pointer;
  border: 1.5px solid var(--ink); background: var(--paper); color: inherit; font: inherit;
  transition: transform .14s var(--ease-out), box-shadow .14s var(--ease-out);
}
.quiz-objopt img { display: block; width: 100%; aspect-ratio: 3 / 4; object-fit: cover; }
.quiz-objopt span { padding: 0 8px; font-family: var(--font-mono); font-size: 10px; letter-spacing: .1em; text-transform: uppercase; color: var(--taupe); line-height: 1.5; }
.quiz-objopt[aria-pressed="true"] { box-shadow: -6px 6px 0 var(--plum); border-color: var(--forest); }
.quiz-objopt[aria-pressed="true"] span { color: var(--forest); }
/* A multi pick question: the tick in the corner says it is a toggle, not a choice */
.quiz-objopt[aria-pressed="true"]::after { content: ""; position: absolute; top: 8px; right: 8px; width: 22px; height: 22px; border-radius: 50%; background: var(--forest); box-shadow: 0 0 0 2px var(--paper); }
.quiz-objopt { position: relative; }
#quiz [data-quiz-anext] + .quiz-skip { margin-left: 6px; }
.quiz-brief-line { margin: 6px 0 0; font-family: var(--font-display); font-weight: 600; font-size: clamp(21px, 2.6vw, 30px); line-height: 1.15; }
[data-quiz-intentblock] { display: grid; gap: 12px; }

.quiz-objopt:hover, .quiz-objopt:focus-visible { transform: translate(-3px, 3px); box-shadow: -6px 6px 0 var(--plum); }

/* Paint chip cards. Five or six swatches in a row, the names under them in the mono label
   face, the combination's name last. Same border, shadow and pressed state as the object
   cards so the round reads as one family. */
.quiz-opts.is-palettes { grid-template-columns: repeat(auto-fill, minmax(min(210px, 100%), 1fr)); gap: 14px; }
.quiz-palopt {
  display: grid; gap: 8px; padding: 10px 10px 12px; text-align: left; cursor: pointer;
  border: 2px solid var(--ink); background: var(--paper); color: inherit; font: inherit;
  box-shadow: -6px 6px 0 var(--ink);
  transition: transform .13s var(--ease-out), box-shadow .13s var(--ease-out), border-color .13s var(--ease-out);
}
.quiz-pal-sw { display: grid; grid-auto-flow: column; grid-auto-columns: 1fr; gap: 4px; height: 96px; }
.quiz-pal-sw i { display: block; border: 1px solid rgba(0,0,0,.08); }
.quiz-pal-names { font-family: var(--font-mono); font-size: 10px; letter-spacing: .08em; text-transform: uppercase; color: var(--taupe); line-height: 1.5; }
.quiz-palopt > span:last-child { font-weight: 600; font-size: 14px; }
.quiz-palopt:hover, .quiz-palopt:focus-visible { transform: translate(-3px, 3px); box-shadow: -6px 6px 0 var(--plum); }
.quiz-palopt[aria-pressed="true"] { box-shadow: -6px 6px 0 var(--plum); border-color: var(--forest); }
@media (max-width: 760px) { .quiz-pal-sw { height: 72px; } }
/* The drawn shape and pattern cards share the paint chip card, with the drawing where the
   swatches were. Ink on sand, so a pressed card can still go forest at the border. */
.quiz-shape { display: block; height: 96px; background: var(--sand); color: var(--ink); overflow: hidden; }
.quiz-shape svg { display: block; width: 100%; height: 100%; }
.quiz-shapeopt[aria-pressed="true"] .quiz-shape { color: var(--forest); }
@media (max-width: 760px) { .quiz-shape { height: 72px; } }

.quiz-life { margin: 4px 0 0; padding-left: 20px; display: grid; gap: 8px; max-width: 62ch; }
.quiz-life li { line-height: 1.55; }

.quiz-opt {
  display: flex; gap: 12px; align-items: flex-start; text-align: left; width: 100%;
  border: 1.5px solid var(--ink); background: var(--paper); color: inherit;
  font: inherit; font-size: 15px; line-height: 1.45; padding: 13px 15px; cursor: pointer;
  transition: transform .14s var(--ease-out), box-shadow .14s var(--ease-out), background .14s var(--ease-out);
}
.quiz-opt:hover, .quiz-opt:focus-visible { transform: translate(-2px, 2px); box-shadow: -5px 5px 0 var(--plum); }
.quiz-opt-key {
  flex: 0 0 auto; width: 22px; height: 22px; border-radius: 50%; margin-top: 1px;
  background: var(--forest); color: var(--paper); font-family: var(--font-mono);
  font-size: 11px; line-height: 22px; text-align: center;
}

/* ---------- the three reads ---------- */
/* ---------- the mood board ----------

   A real composed picture, drawn on a canvas from the rooms they kept, because a board is the
   thing that gets forwarded to a partner and brought to a meeting. Framed the way the rest of
   the site frames pictures so it does not read as an export from somewhere else. */
.quiz-moodboard { display: grid; gap: 20px; justify-items: start; }
/* the frame throws a shadow twelve pixels down and left, so the button underneath needs that
   back before the gap reads as a gap at all */
.quiz-moodboard > .quiz-board-img { margin: 4px 0 16px 12px; }
.quiz-moodboard > .btn { margin-top: 4px; }
.quiz-board-img { width: calc(100% - 12px); border: 2px solid var(--ink); background: var(--paper); box-shadow: -12px 12px 0 var(--ink); }
.quiz-board-img img { display: block; width: 100%; height: auto; }
.quiz-board-img:empty { display: none; }

.quiz-reads { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr)); gap: 16px; }
/* the plain word for it leads, so it gets the emphasis the style names used to carry */
.quiz-read-card.is-feel { background: var(--paper); box-shadow: -8px 8px 0 var(--plum); }
.quiz-read-card.is-feel h3 { font-size: clamp(21px, 2.4vw, 27px); }

.quiz-read-card { border: 2px solid var(--ink); background: var(--sand); padding: 18px 18px 20px; }
.quiz-read-card h3 { margin: 0 0 10px; font-size: clamp(18px, 2vw, 22px); line-height: 1.15; text-wrap: balance; }

.quiz-sws { display: flex; flex-wrap: wrap; gap: 10px 18px; margin: 4px 0 16px; }
.quiz-sw { display: inline-flex; align-items: center; gap: 8px; font-family: var(--font-mono); font-size: 11px; letter-spacing: .12em; text-transform: uppercase; }
.quiz-sw-dot { width: 22px; height: 22px; border-radius: 50%; box-shadow: 0 0 0 1.5px var(--ink); }

@media (max-width: 720px) {
  .quiz-ask { grid-template-columns: 1fr; }
  .quiz-ask-fig img { height: clamp(200px, 34vh, 300px); }
}

.quiz-styles { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr)); gap: 16px; }
.quiz-style { border: 2px solid var(--ink); background: var(--paper); padding: 18px 18px 20px; }
.quiz-style.is-first { box-shadow: -8px 8px 0 var(--plum); }
.quiz-style-rank { margin: 0 0 8px; font-family: var(--font-mono); font-size: 10px; letter-spacing: .18em; text-transform: uppercase; color: var(--taupe); }
.quiz-style h3 { margin: 0 0 8px; font-size: clamp(19px, 2.2vw, 24px); line-height: 1.15; text-wrap: balance; }
.quiz-style-gloss { margin: 0 0 14px; font-size: 14px; line-height: 1.5; color: var(--ink); opacity: .8; }
.quiz-style-bar { height: 5px; background: rgba(20,38,29,.12); border-radius: 3px; overflow: hidden; }
.quiz-style-bar span { display: block; height: 100%; background: var(--forest); }

.quiz-words { display: grid; gap: 18px; }
.quiz-words .body-copy { margin: 0; }
.quiz-sub { margin: 10px 0 0; font-size: clamp(17px, 2vw, 20px); }
.quiz-flat { color: var(--taupe); }

.quiz-raw { border-top: 1.5px solid rgba(20,38,29,.18); padding-top: 18px; }
.quiz-raw > summary { cursor: pointer; font-family: var(--font-mono); font-size: 11px; letter-spacing: .16em; text-transform: uppercase; color: var(--taupe); }
.quiz-raw > summary::marker { color: var(--taupe); }
.quiz-raw[open] > summary { margin-bottom: 24px; }

.quiz-axes { display: grid; gap: clamp(24px, 3vw, 34px); }
.quiz-axis-ends { display: flex; justify-content: space-between; gap: 16px; margin: 0 0 10px; font-family: var(--font-mono); font-size: 10px; letter-spacing: .14em; text-transform: uppercase; color: var(--taupe); }
.quiz-axis-rail { position: relative; height: 8px; background: rgba(20,38,29,.12); border-radius: 4px; }
.quiz-axis-dot { position: absolute; top: 50%; width: 18px; height: 18px; margin: -9px 0 0 -9px; border-radius: 50%; background: var(--plum); box-shadow: 0 0 0 2px var(--paper), 0 0 0 3.5px var(--ink); }
.quiz-axis-read { margin: 12px 0 0; font-size: 14px; }

.quiz-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 16px 18px; }
.quiz-count { font-family: var(--font-mono); font-size: 11px; letter-spacing: .16em; text-transform: uppercase; color: var(--taupe); }
.quiz-note { margin: 0; font-size: 13px; color: var(--taupe); min-height: 1.4em; }
/* An empty note still costs its min-height AND a 44px grid gap either side, which opened a
   void between the rebuild button and the closing buttons. Nothing sits below it that a late
   note would disturb, so collapse it until it has something to say. */
.quiz-note:empty { display: none; }

@media (max-width: 640px) {
  /* stays two up: the whole method is a side by side, stacking them breaks it */
  .quiz-pair { grid-template-columns: 1fr 1fr; gap: 10px; }
  .quiz-pick img { height: clamp(220px, 46vh, 380px); }
  .quiz-pick { box-shadow: -7px 7px 0 var(--ink); }
  .quiz-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
}

/* ---------- the campus case study (Pickleball) ----------
   One block per area, in the order the deck walked them: a numbered head with the area's
   size and a 360 link that reads as a 360, then the plan cut with the area picked out, then
   the renderings. The 360 pill carries an icon and the word, so nobody mistakes it for a
   link to more pictures. */
.pb-area { margin-top: clamp(40px, 5vw, 64px); padding-top: 28px; border-top: 2px solid var(--ink); }
.pb-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 10px 18px; }
.pb-num { flex: none; width: 40px; height: 40px; border-radius: 50%; border: 2px solid var(--ink); display: grid; place-items: center; font-family: var(--font-mono); font-size: 13px; letter-spacing: .04em; align-self: center; }
.pb-head h3 { margin: 0; font-size: clamp(22px, 2.6vw, 30px); }
.pb-size { font-family: var(--font-mono); font-size: 11px; letter-spacing: .18em; text-transform: uppercase; color: var(--taupe); }
.pb-360 { margin-left: auto; display: inline-flex; align-items: center; gap: 8px; padding: 9px 16px 9px 12px; border: 2px solid var(--ink); border-radius: 40px; background: var(--paper); color: var(--ink); text-decoration: none; font-family: var(--font-mono); font-size: 11px; letter-spacing: .16em; text-transform: uppercase; box-shadow: -4px 4px 0 var(--ink); }
.pb-360:hover { background: var(--sand); }
.pb-360 svg { width: 22px; height: 22px; flex: none; }
.pb-lede { max-width: 66ch; margin: 14px 0 0; }
.pb-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 28px; margin-top: 24px; align-items: start; }
/* every picture in an area is the same size: two across, three across when there are three, one when there is one */
.pb-grid.cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 22px; }
.pb-grid.cols-1 { grid-template-columns: minmax(0, 1fr); }
.pb-grid .cs-plate { margin: 0; }
.pb-grid .pb-plan img { background: #fff; }
.pb-grid .pb-plan figcaption::before { content: "PLAN"; margin-right: 8px; padding: 2px 6px; border: 1px solid var(--ink); color: var(--ink); }


@media (max-width: 720px) { .pb-grid, .pb-grid.cols-3 { grid-template-columns: 1fr; gap: 16px; } .pb-360 { margin-left: 0; } }
.pb-chapter { margin-top: clamp(56px, 7vw, 96px); }
.pb-chapter .page-kicker { margin-bottom: 6px; }
.pb-tours { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr)); gap: 18px; margin-top: 28px; }
.pb-tour { display: flex; flex-direction: column; gap: 10px; padding: 18px; border: 2px solid var(--ink); background: var(--paper); box-shadow: -8px 8px 0 var(--ink); text-decoration: none; color: var(--ink); }
.pb-tour:hover { background: var(--sand); }
.pb-tour b { font-family: var(--font-mono); font-size: 11px; letter-spacing: .18em; text-transform: uppercase; display: inline-flex; align-items: center; gap: 8px; }
.pb-tour b svg { width: 22px; height: 22px; }
.pb-tour span { font-size: 15px; line-height: 1.45; color: var(--taupe); }

/* ---------- the detail round, panning ----------
   A category with a dozen or more tiles runs in one row you scroll sideways, snapping tile
   by tile, with arrows for anyone without a trackpad. Tiles keep the object card look. */
.quiz-opts.is-grid.is-scroll { display: flex; overflow-x: auto; overflow-y: hidden; overscroll-behavior-x: contain; gap: 16px; padding: 6px 6px 16px; scrollbar-width: none; touch-action: pan-y; cursor: grab; -webkit-overflow-scrolling: touch; }
.quiz-opts.is-grid.is-scroll::-webkit-scrollbar { display: none; }
.quiz-opts.is-grid.is-scroll.is-dragging { cursor: grabbing; }
.quiz-opts.is-grid.is-scroll .quiz-objopt img { pointer-events: none; -webkit-user-drag: none; user-select: none; }
.quiz-opts.is-grid.is-scroll > * { flex: 0 0 clamp(160px, 22%, 230px); }
.quiz-panbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; font-family: var(--font-mono); font-size: 11px; letter-spacing: .12em; text-transform: uppercase; color: var(--taupe); }
.quiz-panbtns { display: inline-flex; gap: 6px; }
.quiz-panbtns button { width: 36px; height: 36px; padding: 0; border-radius: 50%; border: 1.5px solid var(--ink); background: var(--paper); color: var(--ink); font-size: 16px; line-height: 1; display: grid; place-items: center; cursor: pointer; }
.quiz-panbtns button:hover { background: var(--sand); }

/* --- The quiz band, running ---------------------------------------------------------------
   The top of the style quiz used to be one flat composite picture. It is now Michelle's board
   itself, drifting: the same marquee engine the strips use, at half their pace (data-drift on
   the section). The figures keep the .strip-fig class because the engine reserves anything in
   a strip, which is what stops a picture appearing both up here and on a word further down. */
/* The band runs on a CSS animation rather than the JS marquee: it is decoration, nobody needs
   to drag it, and a keyframe keeps moving where a requestAnimationFrame loop can be throttled.
   The track holds the same pins twice, so travelling exactly -50% lands on the seam invisibly. */
.quiz-band-run { overflow: hidden; cursor: grab; touch-action: pan-y; }
.quiz-band-run.is-dragging { cursor: grabbing; }
.quiz-band-run img { user-select: none; -webkit-user-drag: none; }

.quiz-band-track { display: flex; gap: 0; width: max-content; height: 100%; padding: 0; animation: quiz-band-run 860s linear infinite; will-change: transform; }
@keyframes quiz-band-run { from { transform: translateX(0); } to { transform: translateX(-50%); } }
@media (prefers-reduced-motion: reduce) { .quiz-band-track { animation: none; } }
.quiz-band-fig {
  flex: none; margin: 0; padding: 0; height: 100%;
  width: clamp(200px, 24vw, 330px);
  background: none; box-shadow: none; border-right: 2px solid var(--ink);
}
.quiz-band-fig img { width: 100%; height: 100%; object-fit: cover; display: block; }

/* --- Make a board -------------------------------------------------------------------------
   The quiz library with a pin on every picture. Tiles are the quiz tile, so a pinned picture
   reads exactly like a chosen one there: plum drop shadow, a tick in the corner. The bar along
   the foot is the only fixed thing on the page and it carries one number and one button. */
.pb-stage { display: grid; gap: clamp(24px, 3vw, 40px); }
.pb-stage[hidden] { display: none; }
.pb-tools { display: grid; gap: 16px; }
.pb-chips { display: flex; flex-wrap: wrap; gap: 10px; }
.pb-search { display: flex; align-items: center; gap: 12px; max-width: 520px; border: 2px solid var(--ink); background: var(--paper); padding: 0 0 0 14px; }
.pb-search-label { font-family: var(--font-mono); font-size: 11px; letter-spacing: .16em; text-transform: uppercase; color: var(--taupe); }
.pb-search input { flex: 1; border: 0; background: transparent; font: inherit; font-size: 16px; padding: 12px 14px 12px 0; color: inherit; min-width: 0; }
.pb-search input:focus-visible { outline: 2px solid var(--leaf); outline-offset: -2px; }
.pb-count { margin: 0; font-family: var(--font-mono); font-size: 11px; letter-spacing: .16em; text-transform: uppercase; color: var(--taupe); }
.pb-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(200px, 100%), 1fr)); gap: 18px; }
.pb-tile { position: relative; margin: 0; border: 2px solid var(--ink); background: var(--paper); overflow: hidden; transition: box-shadow .16s var(--ease-out), transform .16s var(--ease-out); }
.pb-tile img { width: 100%; aspect-ratio: 3 / 4; object-fit: cover; display: block; }
.pb-tile figcaption { padding: 8px 10px 10px; font-size: 12px; line-height: 1.35; color: var(--taupe); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.pb-tile figcaption b { color: var(--ink); font-weight: 600; }
.pb-tile.is-pinned { box-shadow: -8px 8px 0 var(--plum); }
.pb-tile.is-pinned img { opacity: .88; }
.pb-pin { position: absolute; right: 10px; top: 10px; width: 34px; height: 34px; border-radius: 50%; border: 0; padding: 0; background: var(--paper); box-shadow: 0 0 0 1.5px var(--ink); cursor: pointer; transition: transform .16s var(--ease-out); }
.pb-pin:hover, .pb-pin:focus-visible { transform: scale(1.1); outline: none; box-shadow: 0 0 0 2.5px var(--leaf); }
.pb-pin-mark { position: absolute; left: 50%; top: 50%; width: 12px; height: 12px; margin: -6px 0 0 -6px; border-radius: 50%; border: 2.5px solid var(--plum); transition: background .16s var(--ease-out); }
.pb-pin[aria-pressed="true"] { background: var(--plum); box-shadow: 0 0 0 1.5px var(--plum); }
.pb-pin[aria-pressed="true"] .pb-pin-mark { border-color: var(--paper); background: var(--paper); }
.pb-more { margin: 0; text-align: center; }
.pb-grid-board .pb-tile { cursor: grab; }
.pb-grid-board .pb-tile.is-dragging { opacity: .4; }
.pb-grid-board .pb-tile.is-over { box-shadow: -8px 0 0 var(--leaf); }
.pb-name-label { display: block; font-family: var(--font-mono); font-size: 11px; letter-spacing: .16em; text-transform: uppercase; color: var(--taupe); margin-bottom: 8px; }
.pb-name { width: 100%; max-width: 520px; border: 0; border-bottom: 2px solid var(--ink); background: transparent; font-family: var(--font-display); font-weight: 600; font-size: clamp(24px, 3vw, 34px); padding: 6px 0; color: var(--ink); }
.pb-name:focus-visible { outline: none; border-bottom-color: var(--leaf); }
.pb-name::placeholder { color: var(--taupe); opacity: .6; }
.pb-hint { margin-top: 12px; max-width: 62ch; }
.pb-actions { margin-top: 8px; }
.pb-status { min-height: 1.5em; color: var(--leaf); font-weight: 600; }
.pb-note-copy { max-width: 62ch; color: var(--taupe); }
.pb-hero-actions { margin-top: 28px; }
.pb-hero-actions .btn-outline { color: var(--paper); border-color: var(--paper); }
.pb-hero-actions .btn-outline:hover { background: var(--paper); color: var(--ink); }
.pb-grid-board .pb-tile { display: flex; flex-direction: column; }
.pb-note { display: block; width: 100%; margin: 0; padding: 8px 10px 10px; border: 0; border-top: 1px dashed var(--taupe); background: transparent; font: inherit; font-size: 13px; line-height: 1.4; color: var(--ink); resize: vertical; min-height: 54px; cursor: text; }
.pb-note::placeholder { color: var(--taupe); opacity: .8; font-style: italic; }
.pb-note:focus-visible { outline: none; background: rgba(255,255,255,.55); border-top-color: var(--leaf); }
.pb-read { margin-top: 48px; padding: clamp(28px, 4vw, 48px) clamp(20px, 4vw, 52px); background: #175844; color: var(--paper); border-radius: 6px; position: relative; overflow: hidden; }
.pb-read::before { content: ""; position: absolute; inset: 0; background: radial-gradient(120% 80% at 100% 0%, rgba(203,220,206,.10), transparent 55%); pointer-events: none; }
.pb-read > * { position: relative; }
.pb-read-head { display: flex; justify-content: space-between; align-items: baseline; gap: 16px; padding-bottom: 18px; border-bottom: 1px solid rgba(243,230,206,.18); }
.pb-read-kicker { color: #CBDCCE; margin: 0; }
.pb-read-meta { margin: 0; font-family: var(--font-mono); font-size: 11px; letter-spacing: .14em; text-transform: uppercase; color: #CBDCCE; }
.pb-read-grid { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); gap: clamp(24px, 4vw, 56px); margin-top: 32px; align-items: start; }
.pb-read-word { margin: 0 0 12px; font-family: var(--font-mono); font-size: 12px; letter-spacing: .16em; text-transform: uppercase; color: #CBDCCE; }
.pb-read-word b { font-family: var(--font-display); font-size: 22px; letter-spacing: .04em; color: var(--paper); text-transform: none; font-weight: 600; margin-left: 6px; }
.pb-read-title { font-family: var(--font-display); font-weight: 600; font-size: clamp(28px, 3.6vw, 46px); line-height: 1.08; letter-spacing: -.01em; margin: 0; max-width: 18ch; text-wrap: balance; color: var(--paper); }
.pb-read-text { font-size: clamp(16px, 1.4vw, 18px); line-height: 1.55; font-weight: 420; color: #DCE6DD; max-width: 58ch; margin: 20px 0 0; text-wrap: pretty; }
.pb-spec { margin: 30px 0 0; display: flex; flex-direction: column; }
.pb-spec-row { display: grid; grid-template-columns: 110px minmax(0, 1fr); gap: 16px; padding: 12px 0; border-top: 1px solid rgba(243,230,206,.16); align-items: baseline; }
.pb-spec-row:last-child { border-bottom: 1px solid rgba(243,230,206,.16); }
.pb-spec dt { font-family: var(--font-mono); font-size: 11px; letter-spacing: .16em; text-transform: uppercase; color: #CBDCCE; }
.pb-spec dd { margin: 0; font-family: var(--font-display); font-weight: 500; font-size: clamp(14px, 1.2vw, 17px); line-height: 1.45; color: var(--paper); }
.pb-spec-dot { display: inline-block; width: 5px; height: 5px; border-radius: 50%; background: var(--leaf); margin: 0 12px; vertical-align: middle; }
.pb-poster { position: relative; overflow: hidden; border-radius: 6px; min-height: 100%; display: flex; align-items: center; justify-content: center; padding: clamp(28px, 9%, 64px) clamp(20px, 8%, 56px); isolation: isolate; box-shadow: 0 0 0 1px rgba(243,230,206,.14); }
.pb-poster-bg { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: -1; filter: saturate(.9); }
.pb-poster::after { content: ""; position: absolute; inset: 0; background: rgba(0,0,0,.18); z-index: -1; }
.pb-cards { width: 100%; max-width: 400px; display: flex; flex-direction: column; gap: 9px; }
.pb-card { padding: 18px 20px 14px; border-radius: 3px; color: var(--ink); box-shadow: 0 18px 40px -14px rgba(0,0,0,.55), 0 2px 6px rgba(0,0,0,.18); }
.pb-card.is-dark { color: var(--paper); }
.pb-card-name { margin: 0 0 10px; font-family: var(--font-display); font-weight: 600; font-size: clamp(17px, 1.7vw, 23px); letter-spacing: .12em; text-transform: uppercase; line-height: 1.1; }
.pb-card-votes { float: right; font-family: var(--font-mono); font-size: 10px; letter-spacing: .1em; text-transform: uppercase; opacity: .75; margin-top: 8px; }
.pb-card-table { width: 100%; border-collapse: collapse; font-family: var(--font-mono); font-size: 10px; letter-spacing: .1em; text-transform: uppercase; }
.pb-card-table th, .pb-card-table td { border: 1px solid currentColor; padding: 5px 6px; text-align: center; font-weight: 500; opacity: .9; }
.pb-card-table th { opacity: .65; font-size: 9px; }
.pb-own { margin-top: 40px; }
.pb-own-label { font-family: var(--font-mono); font-size: 11px; letter-spacing: .16em; text-transform: uppercase; color: #CBDCCE; margin: 0 0 14px; }
.pb-quotes { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 12px 28px; }
.pb-quote { margin: 0; padding-left: 16px; border-left: 2px solid var(--leaf); font-family: var(--font-display); font-style: italic; font-weight: 500; font-size: 17px; line-height: 1.4; color: var(--paper); }
@media (max-width: 820px) {
  .pb-read-grid { grid-template-columns: 1fr; }
  .pb-poster { min-height: 0; padding: 24px; }
  .pb-spec-row { grid-template-columns: 1fr; gap: 6px; }
}
.pb-read + .pb-actions { margin-top: 28px; }
.pb-bar { position: sticky; bottom: 0; z-index: 4; display: flex; align-items: center; justify-content: space-between; gap: 16px; margin: 48px calc(-1 * var(--gutter, 20px)) 0; padding: 12px var(--gutter, 20px); background: var(--paper); border-top: 2px solid var(--ink); }
.pb-bar-count { font-family: var(--font-mono); font-size: 12px; letter-spacing: .14em; text-transform: uppercase; }

/* The board page's sign reads PINS while browsing and YOUR BOARD once the board is open. The
   second word starts a cell's-worth lower so the two read as two words, not one. */
.sign span.sign-word { margin-top: calc(var(--cell) * .38); }

/* Before and after, on the case studies. Two plates side by side: the rendering that was
   signed off, and the photo of the built room. Until a photo lands the right plate is a slot
   that says so, in the same frame, so the pair reads as a pair even while it waits. */
.ba-list { display: grid; gap: 40px; margin-top: 8px; }
/* A rendering and a photo share one height: each plate grows in proportion to its picture's
   shape, after its fixed frame (2px border and 12px padding a side). Stacked on a phone. */
.ba-pair { display: flex; gap: 32px; align-items: flex-start; }
.ba-pair > .cs-plate { margin: 0; flex: var(--ar, 1) 1 28px; min-width: 0; box-sizing: border-box; }
@media (max-width: 700px) { .ba-pair { flex-direction: column; } .ba-pair > .cs-plate { flex: none; width: 100%; } }
.ba-slot { aspect-ratio: 16 / 9; display: flex; align-items: center; justify-content: center; text-align: center; padding: 20px; box-shadow: inset 0 0 0 2px var(--ink); background: repeating-linear-gradient(135deg, rgba(20,38,29,.06) 0 10px, transparent 10px 20px); }
.ba-slot p { margin: 0; font-family: var(--font-mono); font-size: 11px; letter-spacing: .16em; text-transform: uppercase; color: var(--taupe); }

/* Social posts on a case study, set in the same plate as every other picture. Every clip gets
   the same 4:5 window so a row of them reads as one set, whatever shape the post is. Instagram's
   player is a fixed 54px header, then a 4:5 media box as wide as the frame, then a comment
   footer: the window starts below the header and ends at the foot of the media. TikTok's player
   fits its video inside whatever box it is given. */
.social-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr)); gap: 40px 32px; margin-top: 32px; align-items: start; }
.social-plate { margin: 0; }
.social-frame { position: relative; overflow: hidden; aspect-ratio: 4 / 5; background: #0d0f0e; box-shadow: inset 0 0 0 2px var(--ink); }
.social-frame iframe { position: absolute; left: 0; width: 100%; border: 0; display: block; }
.ig-frame iframe { top: -54px; height: calc(100% + 54px + 320px); }
.tt-frame iframe { top: 0; height: 100%; }
/* the ink edge drawn over the player, which would otherwise paint over the inset border */
.social-frame::after { content: ""; position: absolute; inset: 0; box-shadow: inset 0 0 0 2px var(--ink); pointer-events: none; }
.social-plate figcaption { justify-content: flex-start; }
.social-plate figcaption a { margin-left: auto; color: inherit; text-decoration: none; border-bottom: 1px solid currentColor; }
.social-plate figcaption a:hover { color: var(--ink); }

/* One clip on its own keeps a phone-sized plate instead of stretching across the page. */
.social-row > .social-plate:only-child { max-width: 420px; }

/* The Before & After menu lands on the photos, clear of the fixed bar. */
#photos, #built { scroll-margin-top: 116px; }

/* Standalone photos of built rooms: every tile the same tall shape, the picture filling its
   frame, so a wide photo in a row of tall ones never leaves an empty plate under it. */
#photos .cs-row { align-items: start; }
#photos .cs-row .cs-plate img { aspect-ratio: 3 / 4; object-fit: cover; }

/* The Find Your Style picture band, reused on Services above Design development: same height
   and drift as on the quiz page, with an ink rule on both edges so it sits between sections. */
.svc-band .quiz-band { border-top: 2px solid var(--ink); }

/* Services bands for drawings and renderings: wider tiles than the quiz band. Drawings are shown
   whole on white, so a title block or a dimension line is never cropped away; renderings and the
   walkthrough clips fill their tiles edge to edge. */
.band-sheets .quiz-band-fig, .band-renders .quiz-band-fig { width: clamp(300px, 42vw, 600px); }
.band-sheets .quiz-band-fig { background: #fff; }
.band-sheets .quiz-band-fig img { object-fit: contain; padding: 14px; box-sizing: border-box; }
.band-video video { width: 100%; height: 100%; object-fit: cover; display: block; pointer-events: none; }

/* Tables on the case studies and Sourcing, dressed like the Services menu card: a rounded sand
   card with a fine ink edge and no heavy drop shadow, mono labels across the head, the first
   column set as bold tracked names, the rest in plain body type, and a light rule between rows. */
.spec {
  max-width: 860px; margin-top: 32px;
  background: var(--sand); border: 1.5px solid var(--ink); border-radius: 14px;
  box-shadow: none; overflow: hidden;
}
.spec th, .spec td { padding: 14px 20px; text-transform: none; letter-spacing: 0; font-size: 14px; vertical-align: top; }
.spec thead th {
  font-family: var(--font-mono); font-size: 10px; font-weight: 400; letter-spacing: .24em;
  text-transform: uppercase; border-bottom: 1.5px solid var(--ink); padding-top: 18px; padding-bottom: 14px;
}
.spec tbody td { border-top: 1px solid rgba(20, 38, 29, .16); font-weight: 400; color: var(--taupe); line-height: 1.45; }
.spec tbody tr:first-child td { border-top: 0; }
.spec th:first-child, .spec td:first-child { width: auto; border-right: 0; }
.spec tbody td:first-child { color: var(--ink); font-weight: 700; font-size: 12.5px; letter-spacing: .1em; text-transform: uppercase; }
.spec tbody td:last-child { font-family: var(--font-mono); font-size: 11px; letter-spacing: .06em; text-transform: uppercase; color: var(--plum); }
@media (max-width: 760px) {
  .spec { border-radius: 10px; }
  .spec th, .spec td { padding: 11px 14px; }
}

/* Tables, take two: the shop-page look. Flat sage, square corners, thin black rules on every
   edge and between columns, everything in a typewriter mono at one size, black on sage, the
   first column bold. No card, no shadow, no colour accents. */
.spec {
  max-width: 860px; margin-top: 32px;
  background: var(--sand); border: 1px solid var(--ink); border-radius: 0;
  box-shadow: none; overflow: visible; border-collapse: collapse;
}
.spec th, .spec td {
  padding: 13px 18px; vertical-align: top; text-align: left;
  font-family: "Courier Prime", "Courier New", var(--font-mono);
  font-size: 13.5px; line-height: 1.5; letter-spacing: 0; text-transform: none;
  color: var(--ink); font-weight: 400;
  border: 1px solid var(--ink);
}
.spec thead th { font-weight: 700; border-bottom: 1px solid var(--ink); padding-top: 15px; padding-bottom: 15px; letter-spacing: 0; }
.spec tbody td { border-top: 1px solid var(--ink); color: var(--ink); }
.spec tbody tr:first-child td { border-top: 1px solid var(--ink); }
.spec tbody td:first-child { font-weight: 700; font-size: 13.5px; letter-spacing: 0; text-transform: none; color: var(--ink); }
.spec tbody td:last-child { font-family: "Courier Prime", "Courier New", var(--font-mono); font-size: 13.5px; letter-spacing: 0; text-transform: none; color: var(--ink); }
.spec th:first-child, .spec td:first-child { border-left: 1px solid var(--ink); }
@media (max-width: 760px) {
  .spec { border-radius: 0; }
  .spec th, .spec td { padding: 10px 12px; font-size: 12.5px; }
}
/* room for the numbers to stay on one line */
.spec { max-width: 980px; }
.spec td { white-space: normal; }
.spec td:nth-child(3) { white-space: nowrap; }
@media (max-width: 760px) { .spec td:nth-child(3) { white-space: normal; } }
/* On a phone the grid stays a real table that fits the screen, rather than a sideways scroller
   whose last column slides under the edge. */
@media (max-width: 760px) {
  .spec { display: table; width: 100%; overflow: visible; table-layout: auto; }
  .spec th, .spec td { padding: 8px 7px; font-size: 11.5px; white-space: normal; word-break: normal; overflow-wrap: anywhere; }
  .spec th:first-child, .spec td:first-child { position: static; }
}
@media (max-width: 760px) {
  .spec th, .spec td { overflow-wrap: normal; padding: 8px 6px; font-size: 11px; }
  .spec tbody td:first-child { font-size: 10.5px; }
}
@media (max-width: 760px) { .spec tbody td:last-child { font-size: 11px; } }

/* A table beside a rendering, the shop-page layout: one ruled frame, picture on the left, the
   grid on the right, a single black line between them. Stacked on a phone, picture first. */
.spec-split {
  display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.25fr);
  max-width: 1120px; margin-top: 32px;
  border: 1px solid var(--ink); background: var(--sand);
}
.spec-img { margin: 0; border-right: 1px solid var(--ink); min-height: 320px; overflow: hidden; }
.spec-img img { display: block; width: 100%; height: 100%; object-fit: cover; }
.spec-split .spec { margin: 0; max-width: none; width: 100%; border: 0; align-self: start; }
.spec-split .spec tr > :first-child { border-left: 0; }
.spec-split .spec tr > :last-child { border-right: 0; }
.spec-split .spec thead tr:first-child > * { border-top: 0; }
.spec-split .spec tbody tr:last-child td { border-bottom: 0; }
@media (max-width: 760px) {
  .spec-split { grid-template-columns: 1fr; }
  .spec-img { border-right: 0; border-bottom: 1px solid var(--ink); min-height: 0; aspect-ratio: 4 / 3; }
}
/* the picture fills the whole height of its side, however long the table runs */
.spec-img { position: relative; }
.spec-img img { position: absolute; inset: 0; }
@media (max-width: 760px) { .spec-img img { position: static; } }

/* the table's column rules run the full height of the frame, even when the picture is taller */
.spec-split .spec { height: 100%; }

/* Tables, take three: back to the Services-card look (rounded sand card, mono labels, bold
   tracked names, light rules between rows, plum charge column), keeping the rendering beside it. */
.spec {
  max-width: 860px; margin-top: 32px; height: auto;
  background: var(--sand); border: 1.5px solid var(--ink); border-radius: 14px;
  box-shadow: none; overflow: hidden; border-collapse: separate; border-spacing: 0;
}
.spec th, .spec td {
  padding: 14px 20px; vertical-align: top; text-align: left; border: 0;
  font-family: Archivo, "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-size: 14px; line-height: 1.45; letter-spacing: 0; text-transform: none; font-weight: 400;
}
.spec thead th {
  font-family: var(--font-mono); font-size: 10px; font-weight: 400; letter-spacing: .24em; text-transform: uppercase;
  color: var(--ink); border-bottom: 1.5px solid var(--ink); padding-top: 18px; padding-bottom: 14px;
}
.spec tbody td { border-top: 1px solid rgba(20, 38, 29, .16); color: var(--taupe); }
.spec tbody tr:first-child td { border-top: 0; }
.spec th:first-child, .spec td:first-child { border-left: 0; width: auto; }
.spec tbody td:first-child { color: var(--ink); font-weight: 700; font-size: 12.5px; letter-spacing: .1em; text-transform: uppercase; }
.spec tbody td:last-child { font-family: var(--font-mono); font-size: 11px; letter-spacing: .06em; text-transform: uppercase; color: var(--plum); }

/* the frame around picture and table takes the card's rounded edge; the table inside loses its own */
.spec-split { border: 1.5px solid var(--ink); border-radius: 14px; overflow: hidden; background: var(--sand); }
.spec-img { border-right: 1.5px solid var(--ink); }
.spec-split .spec { border: 0; border-radius: 0; margin: 0; max-width: none; height: auto; }

@media (max-width: 760px) {
  .spec, .spec-split { border-radius: 10px; }
  .spec-img { border-right: 0; border-bottom: 1.5px solid var(--ink); }
  .spec th, .spec td { padding: 9px 8px; font-size: 12px; }
  .spec tbody td:first-child { font-size: 10.5px; letter-spacing: .06em; }
  .spec tbody td:last-child { font-size: 10px; letter-spacing: .03em; }
  .spec thead th { font-size: 9px; letter-spacing: .12em; }
}
#calls { scroll-margin-top: 116px; }

/* Signs inside one long section (the quiz start page): each run of blocks carries its own rail,
   lined up with the section-level signs in the page gutter. The run keeps the stage's spacing. */
.sign-in { position: relative; display: grid; gap: clamp(28px, 4vw, 44px); }
@media (min-width: 1404px) {
  .sign-in .sign-rail { position: absolute; z-index: 3; top: 0; bottom: 0; left: -122px; width: max-content; pointer-events: none; }
  .sign-in .sign { display: flex; flex-direction: column; position: sticky; top: 116px; --cell: 40px;
    gap: calc(var(--cell) * .03); padding: calc(var(--cell) * .38) calc(var(--cell) * .34);
    background: var(--sign-bg, var(--cream)); border-radius: calc(var(--cell) * .15); box-shadow: 0 6px 20px rgba(20, 38, 29, .28); }
}

/* A short silent clip at the top of a card: a real Loom, cropped to the model or the Drive folder. */
.card-clip { margin: 0 0 20px; border: 1.5px solid var(--ink); background: var(--ink); overflow: hidden; }
.card-clip video { display: block; width: 100%; aspect-ratio: 16 / 10; object-fit: cover; object-position: top left; }
.card-clip video { object-position: center; }

/* Fewer calls: two framed plates with the Loom clips, text inside the plate under each clip, and
   the third point as a ruled note across the full width instead of a half-empty card. */
.calls-media { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(360px, 100%), 1fr)); gap: 40px; margin-top: 40px; align-items: stretch; }
.calls-plate { margin: 0; display: flex; flex-direction: column; }
.calls-plate video { display: block; width: 100%; aspect-ratio: 16 / 10; object-fit: cover; background: var(--ink); box-shadow: inset 0 0 0 2px var(--ink); }
.calls-plate figcaption { display: block; padding: 20px 10px 18px; font-family: inherit; font-size: inherit; letter-spacing: 0; text-transform: none; color: var(--ink); }
.calls-plate h3 { font-size: 20px; font-weight: 700; }
.calls-plate p { margin-top: 8px; font-size: 15px; line-height: 1.55; color: var(--taupe); max-width: 52ch; }
.calls-note { display: grid; grid-template-columns: minmax(180px, 240px) 1fr; gap: 24px; align-items: baseline; margin-top: 44px; padding: 22px 0; border-top: 1.5px solid var(--ink); border-bottom: 1.5px solid var(--ink); }
.calls-note p { margin: 0; font-size: 16px; line-height: 1.5; }
.calls-note-label { display: flex; align-items: center; gap: 9px; font-family: var(--font-mono); font-size: 11px !important; letter-spacing: .18em; text-transform: uppercase; }
@media (max-width: 700px) { .calls-note { grid-template-columns: 1fr; gap: 8px; } }

/* Email plate: the site's mail window, cut to the same frame as the two clips beside it. */
@media (min-width: 1000px) { .calls-media { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 32px; } }
.calls-mail { margin: 0; box-shadow: none; aspect-ratio: 16 / 10; overflow: hidden; display: flex; flex-direction: column; }
.calls-mail .mail-bar { padding-block: 6px; }
.calls-mail .mail-fields { padding: 6px 14px; }
.calls-mail .mail-fields > div { grid-template-columns: 58px minmax(0, 1fr); padding: 2px 0; }
.calls-mail .mail-fields span:first-child { font-size: 9px; }
.calls-mail .mail-fields span + span { font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.calls-mail .mail-body { padding: 10px 14px; }
.calls-mail .mail-body p { font-size: 13px; line-height: 1.45; }
.calls-mail .mail-body p + p { margin-top: 6px; }

/* Homepage invitation into the style quiz: the pin strip runs full bleed, the words sit under it. */
.quiz-invite { padding: 0 0 clamp(48px, 6vw, 76px); }
.quiz-invite-in { padding-top: clamp(52px, 6vw, 84px); max-width: 760px; }
.quiz-invite-label { font-family: var(--font-mono); font-size: 11px; letter-spacing: .18em; text-transform: uppercase; color: var(--taupe); display: flex; align-items: center; gap: 9px; }
.quiz-invite h2 { margin-top: 14px; }
.quiz-invite .body-copy { margin-top: 14px; max-width: 58ch; }
.quiz-invite-cta { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 24px; }

/* A hairline under the quiz invitation, so it reads as its own block and does not run into the
   argument below it. Inset to the container so it lines up with the text above it. */
.quiz-invite-in { border-bottom: 1.5px solid var(--ink); padding-bottom: clamp(36px, 5vw, 56px); }

/* The homepage work band with the picture strip taken out. Without the strip, the band's own
   padding plus the strip's top margin left a tall empty green field, so the row of routes into
   the portfolio sits tight in the band instead. */
.collage-band:not(:has(.marquee)) { --band-pad: clamp(36px, 4.5vw, 56px); }
.collage-band:not(:has(.marquee)) .work-index { margin-top: 0; }

/* The homepage figures sat right under the trust cards, and those cards carry a 20px offset
   shadow, so the gap read as almost nothing. Clear the shadow, and match the gap the cards
   themselves keep between rows. */
.stats { margin-top: 44px; }
/* Even air on both sides of the rule under the quiz invitation: its own bottom padding equals
   the top padding of the section that follows. */
.quiz-invite-in { padding-bottom: clamp(48px, 6vw, 76px); }
.quiz-invite { padding-bottom: 0; }

/* The four stage cards. Long link names were breaking so the arrow fell onto a line by itself,
   which read as a stray character. The arrow now stays with the last word, lines wrap evenly,
   and the cards get a little more width and inner room so the names mostly fit on one line. */
.svc { padding: 36px 40px; }
@media (min-width: 1080px) { .svc { padding: 40px 36px; } }
.svc-links a { display: inline-block; text-wrap: balance; }
.svc-links a::after { content: ""; }
.svc h3, .svc p { text-wrap: pretty; }

/* Film strip: the walkthrough films as a wide shallow band across the page. Height is capped so
   it stays a strip at any width, and the picture is cropped to fill rather than letterboxed. */
.film-strip { position: relative; border-top: 2px solid var(--ink); border-bottom: 2px solid var(--ink); background: var(--ink); }
.film-strip video { display: block; width: 100%; height: clamp(150px, 19vw, 300px); object-fit: cover; }
.film-strip-cap { position: absolute; left: 0; bottom: 0; display: flex; align-items: center; gap: 9px; margin: 0; padding: 8px 16px; background: var(--paper); border-top: 2px solid var(--ink); border-right: 2px solid var(--ink); font-family: var(--font-mono); font-size: 11px; letter-spacing: .16em; text-transform: uppercase; }
.film-strip-cap a { color: var(--plum); }
@media (max-width: 640px) { .film-strip video { height: 140px; } .film-strip-cap { font-size: 10px; letter-spacing: .12em; } }

/* Taller strip: Michelle asked for more of the picture, so the band grows and the clip is cut
   from a deeper slice of the frame rather than being blown up. */
.film-strip video { height: clamp(210px, 27vw, 430px); }
@media (max-width: 640px) { .film-strip video { height: 190px; } }

/* Taller again, on request. The clip is encoded at 1400x780 so the band can grow without the
   picture softening, and every shot is cropped to fill rather than stretched. */
.film-strip video { height: clamp(260px, 33vw, 540px); }
@media (max-width: 640px) { .film-strip video { height: 220px; } }

/* Client reviews. Cards in a masonry-ish grid: the quote is the object, the project name is the
   label above it and the client's name signs it off. No stars: ten identical five-star rows read
   as decoration, the words are the evidence. */
.review-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr)); gap: 24px; margin-top: 40px; align-items: start; }
.review { margin: 0; background: var(--cream); border: 1.5px solid var(--ink); box-shadow: -10px 10px 0 var(--ink); padding: 26px 26px 22px; }
.review-project { display: flex; align-items: center; gap: 9px; margin: 0 0 14px; font-family: var(--font-mono); font-size: 11px; letter-spacing: .16em; text-transform: uppercase; color: var(--taupe); }
.review blockquote { margin: 0; }
.review blockquote p { font-size: 16px; line-height: 1.55; color: var(--ink); text-wrap: pretty; }
.review figcaption { margin-top: 16px; padding-top: 12px; border-top: 1.5px solid var(--ink); font-weight: 700; font-size: 15px; }
@media (max-width: 640px) { .review { padding: 20px; box-shadow: -7px 7px 0 var(--ink); } }


/* --- Reviews page ---------------------------------------------------------------------------
   Three parts: a row of figures, three short quotes set large, then the rest in columns. The
   columns are CSS multi-column, not a grid: reviews run from two lines to twenty, and a grid
   leaves a ragged hole under every short one. Cards cannot break across columns. */
.review-figures { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(200px, 100%), 1fr)); gap: 1.5px; background: var(--ink); border: 1.5px solid var(--ink); }
.review-figures > div { background: var(--cream); padding: 24px 26px; }
.review-figures strong { display: block; font-family: var(--font-display); font-size: clamp(34px, 5vw, 52px); line-height: 1; color: var(--leaf); }
.review-figures span { display: block; margin-top: 8px; font-family: var(--font-mono); font-size: 11px; letter-spacing: .16em; text-transform: uppercase; color: var(--taupe); }
.review-leads { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr)); gap: 28px; margin-top: clamp(32px, 4vw, 48px); }
.review-lead { margin: 0; padding: 0 0 0 18px; border-left: 3px solid var(--plum); }
.review-lead blockquote { margin: 0; }
.review-lead blockquote p { font-size: clamp(18px, 2vw, 22px); line-height: 1.4; font-weight: 420; text-wrap: pretty; }
.review-lead figcaption, .review figcaption { margin-top: 14px; font-weight: 700; font-size: 15px; }
.review-lead figcaption span, .review figcaption span { display: block; margin-top: 3px; font-family: var(--font-mono); font-size: 10px; font-weight: 400; letter-spacing: .16em; text-transform: uppercase; color: var(--taupe); }
.review-cols { margin-top: 32px; columns: 3 300px; column-gap: 24px; }
.review-cols .review { break-inside: avoid; display: inline-block; width: 100%; margin: 0 0 24px; background: var(--cream); border: 1.5px solid var(--ink); box-shadow: none; padding: 22px 24px; }
.review-cols .review blockquote { margin: 0; }
.review-cols .review blockquote p { font-size: 15px; line-height: 1.55; text-wrap: pretty; }
@media (max-width: 640px) { .review-cols { columns: 1; } .review-lead { padding-left: 14px; } }

/* A picture band butts straight onto the section under it. The band is a full-bleed block of
   pictures, so the 76px the next band-section normally keeps above itself reads as a hole. */
.svc-band + :is(.services-band, .quote-band, .process-band, .section-band, .collage-band, .band-forest) { margin-top: 0; }
:is(.services-band, .quote-band, .process-band, .section-band, .collage-band, .band-forest):has(+ .svc-band) { margin-bottom: 0; }

/* ---- the quick quiz picking tray -------------------------------------------------
   Tapping a word opens fifteen of Michelle's rooms for that word, drawn from her whole
   list so the run is fresh, and few enough on screen that nobody feels examined. */
.quiz-tray{background:var(--paper);border-radius:22px;padding:22px 22px 26px;margin:22px 0 8px}
.quiz-tray[hidden]{display:none}
.quiz-trayhead{display:flex;flex-wrap:wrap;gap:14px;align-items:baseline;
  justify-content:space-between;margin-bottom:16px}
.quiz-trayhead h3{font-family:var(--font-display);font-size:1.25rem;margin:0}
.quiz-trayhead p{margin:2px 0 0;font-size:.9rem;opacity:.75}
.quiz-traybtns{display:flex;gap:8px;flex-wrap:wrap}
.quiz-tray15{display:grid;grid-template-columns:repeat(5,1fr);gap:12px}
@media(max-width:900px){.quiz-tray15{grid-template-columns:repeat(3,1fr)}}
@media(max-width:560px){.quiz-tray15{grid-template-columns:repeat(2,1fr)}}
.quiz-tray15 .quiz-pick{position:relative;display:block;width:100%;padding:0;border:0;
  border-radius:14px;overflow:hidden;aspect-ratio:3/4;background:var(--sand);cursor:pointer}
.quiz-tray15 .quiz-pick img{width:100%;height:100%;object-fit:cover;display:block;
  transition:transform .35s ease}
.quiz-tray15 .quiz-pick:hover img{transform:scale(1.04)}
.quiz-tray15 .quiz-pick .tick{position:absolute;inset:auto 8px 8px auto;width:26px;height:26px;
  border-radius:50%;display:grid;place-items:center;font-size:.95rem;opacity:0;
  background:rgba(249,240,222,.9);color:var(--ink)}
.quiz-tray15 .quiz-pick[aria-pressed=true]{outline:3px solid var(--leaf);outline-offset:-3px}
.quiz-tray15 .quiz-pick[aria-pressed=true] .tick{opacity:1;background:var(--leaf);color:#fff}
.quiz-chip .quiz-tally{display:inline-block;margin-left:7px;background:var(--tan);color:var(--ink);
  border-radius:999px;font-size:.75rem;font-weight:700;min-width:20px;text-align:center;padding:0 6px}
.quiz-chip[data-kept="0"] .quiz-tally,.quiz-chip:not([data-kept]) .quiz-tally{display:none}

/* Michelle's note about the board, on the quiz page. Narrow measure, a little air above it,
   and a hairline so it reads as her talking rather than as more instructions. */
.quiz-note{max-width:660px;border-top:1px solid var(--rule);padding-top:30px}
.quiz-note h2{font-family:var(--font-display);margin:6px 0 18px}
.quiz-note .body-copy + .body-copy{margin-top:16px}
