/* ===== Lab1 — homepage ===== */

/* ===== Futura PT (self-hosted, Paratype Futura Cyrillic — full Latin) ===== */
@font-face {
  font-family: "Futura PT";
  font-weight: 300;
  font-style: normal;
  font-display: swap;
  src: url("assets/fonts/FuturaCyrillicLight.ttf") format("truetype");
}
@font-face {
  font-family: "Futura PT";
  font-weight: 400;
  font-style: normal;
  font-display: swap;
  src: url("assets/fonts/FuturaCyrillicBook.ttf") format("truetype");
}
@font-face {
  font-family: "Futura PT";
  font-weight: 500;
  font-style: normal;
  font-display: swap;
  src: url("assets/fonts/FuturaCyrillicMedium.ttf") format("truetype");
}
@font-face {
  font-family: "Futura PT";
  font-weight: 600;
  font-style: normal;
  font-display: swap;
  src: url("assets/fonts/FuturaCyrillicDemi.ttf") format("truetype");
}
@font-face {
  font-family: "Futura PT";
  font-weight: 700;
  font-style: normal;
  font-display: swap;
  src: url("assets/fonts/FuturaCyrillicBold.ttf") format("truetype");
}
@font-face {
  font-family: "Futura PT";
  font-weight: 800;
  font-style: normal;
  font-display: swap;
  src: url("assets/fonts/FuturaCyrillicExtraBold.ttf") format("truetype");
}
@font-face {
  font-family: "Futura PT";
  font-weight: 900;
  font-style: normal;
  font-display: swap;
  src: url("assets/fonts/FuturaCyrillicHeavy.ttf") format("truetype");
}

/* ===== Design tokens =====
   The single source of truth for the site. Documented and rendered live at
   /design-system.html (hidden page, not linked from the nav). */
:root {
  /* ── Color / ink ── */
  --bg: #ffffff;
  --fg: #000000;
  --muted: #8a8a8a;          /* secondary labels — .project-spread-note only;
                                .coming-soon still uses it but is off the live site */
  --ink-inverse: #ffffff;    /* header ink over dark imagery */
  --ink-soft: #1f1f1f;       /* scroll cue over light imagery */

  /* ── Surfaces ── */
  --surface-thumb: #f2f2f2;  /* gallery thumb placeholder */
  --surface-figure: #f7f4ee; /* warm sand behind project images / media hero */
  --rule-soft: #eee;         /* hairline rule — no consumer since the mobile
                                nav went full-screen and lost its dividers */

  /* ── Layout ── */
  --max: 1600px;     /* centered cap: .case + .site-footer (NOT home <main>) */
  --gutter: 4.5vw;   /* header / gallery / footer edge inset */
  --indent: 14.5%;   /* left indent of the intro block (grid col ~5/24) */
  --shell-inset: 56px;    /* logo + nav distance from the viewport edge */
  --header-pad-top: 86px; /* header top padding (nav baseline) */
  --brand-top: 84px;      /* logo y-position, pinned from Figma */
  --gallery-inset: 54px;  /* gallery side padding (desktop only) */
  --gallery-gap-x: 32px;
  --gallery-gap-y: 5rem;
  --space-hero: 180px;    /* air above every hero, and below the intro text */

  /* ── Type ── */
  --font: "Futura PT", "Futura", "Century Gothic", "Avenir Next", "Avenir",
    system-ui, sans-serif;
  --text-h1: 53px;         /* live futura-pt hero spec */
  --leading-h1: 69px;      /* ~1.3 — a touch airier, matches lab1.design */
  --tracking-h1: -0.01em;  /* -1% — looser, matches Squarespace */

  /* ── Motion ── */
  --ease-out: cubic-bezier(0.2, 0.7, 0.2, 1);
  --dur-page: 0.45s;
  --dur-preview: 0.6s;    /* an image sharpening out of its blurred preview */
  --dur-slow: 0.5s;
  --dur-ink: 0.35s;
  --dur-fast: 0.25s;
  --dur-snap: 0.2s;
}

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

html {
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  background: var(--bg);
  color: var(--fg);
  font-family: var(--font);
  font-weight: 500;
  line-height: 1.4;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

/* ===== Soft, snappy page load — content fades up on every navigation ===== */
@keyframes page-enter {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: translateY(0); }
}

body {
  /* backwards (not both): applies the start state before the run, but drops the
     lingering transform afterwards. A persistent transform on <body> would make
     it the containing block for position:fixed, breaking the fixed header. */
  animation: page-enter var(--dur-page) var(--ease-out) backwards;
}

/* Smooth cross-page morph in supporting browsers (progressive enhancement) */
@view-transition {
  navigation: auto;
}

@media (prefers-reduced-motion: reduce) {
  body { animation: none; }
}

a {
  color: inherit;
  text-decoration: none;
}

img {
  display: block;
  max-width: 100%;
}

/* ===== Soft image loading =============================================
   An <img> paints nothing until its file has arrived, so every photo on the
   site used to open as a hole in the layout and then land in it hard. Each
   opaque raster now ships a ~20px copy of itself, inlined as a data URI in
   assets/css/image-previews.css and generated by tools/image-previews.py:

     · --preview       the tiny image, stretched to fill the frame. Whatever
                       the browser's own smoothing makes of 20px scaled up 50×
                       is exactly what is wanted here — the colours and the
                       composition, none of the detail.
     · --preview-tint  its average colour. It paints under the preview, so it
                       covers even the beat before the data URI has decoded,
                       and it is what the adaptive ink in site.js reads off the
                       element while the real pixels are still unreadable —
                       which is why the logo over a dark hero no longer starts
                       black and flips.

   That much needs no JS at all: the frame is filled on the first paint, and
   because every <img> also carries its width/height (or sits in a box with an
   aspect-ratio), nothing moves when the file lands. What JS adds on top is the
   arrival — site.js flags the image the moment its file is decoded, and
   .is-arriving sharpens the real bitmap out of the same blur the preview was
   already wearing, so the swap itself has nothing to see.

   The flag is data-preview on the <img>, written by the generator, not by
   hand; its header says which images it leaves alone and why.
   ==================================================================== */
img[data-preview] {
  background-color: var(--preview-tint, transparent);
  background-image: var(--preview, none);
  background-repeat: no-repeat;

  /* These two mirror the object-fit / object-position the components give
     their own images, so the preview is cropped the way the photo will be.
     cover from the centre is the common case; the few components that crop
     differently restate the pair next to their own object-* lines. */
  background-size: cover;
  background-position: 50% 50%;
}

/* A blur samples the transparency outside the element as well, which both
   bleeds a halo past the frame and washes the edge pixels out towards whatever
   sits behind. So two things ride along with it: clip-path cuts the halo off
   (it is applied after the filter, so it clips the blurred result, not the
   sharp one), and a spread-only shadow in the image's own average colour —
   blurred with everything else, and shrinking as the blur does — is what the
   edges wash out to instead of the white page.

   Both live in the keyframes rather than in the rule below on purpose: an
   image that is display:none at its breakpoint never runs the animation and
   so never fires animationend, and a leftover ring or clip would then be
   permanent. Inside the keyframes there is nothing to leave behind. */
@keyframes preview-sharpen {
  from {
    filter: blur(var(--preview-blur));
    box-shadow: 0 0 0 calc(2 * var(--preview-blur)) var(--preview-tint, transparent);
    clip-path: inset(0);
  }
  to {
    filter: blur(0);
    box-shadow: 0 0 0 0 var(--preview-tint, transparent);
    clip-path: inset(0);
  }
}

img[data-preview].is-arriving {
  --preview-blur: 14px;
  animation: preview-sharpen var(--dur-preview) var(--ease-out);
}

@media (prefers-reduced-motion: reduce) {
  /* site.js does not flag anything under reduced motion; this is the backstop
     for a preference that changes mid-animation. */
  img[data-preview].is-arriving { animation: none; }
}

.wrap {
  max-width: var(--max);
  margin: 0 auto;
}

/* ===== Header ===== */
.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background: transparent;   /* overlays content — no white strip */
}

.header-inner {
  position: relative;
  display: flex;
  align-items: flex-start;
  justify-content: flex-end;
  gap: 2rem;
  min-height: 128px;
  /* full-width: logo/menu near the screen edges */
  padding: var(--header-pad-top) var(--shell-inset) 0;
}

/* Logo — pinned to exact Figma coordinates (desktop) */
.brand {
  position: absolute;
  top: var(--brand-top);
  left: var(--shell-inset);
}

.brand img {
  width: 83.39px;
  height: 30.46px;
  filter: invert(0);            /* black by default; flips to white over dark content */
  transition: filter var(--dur-ink) ease;
}

.brand[data-theme="dark"] img {
  filter: invert(1);
}

.nav-list {
  display: flex;
  align-items: center;
  gap: 1.9rem;
  margin: 0;
  padding: 0;
  list-style: none;
}

.nav-list a {
  font-size: 1.0625rem;
  font-weight: 400;
  letter-spacing: 0.005em;
  padding-bottom: 3px;
  border-bottom: 1.5px solid transparent;
  transition: color var(--dur-ink) ease, border-color var(--dur-snap) ease,
    opacity var(--dur-snap) ease;
}

/* Whole-row override — used by the design-system preview and by any markup
   that themes the nav by hand. The per-link rules below come later, so the
   script's per-item answer wins wherever it has one. */
.nav[data-theme="dark"] .nav-list a {
  color: var(--ink-inverse);
}

/* Per-link ink: each link reads the luminance directly behind itself, so a
   nav row half over a dark image and half over the white page stays legible
   end to end instead of picking one wrong colour for all six. */
.nav-list a[data-theme="light"] {
  color: var(--fg);
}

.nav-list a[data-theme="dark"] {
  color: var(--ink-inverse);
}

.nav-list a:hover {
  opacity: 0.5;
}

.nav-list a.is-active {
  border-bottom-color: currentColor;
}

/* Hamburger — two-bar */
.nav-toggle {
  display: none;
  flex-direction: column;
  align-items: flex-end;
  justify-content: center;
  gap: 8px;
  width: 40px;
  height: 40px;
  padding: 0;
  background: none;
  border: 0;
  /* iOS Safari paints an unstyled <button> in system blue — the bars are
     currentColor, so the ink has to be stated, not inherited. */
  color: var(--fg);
  -webkit-appearance: none;
  appearance: none;
  -webkit-tap-highlight-color: transparent;
  cursor: pointer;
  transition: filter var(--dur-ink) ease, color var(--dur-ink) ease;
}

.nav-toggle span {
  display: block;
  width: 34px;
  height: 2px;
  background: currentColor;
  transition: transform var(--dur-fast) ease, opacity var(--dur-fast) ease,
    background-color var(--dur-ink) ease;
}

.nav-toggle[data-theme="light"] {
  color: var(--fg);
}

.nav-toggle[data-theme="dark"] {
  color: var(--ink-inverse);
}

.nav-toggle span:nth-child(3) {
  display: none;   /* two bars only */
}

/* ===== Main ===== */
main {
  /* full-width like the original — hero spans wide, gallery reaches the edges */
}

/* ===== Intro (indented editorial block) ===== */
.intro {
  margin: var(--space-hero) 0 6vw;
  padding-left: var(--indent);
  padding-right: 12%;        /* more breathing room on the right of the hero text */
}

.intro-eyebrow {
  margin: 0 0 2rem;
  font-size: 1.3rem;
  font-weight: 700;
  line-height: 1.2;
  letter-spacing: 0.005em;
}

.intro-heading {
  margin: 0;
  font-size: var(--text-h1);
  font-weight: 700;         /* futura-pt Bold (600 Demi read too thin) */
  line-height: var(--leading-h1);
  letter-spacing: var(--tracking-h1);
}

/* ===== Gallery ===== */
.gallery {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: var(--gallery-gap-y) var(--gallery-gap-x);  /* row 80px, column 32px */
  padding: 0 var(--gallery-inset);
  margin-top: 2vw;
  /* The gallery used to be closed by `p.coming-soon`, whose 4rem top margin was
     doing the page's closing air. That line came off the home page on
     2026-08-27; the air was not meant to go with it, so the 4rem moved here.
     Without it the last row's caption sits 82px off the footer and the page
     reads as though it were cut short. */
  margin-bottom: 4rem;
}

.project {
  display: block;
}

/* Parked cards: `hidden` on a .project takes it off the home gallery
   without deleting the markup (In-Cab Driver App, ZOTTAQ — parked
   2026-10-29, coming back later). Remove the attribute to bring one back. */
.project[hidden] {
  display: none;
}

.project-thumb {
  overflow: hidden;
  background: var(--surface-thumb);
  aspect-ratio: 1 / 1;
}

.project-thumb img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  transition: transform var(--dur-slow) var(--ease-out);
}

.project:hover .project-thumb img {
  transform: scale(1.04);
}

.project-title {
  display: inline-block;
  margin-top: 1.1rem;
  font-size: 1.2rem;
  font-weight: 500;
  letter-spacing: 0.005em;
}

.coming-soon {
  margin: 4rem 0 2rem;
  padding: 0 var(--gutter);
  color: var(--muted);
  font-size: 1.1rem;
}

/* ===== Footer =====
   Ported from the live site's footer section, which is a Squarespace fluid-
   engine grid — its air is a fraction of the *content* width (viewport minus
   the 3vw page gutter a side, capped at the 1800px max page width), not a
   fixed px value, so it opens up on a wide screen. --footer-measure is that
   content width; the top ratio reproduces the original's 73.8px above the line
   at 1440. Same reverse-engineering as the contact page. The original's 187px
   below left the line floating mid-page, so the bottom is tightened to a small
   fluid band (clamped 24-48px) that reads as a closing footer rule. */
.site-footer {
  --footer-measure: min(94vw, 1800px);
  max-width: var(--max);
  margin: 0 auto;
  padding: calc(0.0545 * var(--footer-measure))
           var(--gutter)
           clamp(24px, 0.032 * var(--footer-measure), 48px);
}

/* Lab1 Nexus and Balticca were moved out of the primary nav on 2026-08-27 (it
   went from seven items to five) and live here instead. One centred line, the
   user's own composition: credit, a pipe, then the two destinations split by a
   bullet. Both glyphs were checked against the .ttf cmaps before being used —
   U+007C and U+2022 are in Futura PT, U+00B7 (middle dot) is NOT, so don't
   "tidy" the bullet into a middot. The separators are FULL-STRENGTH ink, not a
   muted grey — they were shipped at opacity 0.32 for half an hour and the user
   asked for black. They set no colour of their own, so they inherit
   .site-footer p and follow the --warm-pink override for free.

   Flex, not inline text: whitespace-only nodes between flex items are dropped,
   so the source can stay indented and every gap is the one declared here. The
   links need no colour of their own — `a { color: inherit }` picks up
   .site-footer p, which is what carries the --warm-pink override. The gap is in
   em so it tracks the type; at the flat 1.0625rem that is ~13.6px, the same
   gap-to-type ratio the old fluid pair happened to hit at 1600. */
.footer-line {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: baseline;
  gap: 0 0.8em;
}

.footer-line a {
  transition: opacity var(--dur-snap) ease;
}

.footer-line a:hover {
  opacity: 0.5;   /* the same fade the header nav uses */
}

/* Measured: the line holds from 375px up. The threshold moved when the type
   went flat — at the old fluid size it fitted from 360, and leaving the query
   at 359 let 360–374 break as "… Lab1 Nexus •" / "Balticca", stranding the
   bullet at a line end. Below 375 the credit takes its own row and the pipe —
   exactly the separator that row break replaces — goes. The bullet stays,
   because "Lab1 Nexus • Balticca" is still one line at 320. Re-measure this
   number if the footer type or the gap ever changes. */
@media (max-width: 374px) {
  .footer-line > span:first-child {
    flex: 0 0 100%;
    margin-bottom: 0.35em;
  }

  .footer-sep--pipe {
    display: none;
  }
}

/* Centred, like the original — the footer text block on lab1.design carries
   text-align: center site-wide, not just on the case studies.

   Type is a FLAT 1.0625rem, deliberately not the site's fluid body paragraph
   (0.36% of page width + 1rem) it used to carry. The user asked for the footer
   at "taki jak na mobile" — and the value that lands on is the same 1.0625rem
   `.nav-list a` already uses, which is the right answer for a different reason:
   this line is navigation plus a credit, so it should read at the size the
   header nav reads at, not at body size. Flat, like the nav, so it stays quiet
   on a 1920 screen instead of growing to 22px. */
.site-footer p {
  margin: 0;
  font-size: 1.0625rem;
  line-height: 1.7;
  font-weight: 400;
  color: var(--fg);
  text-align: center;
}

/* ===== Project (case-study template) =====
   .case keeps the 1600px centered cap (homepage <main> is now full-width).
   Each child manages its own gutter, mirroring the homepage. */
.case {
  max-width: var(--max);
  margin: 0 auto;
}

.project-hero {
  margin: var(--space-hero) 0 0;
  padding: 0 var(--gutter);   /* symmetric padding, centered text */
  text-align: center;
}

/* The Join us hero (.page-eyebrow / .page-lead) reads at the same scale —
   grouped here so the two page types share one type spec instead of two
   copies that drift apart. */
/* Client mark above the eyebrow — clamp(25px, 2.3vw, 44px), the height of the former HOME logo row */
.project-logo {
  display: block;
  width: auto;
  height: clamp(25px, 2.3vw, 44px);
  margin: 0 auto 2rem;
}

.project-eyebrow,
.page-eyebrow {
  margin: 0 0 2rem;
  font-size: 1.3rem;
  font-weight: 600;
  line-height: 1.2;
  letter-spacing: 0.005em;
}

/* Case-study eyebrows in Futura Book, the body weight — lighter over the lead. */
.project-eyebrow {
  font-weight: 400;
}

.project-lead,
.page-lead {
  margin: 0 auto;
  max-width: 1000px;
  font-size: var(--text-h1);
  font-weight: 700;
  line-height: var(--leading-h1);
  letter-spacing: var(--tracking-h1);
}

.project-intro-body {
  margin: 2rem auto 0;
  max-width: 1000px;
  font-size: 1.35rem;
  font-weight: 400;
  line-height: 1.6;
  letter-spacing: 0.002em;
}

/* Template #2 hero: image covers the full viewport (width + height).
   Square/landscape sources are cropped top+bottom only (object-position
   center + cover), never on the sides. Sits under the overlaid header —
   the adaptive-ink script flips the logo/menu to white/black over it. */
.project-hero-full {
  position: relative;          /* anchor for the scroll cue */
  width: 100vw;
  height: 100vh;
  height: 100dvh;
  margin: 0;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  overflow: hidden;
  background: var(--surface-figure);
}

/* Scroll cue — the animated "mouse" arrow, ported 1:1 from balticca.com
   (own asset). Sits bottom-centre of the full-screen hero and bobs. */
.scroll-cue {
  position: absolute;
  left: 50%;
  bottom: 48px;
  transform: translateX(-50%);
  z-index: 2;
  display: grid;
  place-items: center;
  color: var(--ink-soft);      /* fallback; adaptive-ink script overrides per background */
  transition: color var(--dur-ink) ease;
  animation: bob 2.4s ease-in-out infinite;
}

/* Adaptive: white over dark pixels, dark over light (same logic as the header ink). */
.scroll-cue[data-theme="dark"]  { color: var(--ink-inverse); }
.scroll-cue[data-theme="light"] { color: var(--ink-soft); }

.scroll-icon {
  width: 17px;
  height: auto;
  display: block;
}

@keyframes bob {
  0%, 100% { transform: translate(-50%, 0); }
  50%      { transform: translate(-50%, 6px); }
}

@media (prefers-reduced-motion: reduce) {
  .scroll-cue { animation: none; }
}

.project-hero-full picture {
  display: block;
  width: 100%;
  height: 100%;
}

.project-hero-full img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center top;   /* anchor to the top — crop from the bottom */
  background-position: center top;   /* …and the preview under it, likewise */
}

/* Modifier for a full-bleed source whose subject sits in the middle of the
   frame rather than in its top half (e.g. Circle K / EV — a phone centred in
   a 1.96 photograph). Top-anchoring crops such a frame from the bottom only,
   which eats the subject: at a 3:2 laptop ratio `cover` drops the lowest 18%
   of the image, and the phone's base is at 92%. Centring splits that crop
   between the top and the bottom, where this composition has margin to give.
   Still `cover` — the image fills the viewport at every ratio, and the
   ≤820px `--mobile-*` overrides below can re-anchor it per breakpoint. */
.project-hero-full--center img {
  object-position: center center;
  background-position: center center;
}

/* Modifier for "framed" sources — a UI screenshot floating on a flat colour
   field with even margins (e.g. Cognite), rather than an edge-to-edge photo.
   The section takes the field colour, passed per project as --hero-field, so
   the image letterboxes into it with no visible seam. Crop is centred (the
   0.5/0.5 focal point these carry on the original site) instead of
   top-anchored, and on narrow screens it switches to contain: cover on a
   landscape source in a portrait viewport slices the interface in half. */
.project-hero-full--framed {
  background: var(--hero-field, var(--surface-figure));
}

.project-hero-full--framed img {
  object-position: center center;
  background-position: center center;
}

@media (max-width: 820px) {
  .project-hero-full--framed img {
    object-fit: contain;

    /* The preview letterboxes with it, and gives up its tint so the field
       around it stays --hero-field. That field is the colour of the image's
       own margins, so preview and field still meet with no seam. */
    background-size: contain;
    background-color: transparent;
  }

  /* A landscape source letterboxed into a portrait viewport leaves a lot of
     empty field, so this hero stops short of full height — enough for the
     image, the overlaid header and the scroll cue, and no more. */
  .project-hero-full--framed {
    height: 72vh;
    height: 72dvh;
  }

  /* …unless the page ships a purpose-cropped vertical source (a <source>
     inside the hero's <picture>). Then the hero goes back to filling the
     screen: a portrait image in a portrait viewport needs no shortening,
     and framed sources still `contain` so the crop stays intact — the
     leftover field is the image's own flat colour, so there is no seam. */
  .project-hero-full--mobile-portrait {
    height: 100vh;
    height: 100dvh;
  }

  /* Crops whose subject runs off the bottom of the frame: centring them parks
     a band of field under something that is meant to bleed, so the image is
     pinned to the bottom edge and all the leftover field goes to the top,
     where the overlaid header sits anyway. Same specificity as the --framed
     `center center` rule above, and later in the file, so it wins. */
  .project-hero-full--mobile-bottom img {
    object-position: center bottom;
    background-position: center bottom;
  }

  /* Same idea on the horizontal axis: at viewport ratios where `contain`
     leaves slack left and right (short phones, small tablets) centring parks
     a field bar down each side. Pinning left puts the crop against the screen
     edge and collects all the slack on the right. Same specificity as the
     --framed `center center` rule, and later in the file, so it wins; the
     combined selector below outranks both when a page asks for left + bottom. */
  .project-hero-full--mobile-left img {
    object-position: left center;
    background-position: left center;
  }

  .project-hero-full--mobile-left.project-hero-full--mobile-bottom img {
    object-position: left bottom;
    background-position: left bottom;
  }

  /* Mirror of the above for crops whose subject bleeds off the right-hand
     side: pinning right keeps that bleed flush with the screen edge and
     collects all the leftover field on the left. */
  .project-hero-full--mobile-right img {
    object-position: right center;
    background-position: right center;
  }

  .project-hero-full--mobile-right.project-hero-full--mobile-bottom img {
    object-position: right bottom;
    background-position: right bottom;
  }

  /* A framed hero whose *mobile* source is an edge-to-edge photograph rather
     than a screenshot floating on a field (cognite-docs). The `contain` above
     exists so a framed crop is never sliced; a photo has no frame to protect,
     so it goes back to `cover` and fills the screen — which also means no
     letterbox, so --hero-field never shows on mobile and the preview gets its
     tint back (the --framed rule gave it up for the field). */
  .project-hero-full--mobile-cover img {
    object-fit: cover;
    background-size: cover;
    background-color: var(--preview-tint, transparent);
  }
}

/* Template #2 pages: header overlays the full-bleed hero.
   fixed (not sticky) keeps it out of flow — so no white strip above the
   image and the hero starts at the very top — while staying pinned to the
   top on scroll, matching the sticky header on the rest of the site. */
.page--media-hero .site-header {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
}

/* Media hero on phones only (field-operations-crm). Above 820px the page
   opens like template #1 (autocharge) — straight onto the text hero, with the
   ordinary sticky header — so the image hero is dropped and the header is
   fixed only where the hero is actually there to sit under. */
@media (max-width: 820px) {
  .page--media-hero-mobile .site-header {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
  }
}

@media (min-width: 821px) {
  .project-hero-full--mobile-only {
    display: none;
  }
}

/* Template #2 variant: FILM hero (circle-k-ev.html).
   The same full-bleed 100dvh box, but the source is a looping <video>.

   Two things differ from the image hero, both deliberate:

   · The field is #000, not --surface-figure. The film resolves to pure black
     at every one of its own edges, so a black box means there is no seam to
     find — letterboxed or cropped, at any viewport ratio.

   · object-fit is `cover` on landscape viewports and `contain` on portrait ones,
     and the split is not cosmetic.

     Landscape needs cover: the device deliberately runs off the bottom of the
     16:9 film, so the film's bottom edge has to BE the viewport's bottom edge —
     letterbox it and the device stops dead in mid-screen with black underneath.
     Cover is safe there because the device is only ~37% of the frame width, so
     the horizontal crop below 16:9 cannot reach it, and above 16:9 the vertical
     crop only eats into the (generous) gap above it.

     Portrait needs contain, for a reason that cost two rounds to find: cover
     scales a 1:2 film to fill a WIDER portrait box by width, which crops its
     height — a 0.75 tablet loses 25%, which swallowed the entire gap above the
     device and pinned it under the logo. Cover cannot be made safe across the
     whole portrait range; nor can the landscape film be borrowed for it, since
     below aspect 0.6 the horizontal crop starts clipping the device itself.
     Contain sidesteps it: the composition arrives exactly as authored at every
     ratio, the gap above the device is always at least the 17% it was rendered
     with, and the letterbox bars are invisible because the film resolves to
     #000 at every edge and sits on a #000 field.

     The 1/1 breakpoint below is the same condition assets/js/hero-film.js uses
     to choose the source file — keep the two in step. */
.project-hero-full--film {
  background: #000;
}

.hero-film-video,
.hero-film-still,
.project-hero-full--film img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center center;
}

/* The resting still. Sits exactly on top of the film and fades in when playback
   ends, which is when the device has stopped moving. Both layers are the same
   source aspect under the same `cover`, so they scale and crop identically and
   the crossover has nothing to misalign. The fade only exists to hide sub-pixel
   differences between an H.264 frame and its lossless original — it is not a
   transition, and should not be lengthened into one. */
.hero-film-still {
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 260ms linear;
  pointer-events: none;
}

.hero-film-still.is-shown {
  opacity: 1;
}

@media (max-aspect-ratio: 1 / 1) {
  .hero-film-video,
  .hero-film-still,
  .project-hero-full--film img {
    object-fit: contain;
  }
}

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



.project-figure {
  /* full-bleed: span the full viewport width, breaking out of the 1600 .case */
  width: 100vw;
  margin: 4.5vw 0 0;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  background: var(--surface-figure);
}

/* First figure right after the intro text: mirror the hero's top margin so
   the space below the intro matches the space above the eyebrow line. */
.project-hero + .project-figure {
  margin-top: var(--space-hero);
}

.project-figure picture {
  display: block;
}

/* An extra frame that exists only in the narrow layout — a vertical crop with
   no desktop counterpart. Desktop-first, so it is hidden here and revealed in
   the ≤820px block; `loading="lazy"` keeps desktop from fetching it at all. */
.project-figure--mobile {
  display: none;
}

.project-figure img {
  display: block;
  width: 100%;
  height: auto;
}

/* ---------------------------------------------------------------------------
   Swipe carousel (intel.html, 2026-10-23) — the Instagram "one photo cut into
   slices" trick. Lives inside a .project-figure--mobile, so it only exists at
   ≤820px; its desktop counterpart is a plain .project-figure--desktop.
   Native scroll-snap does the swiping; site.js only keeps the dots in step and
   makes them clickable. The slices are contiguous crops of one frame, so a
   half-way swipe reads as a single panorama sliding past.
   ------------------------------------------------------------------------- */
.project-carousel {
  background: none;
}

.project-carousel-track {
  display: flex;
  margin: 0;
  padding: 0;
  list-style: none;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  background: var(--surface-figure);
}

.project-carousel-track::-webkit-scrollbar {
  display: none;
}

.project-carousel-track > li {
  flex: 0 0 100%;
  scroll-snap-align: start;
  scroll-snap-stop: always;
}

.project-carousel-dots {
  display: flex;
  justify-content: center;
  gap: 6px;
  padding-top: 14px;
}

.project-carousel-dots button {
  width: 6px;
  height: 6px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--fg);
  opacity: 0.2;
  cursor: pointer;
  transition: opacity var(--dur-fast) var(--ease-out);
  position: relative;
}

/* A bigger tap target than the 6px dot, without moving the dots apart. */
.project-carousel-dots button::after {
  content: "";
  position: absolute;
  inset: -10px -4px;
}

.project-carousel-dots button[aria-current="true"] {
  opacity: 1;
}

/* ---------------------------------------------------------------------------
   Project photo gallery (template #2, added 2026-08-16 for Shoptalk/Rezolve).

   A "justified" gallery: rows of photos that always fill the width exactly and
   whose images all share one height, with no cropping. The trick is one line —
   each figure's flex-grow is its own aspect ratio (passed as --ar on the
   element), with flex-basis 0. Widths then come out proportional to the
   ratios, which is precisely the condition for equal heights. So a landscape
   3:2 next to a portrait 2:3 lands at the same height automatically, and the
   markup never has to know how wide the viewport is.

   Set --ar per figure = natural width / height (3:2 → 1.5, 2:3 → 0.667,
   16:9 → 1.778). Getting it slightly wrong only nudges the row; it can't
   break the layout.

   Full-bleed like .project-figure, but inset by the same 54px the home
   gallery uses, so many photos read as a grid rather than one wall of image.
   --------------------------------------------------------------------------- */
.project-gallery {
  width: 100vw;
  margin: 4.5vw 0 0;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  padding: 0 var(--gallery-inset);
}

.project-hero + .project-gallery {
  margin-top: var(--space-hero);
}

.pg-row {
  display: flex;
  gap: var(--gallery-gap-x);
}

.pg-row + .pg-row {
  margin-top: var(--gallery-gap-x);
}

.pg-row figure {
  flex: var(--ar, 1.5) 1 0;   /* grow ∝ aspect ratio ⇒ every row is one height */
  min-width: 0;
  margin: 0;
  background: var(--surface-figure);
}

.pg-row picture {
  display: block;
}

.pg-row img {
  display: block;
  width: 100%;
  height: auto;
}

@media (max-width: 820px) {
  /* One photo per row: at phone width a 3-up row shrinks each frame to a
     stamp, and the equal-height trick has nothing left to balance. Side inset
     drops to the shared --gutter so the gallery lines up with the header,
     intro and body copy (same move the home gallery makes). */
  .project-gallery {
    margin-top: 12vw;
    padding: 0 var(--gutter);
  }

  .project-hero + .project-gallery {
    margin-top: 12vw;
  }

  .pg-row {
    display: block;
  }

  .pg-row figure + figure,
  .pg-row + .pg-row {
    margin-top: var(--gallery-gap-x);
  }
}

/* ---------------------------------------------------------------------------
   Full-bleed brand band: one piece of artwork with a short copy column laid
   over it (added 2026-08-25 for cognite.html's design-system note).

   The artwork it was built for is composed with every shape inside the left
   ~46% and nothing but a flat horizontal gradient after that, so the copy
   parks in the empty right half and never sits on top of anything. That is a
   property of the image, not of this component — anything else dropped in
   here needs its own right-hand air.

   The <img> stays in flow and sets the band's height, so the band keeps the
   artwork's own ratio at every width and the copy is absolutely positioned
   over it. No fixed height, no object-fit, nothing to re-tune per image.
   --band-field is the colour of the artwork's right edge: it paints the band
   before the image decodes and covers any sub-pixel seam at the edges.

   Ink is white and page-scoped-by-default rather than a token, the same way
   --hero-field is: this band brings its own dark field with it, so it can sit
   on a light page (cognite.html) or a dark one without either page having to
   know about it.
   --------------------------------------------------------------------------- */
.project-band {
  position: relative;
  width: 100vw;
  margin: 4.5vw 0 0;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  background: var(--band-field, var(--surface-figure));
}

/* Named path only: this is the band's own artwork. The credit line further
   down carries an <img> too, and a descendant selector here would hand it
   width:100% at higher specificity than its own sizing rule. */
.project-band > picture {
  display: block;
}

.project-band > img,
.project-band > picture > img {
  display: block;
  width: 100%;
  height: auto;
}

/* Right-aligned, vertically centred, and deliberately narrow — the brief was
   that the copy should not fill this space, only sit in it. The width is
   capped in px as well as in %, so on a wide monitor the measure stays a
   short column instead of growing into a second body block. */
.project-band-copy {
  position: absolute;
  top: 0;
  bottom: 0;
  right: 8vw;
  width: min(32%, 440px);
  display: flex;
  flex-direction: column;
  justify-content: center;
  text-align: right;
  color: var(--ink-inverse);
}

.project-band-title {
  margin: 0 0 1rem;
  font-size: clamp(1.9rem, 2.8vw, 3rem);
  font-weight: 700;
  line-height: 1.12;
  letter-spacing: -0.015em;
}

/* The copy is a pull quote in the designer's own voice, so it is a real
   <figure>/<blockquote>/<figcaption> — both carry default UA margins that have
   to go, since the spacing here is set by the credit's own margin-top. */
.project-band-quote,
.project-band-quote blockquote {
  margin: 0;
}

.project-band-text {
  margin: 0;
  font-size: clamp(1.05rem, 1.1vw, 1.25rem);
  font-weight: 400;
  line-height: 1.6;
  letter-spacing: 0.002em;
  opacity: 0.82;
}

/* Credit line: avatar + name, held as one group flush to the column's right
   edge, so the name's right edge lands on the same rag as the quote above it
   (avatar first in the DOM, to the left of the name — putting it on the far
   right would push the name off that edge).

   The avatar sizes in vw between a 34px floor and a 52px cap: small enough to
   stay a credit rather than a portrait, which is what "niewielkie" asked for.
   --band-avatar-field is the photo's own flat background, so the circle is the
   right colour for the split second before the image decodes. */
.project-band-cite {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  gap: 0.7rem;
  margin-top: 1.75rem;
  font-size: clamp(0.9rem, 0.88vw, 1rem);
  font-weight: 500;
  letter-spacing: 0.01em;
  opacity: 0.92;
}

.project-band-avatar {
  flex: none;
  width: clamp(34px, 2.9vw, 52px);
  height: clamp(34px, 2.9vw, 52px);
  border-radius: 50%;
  object-fit: cover;
  background: var(--band-avatar-field, var(--surface-thumb));
}

/* Futura PT ships no italic cut in this build — every @font-face at the top of
   this file is font-style: normal — so this is a browser-synthesised oblique,
   not a drawn one. At this size, on one word, the shear reads as intended
   emphasis; do not push it onto anything longer or larger, where a geometric
   sans makes faux-italic obvious. Light weight keeps the role subordinate to
   the name without a second opacity. */
.project-band-cite em {
  font-weight: 300;
  font-style: italic;
}

/* Quote-only band (added 2026-10-11 for autocharge.html's client reference):
   the same dark field and credit, no artwork. With no image to set the height,
   the copy goes back into flow and the section pads itself. The quote is five
   paragraphs, so it runs in two columns on the --gallery-inset edges; the
   left-aligned rag reads better than the overlay's right rag at this length.
   The credit takes a second line for the role, set upright and light rather
   than in the cognite credit's <em> — faux italic on a whole job title is
   exactly the case that note warns against. */
.project-band--quote {
  padding: 7vw var(--gallery-inset);
  color: var(--ink-inverse);
  /* A faint lift on top of --band-field (which stays the flat fallback):
     a soft light pool behind the title, top left, falling off to a slightly
     deeper corner bottom right. White/black at low alpha, so it works on any
     --band-field a page passes in. */
  --band-glow:
    radial-gradient(ellipse 70% 90% at 15% 0%, rgba(255, 255, 255, 0.07), transparent 70%),
    linear-gradient(160deg, transparent 40%, rgba(0, 0, 0, 0.35));
  background-image: var(--band-glow);
}

.project-band--quote .project-band-copy {
  position: static;
  width: auto;
  max-width: 1320px;
  margin: 0 auto;
  text-align: left;
}

.project-band--quote .project-band-title {
  margin-bottom: 2.5rem;
}

.project-band--quote blockquote {
  column-count: 2;
  column-gap: clamp(2.5rem, 5vw, 6rem);
}

.project-band--quote .project-band-text {
  break-inside: avoid;
}

.project-band--quote .project-band-text + .project-band-text {
  margin-top: 1.1em;
}

.project-band--quote .project-band-cite {
  justify-content: flex-start;
  gap: 0.9rem;
  margin-top: 3rem;
}

.project-band-who {
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

.project-band-role {
  font-weight: 300;
  opacity: 0.75;
}

/* Case-study body block. Left-aligned (user, 2026-08-13 — centred body copy
   read poorly at this measure), and the whole block — not just the body —
   carries the 1000px measure, so the h2 and the paragraphs share one left
   edge. The block itself stays centred in the page, so the text column sits
   exactly where the centred version put it. */
.project-block {
  max-width: calc(1000px + 2 * var(--gutter));
  margin: 8vw auto;
  padding: 0 var(--gutter);
  text-align: left;
}

/* The intro paragraph straight under the hero title belongs to it — the
   full 8vw section break read as a gap, so it sits a step closer. */
.project-hero + .project-block {
  margin-top: 5vw;
}

.project-block-title {
  margin: 0 0 2rem;
  font-size: 2.75rem;
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -0.01em;
}

.project-block-body {
  max-width: 1000px;
  margin: 0 auto;
}

.project-block-body p {
  margin: 0 0 1.2em;
  font-size: 1.35rem;
  font-weight: 400;
  line-height: 1.6;
  letter-spacing: 0.002em;
}

.project-block-body p + p {
  margin-top: 2.2em;
}

.project-block-body p:last-child {
  margin-bottom: 0;
}

/* ---------------------------------------------------------------------------
   Brand spread (added 2026-08-25 for cognite.html). Three stacked rows on one
   shared measure:

     1. head — big title + two narrow paragraphs, 2fr 1fr 1fr
     2. pair — two brand squares, 1fr 1fr
     3. foot — client logo left, credit right

   Full-bleed and inset by --gallery-inset, the same 54px the home gallery and
   .project-gallery use, so the three rows and the site's other grids all share
   one left and right edge. Everything collapses to a single column at 820px,
   which puts the foot under the second square.

   The squares are the one place on this site with a corner radius (the avatar
   aside, which is a circle). It is deliberate — it comes from the reference
   layout this row was built from — but it is also the only one, so keep it
   here rather than promoting it to a token.
   --------------------------------------------------------------------------- */
.project-spread {
  width: 100vw;
  margin: 12vw 0 0;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  padding: 0 var(--gallery-inset);
}

.project-spread-head {
  display: grid;
  grid-template-columns: 2fr 1fr 1fr;
  gap: var(--gallery-gap-x);
  align-items: start;
}

.project-spread-title {
  margin: 0;
  font-size: clamp(2rem, 3.2vw, 3.5rem);
  font-weight: 700;
  line-height: 1.08;
  letter-spacing: -0.015em;
}

/* Nudged down so the paragraph's first line sits on the title's cap height
   rather than on its em-box top — the title is 3x the size, so aligning the
   raw boxes leaves the columns looking unaligned. */
.project-spread-text {
  margin: 0.45em 0 0;
  font-size: clamp(1rem, 1vw, 1.125rem);
  font-weight: 400;
  line-height: 1.55;
  letter-spacing: 0.002em;
}

.project-spread-pair {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--gallery-gap-x);
  margin-top: 3vw;
}

/* The two sources are near-square but not identical (0.913 vs 0.908), and at
   650px wide that half-percent shows as a few px of mismatch along the bottom
   edge. Pinning one ratio on the figure and cropping the images into it with
   object-fit: cover makes the pair exactly equal height whatever the sources
   are; the crop is well under 1% a side. Corners stay square: every other
   image on the site is a hard rectangle and so is the source artwork, so the
   14px radius these used to carry read as the odd one out. object-fit already
   confines the image to the box, so nothing here needs overflow: hidden. */
.project-spread-pair figure {
  margin: 0;
  aspect-ratio: 1 / 1.1;
  background: var(--surface-figure);
}

.project-spread-pair img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.project-spread-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gallery-gap-x);
  margin-top: 2vw;
}

/* The source is a black PNG on transparency, so it needs no theming here —
   this section sits on the white page, not on the dark band above it. */
.project-spread-logo {
  display: block;
  width: auto;
  height: clamp(20px, 1.9vw, 30px);
  opacity: 0.72;
}

.project-spread-note {
  margin: 0;
  font-size: clamp(0.85rem, 0.85vw, 0.95rem);
  font-weight: 400;
  letter-spacing: 0.01em;
  text-align: right;
  color: var(--muted);
}

/* Third photograph of the image set, between the pair and the credit foot. It
   carries no inset of its own — it sits inside .project-spread, so it is flush
   with the two squares above it at every width by construction, and the gap it
   opens is --gallery-gap-x, the same gap that runs between the squares, so the
   three read as one set rather than two things and an afterthought. Desktop
   keeps the source's own wide ratio (height: auto); the fixed box only comes
   in on mobile, below. */
.project-spread-shot {
  margin: var(--gallery-gap-x) 0 0;
  background: var(--surface-figure);
}

.project-spread-shot img {
  display: block;
  width: 100%;
  height: auto;
}

/* ---------------------------------------------------------------------------
   Market context (added 2026-08-25 for autocharge.html / Circle K). A narrow text
   column on the left and two photographs on the right that overlap: the front
   one is lifted up and pushed left so it breaks the back plate's left edge.
   The overlap is the whole composition — the two plates never sit side by side.

   The geometry is the original Squarespace fluid-engine grid rebuilt literally
   — 24 columns, 11px gutters, row height = content width x 0.0215 — the same
   three constants contact.html already encodes, so the two ports stay in step.
   Only the type is ours: title on the .project-spread-title scale, body on the
   site's own body spec rather than the original's.

   Full-bleed and inset by --gallery-inset, so it shares its left and right edge
   with the home gallery, .project-gallery and .project-spread.
   --------------------------------------------------------------------------- */
.project-context {
  --context-row: calc((100vw - 2 * var(--gallery-inset)) * 0.0215);

  width: 100vw;
  margin: 12vw 0 0;
  margin-left: calc(50% - 50vw);
  margin-right: calc(50% - 50vw);
  padding: 0 var(--gallery-inset);

  display: grid;
  grid-template-columns: repeat(24, minmax(0, 1fr));
  grid-template-rows: repeat(20, minmax(var(--context-row), auto));
  gap: 11px;
  text-align: left;
}

.project-context-title {
  grid-area: 5 / 1 / 7 / 9;
  align-self: start;
  z-index: 2;
  margin: 0;
  font-size: clamp(1.9rem, 3.2vw, 3.4rem);
  font-weight: 700;
  line-height: 1.08;
  letter-spacing: -0.015em;
}

.project-context-body {
  grid-area: 8 / 1 / 21 / 9;
  z-index: 2;
}

/* Matches .project-block-body at both ends of the range (1.15rem on a phone,
   the desktop body size on a wide screen) but scales in between, because this
   column is a fixed 8/24 of the viewport rather than a capped measure. */
.project-context-body p {
  margin: 0;
  font-size: clamp(1.15rem, 1.15vw, 1.3rem);
  font-weight: 400;
  line-height: 1.75;
  letter-spacing: 0.002em;
}

.project-context-body p + p {
  margin-top: 1.6em;
}

.project-context-photo {
  margin: 0;
  background: var(--surface-figure);
}

.project-context-photo img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* Back plate: the wide source cropped into a tall box hugging the right edge,
   running from the very top of the section down past the front plate. */
.project-context-photo--back {
  grid-area: 1 / 15 / 19 / 25;
  z-index: 0;
}

/* The focal point the live site stores on this asset (0.332 / 0.512) — without
   it the portrait crop centres on the sky and loses the terrace. */
.project-context-photo--back img {
  object-position: 33% 51%;
  background-position: 33% 51%;
}

.project-context-photo--front {
  grid-area: 5 / 12 / 15 / 23;
  z-index: 1;
}

/* Tablet band. The 8/24 text column is ~33% of the viewport at every width,
   which is a comfortable measure at 1600 and a 28-character alley at 1024 —
   the copy then runs a couple of hundred px past the bottom of the photographs
   and the composition stops balancing. Between the mobile breakpoint and 1200
   the column widens to 11/24 and the two plates slide one column right to keep
   their gap; the overlap and the row grid are untouched. */
@media (min-width: 821px) and (max-width: 1200px) {
  .project-context-title,
  .project-context-body {
    grid-column: 1 / 12;
  }

  .project-context-photo--back {
    grid-area: 1 / 16 / 19 / 25;
  }

  .project-context-photo--front {
    grid-area: 5 / 13 / 15 / 24;
  }
}

/* Prev / next pagination closing every case study — ported 1:1 from the live
   site's #itemPagination block. Full-bleed (breaks out of the 1600 .case cap)
   because the original sits on the page gutter, not the content column: 3vw
   padding all round, chevron + title only. The original renders "Previous" /
   "Next" meta labels display:none, so they live on the link's aria-label here
   instead of as hidden markup. */
.project-nav {
  display: flex;
  align-items: center;
  width: 100vw;
  margin: 3vw calc(50% - 50vw) 0;
  padding: 3vw;
}

.project-nav-link {
  display: flex;
  align-items: center;
  max-width: 50%;
  color: var(--fg);
}

.project-nav-link--prev { margin-right: auto; }

/* margin-left: auto keeps "next" flush right even when there is no "prev"
   (the first case study), matching the original's optional-link markup. */
.project-nav-link--next {
  margin-left: auto;
  text-align: right;
}

/* 18×32 box + a 25px gap to the title. The original spends that 25px as
   padding on a content-box div; expressed as margin here so our global
   border-box doesn't eat it. */
.project-nav-icon {
  flex: none;
  width: 18px;
  height: 32px;
  margin-right: 25px;
}

.project-nav-link--next .project-nav-icon {
  margin-right: 0;
  margin-left: 25px;
}

.project-nav-icon svg {
  display: block;
  width: 100%;
  height: 100%;
  fill: transparent;
  stroke: currentColor;
  stroke-width: 1px;   /* user units: 2 device px at this 2× scale */
}

/* Heading 3 on the original: Futura PT 700, fluid 1.2vw + 1rem, capped at
   37.6px (where the site's 1800px max page width lands), line-height 1. */
.project-nav-title {
  margin: 0;
  font-size: min(calc(1.2vw + 1rem), 37.6px);
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0;
}

.project-nav-link:hover .project-nav-title {
  opacity: 0.5;
}

.project-nav-title,
.project-nav-icon {
  transition: opacity var(--dur-snap) ease;
}

.project-nav-link:hover .project-nav-icon {
  opacity: 0.5;
}

/* ===== Colour variant: brown / pink case study =====
   Opted into per page with .page--warm-dark on <body> (currently only
   cognite-alt.html, a colour study of cognite.html). The page ships the same
   markup, the same images and the same geometry as the default template — the
   only difference is that the paper turns dark brown and every piece of ink
   on it turns pink.

   The pair is the client-supplied one: #3E2F29 (C0 M24 Y34 K76) and #DDB1AA
   (C0 M20 Y23 K13); they measure 6.7:1 against each other, so the body copy
   clears AA and the headings clear AAA. Page-scoped, like the contact page's
   #253551 link ink — deliberately NOT :root tokens, so no other page can pick
   them up by accident and no site-wide dark theme is implied.

   Colouring the whole <body> rather than each text section is what keeps this
   to a colour change: the copy sections already sit on transparent
   backgrounds, so they inherit the brown, and the air between them (which is
   margin, not padding) inherits it too. The full-bleed .project-figure images
   then read as pink bands punched out of the brown — no per-section
   backgrounds, no measures moved onto children, nothing that can drift out of
   sync with cognite.html. --surface-figure follows so an image that hasn't
   decoded yet doesn't flash warm sand; the hero keeps its own --hero-field
   (the screenshot's pink), which is set inline on the section.

   The header ink is left alone: the adaptive-ink script reads the brown as
   dark content and flips the logo/nav to white via --ink-inverse. White on
   this brown is legible, and the logo is a black PNG tinted with
   `filter: invert(1)` — it can't be recoloured to the pink without a filter
   chain that the shared script knows nothing about. */
.page--warm-dark {
  --warm-dark: #3e2f29;
  --warm-pink: #ddb1aa;

  /* --bg carries the brown to both consumers it has: this <body> and the
     full-screen mobile menu sheet. Retinting the token rather than the body's
     background is what keeps the sheet from opening white with pink links on
     it (1.7:1 — invisible), since .mobile-nav a has no colour of its own and
     inherits the page ink. --surface-figure is the placeholder that shows
     behind a .project-figure whose image hasn't decoded yet.

     --fg is deliberately NOT retinted: the adaptive-ink script reads it for
     the header over *light* content, which on this page is the pink hero
     screenshot — pink ink on a pink field would disappear. Only the two
     places that name --fg for page copy are overridden below. */
  --bg: var(--warm-dark);
  --surface-figure: var(--warm-dark);

  color: var(--warm-pink);
}

.page--warm-dark .project-nav-link,
.page--warm-dark .site-footer p {
  color: var(--warm-pink);
}

/* While the menu is up the shared rules force the logo and the X to black,
   because the adaptive script would otherwise read the page *under* the
   sheet. That answer assumes a white sheet; here the sheet is brown, so both
   flip back to light. Outranks `body.nav-open …` on class count (0,4,1) vs
   (0,3,2), so it holds wherever it sits in the file. */
.page--warm-dark.nav-open .brand[data-theme] img {
  filter: invert(1);
}

.page--warm-dark.nav-open .nav-toggle[data-theme] {
  color: var(--warm-pink);
}

/* ===== Simple content page (Join us) =====
   Same centred hero as the project template: a small eyebrow over a
   large lead. Type lives on the grouped .project-eyebrow / .project-lead
   rules above — only the inline link ink is specific to this page. */
.page-hero {
  margin: var(--space-hero) 0 0;
  padding: 0 var(--gutter);
  text-align: center;
}

/* Email inside the lead: same ink as the sentence, underlined at the
   weight the 53px type can carry (1px disappears at this size). */
.page-lead a {
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 0.16em;   /* clears the descender of the "g" */
  transition: opacity var(--dur-fast) var(--ease-out);
}

.page-lead a:hover {
  opacity: 0.5;
}

/* Type spec below is measured off the live lab1.design pages at a
   1600px viewport: role 35.2px/43.93 (1px underline, 7.04px offset),
   body 21.76px/36.99. */

/* ===== Roles listing (Join us) =====
   Two-column rows; odd rows put the illustration right, even rows left,
   so a new <a class="role"> alternates without extra markup. */
.roles {
  margin: 12vw 0 16vw;
  padding: 0 var(--gallery-inset);
}

.role {
  display: grid;
  grid-template-columns: 1fr 1fr;
  align-items: center;
  gap: 4vw;
  /* The anchor box is only a layout frame — rows deliberately overlap it,
     so hit-testing is handed to the media/body instead (below). */
  pointer-events: none;
}

/* Rows interleave the way the original site staggers them: each
   illustration starts a little before the row above it has finished.
   Safe because consecutive rows sit in opposite columns — the overlap
   eats into the empty half of the neighbouring row, never its content.
   -5vw is measured off the live page (~90px at a 2000px viewport). */
.role + .role {
  margin-top: -5vw;
}

/* Restore clicks on the parts that actually carry content, so the
   overlapping frame above never steals a click from the row beneath. */
.role-media,
.role-body {
  pointer-events: auto;
}

.role-media {
  grid-row: 1;
  grid-column: 2;
}

.role-body {
  grid-row: 1;
  grid-column: 1;
}

.role:nth-child(even) .role-media { grid-column: 1; }
.role:nth-child(even) .role-body  { grid-column: 2; }

.role-media img {
  display: block;
  width: 100%;
  height: auto;
}

.role-title {
  margin: 0 0 1.2rem;
  font-size: 2.2rem;         /* 35.2px */
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: normal;
  color: var(--fg);          /* stays solid black — no hover fade */
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 0.2em;
}

.role-tags {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.35rem 1rem;
  margin: 0;
  padding: 0;
  font-size: 1.36rem;        /* 21.76px */
  font-weight: 400;
  line-height: 1.7;
  color: var(--fg);
}

.role-tags li::before {
  content: "#";
}

/* ===== Job posting (Join us → role) =====
   Two columns: the copy runs down the left, a tall photo crop fills the
   right. The title spans both, so it reads across the photo. */
.job {
  display: grid;
  grid-template-columns: 1fr 1fr;
  column-gap: 4vw;
  padding-left: var(--gutter);
}

.job-media {
  grid-column: 2;
  grid-row: 1 / span 2;
  align-self: start;
  margin: 0;
  aspect-ratio: 3 / 8;      /* landscape source, cropped to a tall column */
  background: var(--surface-figure);
}

.job-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.job-title {
  grid-column: 1 / -1;
  grid-row: 1;
  z-index: 1;               /* reads over the photo */
  margin: var(--space-hero) 0 0;
  /* Live page scales the title to fill the column (170.6px at 1600). Our
     Futura PT runs ~3% wider than theirs, so 10.4vw is the largest size
     that still keeps "Senior UX Designer" on one line. */
  font-size: clamp(2.6rem, 10.4vw, 10.4rem);
  font-weight: 700;
  line-height: 1;
  letter-spacing: normal;
}

.job-body {
  grid-column: 1;
  grid-row: 2;
  margin-top: 6.5rem;
  padding-bottom: 8vw;
  font-size: 1.36rem;        /* 21.76px */
  font-weight: 400;
  line-height: 1.7;
}

/* Opening paragraph — underlined on the live posting */
.job-lead {
  text-decoration: underline;
  text-decoration-thickness: 1.5px;
  text-underline-offset: 0.25em;
}

.job-body p {
  margin: 0 0 1.8em;
}

.job-body strong {
  font-weight: 700;
}

.job-body a {
  font-weight: 700;
  border-bottom: 1.5px solid currentColor;
}

/* Emoji summary lines under the title */
.job-perks {
  list-style: none;
  margin: 0 0 3.5rem;
  padding: 0;
}

.job-perks li {
  display: flex;
  gap: 0.6rem;
  margin-bottom: 0.35rem;
}

.job-perk-icon {
  flex: none;
}

.job-list {
  margin: 0 0 1.8em;
  padding-left: 1.2em;
}

.job-list li {
  margin-bottom: 1em;
}

/* ===== Contact =====
   Ported 1:1 from lab1.design/contact. The original is a Squarespace
   fluid-engine page: a 24-column grid with an 11px gutter, a 3vw page inset
   and an 1800px content cap. Both the row height and the type scale with that
   container, so the values below are formulas rather than fixed px — all
   measured off the live page. At a 1600px viewport they resolve to: 48px
   inset, 52.8px section padding, body 21.76px/36.99, expertise list
   35.2px/43.93, photo 1504×812. */
.contact {
  --fe-gap: 11px;                          /* fluid-engine grid gutter */
  --fe-cw: min(94vw, 1800px);              /* content width — 3vw inset a side */
  --fe-row: calc(var(--fe-cw) * 0.0215);   /* engine row height */
  --fe-pad: 3.3vw;                         /* section padding, top and bottom */
  --fe-vw: min(100vw, 1800px);             /* type scales with the viewport, capped */

  width: var(--fe-cw);
  margin: 0 auto;
  padding-bottom: var(--fe-pad);
  font-size: calc(16px + var(--fe-vw) * 0.0036);
  font-weight: 400;
  line-height: 1.7;
}

/* Photo — the original sizes it as 19 engine rows, so its crop loosens as the
   viewport grows (1.85:1 at 1600px, 1.93:1 once the 1800 cap bites). Our
   header is a flat 128px against the original's 120px + 5vw, so the top
   margin makes up the difference and lands the image on the same y. */
.contact-media {
  height: calc(var(--fe-row) * 19 + var(--fe-gap) * 18);
  margin: calc(8.3vw - 8px) 0 0;
  overflow: hidden;
}

.contact-media picture,
.contact-plate picture {
  display: block;
  width: 100%;
  height: 100%;
}

.contact-media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
}

/* Text — the same 24-column grid the original places its blocks on: the postal
   address across columns 1–12, the prose on 14–24, both starting on row 1.
   The sides are not interchangeable:
   the address sits on the same left line as the photograph of the building it
   names, and the prose sits over the coffee it promises. */
.contact-body {
  display: grid;
  grid-template-columns: repeat(24, minmax(0, 1fr));
  grid-template-rows: repeat(8, minmax(var(--fe-row), auto));
  gap: var(--fe-gap);
  margin-top: calc(var(--fe-pad) * 2);
}

.contact-address { grid-area: 1 / 1 / 9 / 13; }
.contact-where   { grid-area: 1 / 14 / 9 / 25; }

.contact-address {
  margin: 0;
  font-style: normal;   /* <address> defaults to italic, and there is no italic cut */
}

/* The 11-column track carries the left edge down onto the coffee plate below.
   The cap only bites above ~1500px, where the track alone would run the measure
   past 70 characters; under that it is inert and the track is the measure. */
.contact-where p {
  margin: 0 0 1.15em;
  max-width: 52ch;
}

.contact-where p:last-child { margin-bottom: 0; }

.contact-list {
  margin: 0;
  font-size: calc(16px + var(--fe-vw) * 0.0094);
  font-weight: 700;
  line-height: 1.248;
  letter-spacing: normal;
}

/* The email sits a full engine row under the address, small enough to read as
   the practical footnote to the display block above it. */
.contact-lines {
  margin: calc(var(--fe-row) + var(--fe-gap)) 0 0;
}

/* Registration data, not a way to reach anyone — plain ink, no underline, and
   a shorter gap than the one above the email so it reads as a footnote to it. */
.contact-tax {
  margin: calc(var(--fe-row) * 0.55) 0 0;
}

.contact-lines a {
  color: #253551;   /* the live page's link ink — used nowhere else */
  text-decoration: underline;
  text-decoration-thickness: 1px;
}

/* The office — two plates hung off the text grid above them, so the building's
   right edge lines up with the prose and the coffee's left edge lines up with
   the address. Both start on row 1; the building runs 18 rows and the coffee 9,
   exactly half, and the white that falls away beside the building's lower half
   is the composition rather than a gap. Row counts also set the crops: 18 rows
   against 12 columns is very close to the building's native 1.09 frame, and 12
   rows against 11 columns takes the coffee's exported 1.8 in to about 1.35 —
   the cup was placed a third in from the left on export precisely so this crop
   leaves it there rather than dead centre. */
.contact-visit {
  display: grid;
  grid-template-columns: repeat(24, minmax(0, 1fr));
  grid-template-rows: repeat(18, var(--fe-row));
  gap: var(--fe-gap);
  margin-top: calc(var(--fe-pad) * 2.6);
}

.contact-plate {
  margin: 0;
  overflow: hidden;
}

.contact-plate img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.contact-plate--building { grid-area: 1 / 1 / 19 / 13; }
.contact-plate--coffee   { grid-area: 1 / 14 / 13 / 25; }

/* ===== Services hero — full-screen pattern =====
   The one place on the site that carries colour: a 100dvh field of Heimka!-
   style triangles with the copy set over it.

   The artwork is assets/images/about/hero-banner-dark.png, recoloured
   from the user's own composition. It is NOT a seamless tile — sampling its
   edges, only 66% of the left/right pixels and 37% of the top/bottom pixels
   match, so `repeat` would draw a visible grid of joins. It goes in as
   `cover`, centred, and crops rather than tiles.

   COPY IS WHITE HERE, and that is what dictated the palette. The original
   artwork was coral #EA5C46 over lavender #BBA8D3; white measures 3.4:1 on
   that coral and only 2.2:1 on that lavender, so a white headline thinned out
   wherever it crossed a triangle and the small paragraph disappeared
   altogether. Of the nine colours on the client's swatch page only three take
   white at all — maroon #803537 (8.5:1), navy #375F8A (6.6:1) and green
   #2F5349 (8.6:1) — so white forces a two-dark pairing. This is maroon over
   navy: warm mapped to warm, cool to cool, so the composition's own figure and
   ground survive the swap. The cost is a quieter pattern — the two tones sit
   1.28:1 apart against each other where coral and lavender sat 1.58:1, and the
   triangles now read on hue separation more than on value. Maroon with green
   would be 1.01:1, i.e. invisible; do not reach for that pair.

   background-color is the artwork's dominant maroon, measured off the file
   (63% of its pixels; the navy is 36%). It shows only in the moment before the
   PNG decodes, and it is also the value the adaptive-ink script reads — the
   script looks at backgroundColor and is blind to background-image.
   data-header-theme="dark" on the section pins the logo, nav and scroll cue to
   white to match the copy.

   Requires .page--media-hero on <body> — that switches the header to fixed so
   the field starts at y=0 instead of below a white strip. Same mechanism the
   template-2 media heroes use.

   Selector is doubled on purpose. At a single class it ties with the plain
   .page-hero rules, and the mobile block's `.page-hero { margin: 12vw 0 0 }`
   comes later in the file — which silently took the full-bleed margin back and
   pushed a white strip above the field on phones. Two classes outrank it at
   every breakpoint. */
.page-hero.page-hero--pattern {
  /* Full-bleed out of the 1600 .case, the same breakout .project-figure uses.
     Replaces .page-hero's `margin: var(--space-hero) 0 0`. */
  width: 100vw;
  margin: 0 calc(50% - 50vw);

  /* Full screen, with the copy optically centred: justify-content centres it
     inside the padding box, and the box is deliberately lopsided — the top
     clears the fixed header, the bottom clears the scroll cue — so the block
     settles a little above true centre, which is where the eye expects it. */
  position: relative;              /* containing block for .scroll-cue */
  min-height: 100dvh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding: calc(var(--header-pad-top) + 2rem) 0 calc(48px + 4rem);

  text-align: left;          /* .page-hero is centred; this hero is not */

  /* Page copy, not header ink — but it is the same white, and naming the token
     keeps the two in step if the inverse ink is ever retuned. */
  color: var(--ink-inverse);

  background-color: #803537;
  /* Layers paint front to back: scrim-free here, so the artwork sits on its
     own ~20px preview (assets/css/image-previews.css), which is what fills
     the fold for the beat before the PNG decodes. */
  background-image: url("assets/images/about/hero-banner-dark.png"),
    var(--preview-about-hero-banner-dark, none);
  background-repeat: no-repeat;
  background-position: center center;
  background-size: cover;
}

/* The field bleeds edge to edge, but its copy must line up with the service
   rows below, which sit inside the centred 1600 .case. So the text gets its
   own copy of that measure and the same --gallery-inset the rows use. */
.page-hero-inner {
  /* width: 100% is load-bearing. The hero is a column flex container, so this
     is a flex item on the cross axis, and `margin: 0 auto` switches off the
     default `align-items: stretch` — without an explicit width the block
     shrinks to its widest child and centres, which slides the copy ~170px
     right of the service rows it is supposed to line up with. */
  width: 100%;
  max-width: var(--max);
  margin: 0 auto;
  padding: 0 var(--gallery-inset);
}

/* .page-lead centres itself on a 1000px measure; left-aligned it wants a
   slightly wider one, and no auto margins. */
.page-hero--pattern .page-lead {
  margin: 0;
  max-width: 1150px;
}

/* Supporting paragraph under the lead. Same spec as .project-intro-body, but
   left-aligned on its own narrower measure instead of centred on 1000px — the
   lead runs to 1150 above it, so a shorter line here keeps the two blocks
   from reading as one slab. */
.page-hero-body {
  margin: 2.2rem 0 0;
  max-width: 720px;
  font-size: 1.35rem;
  font-weight: 400;
  line-height: 1.6;
  letter-spacing: 0.002em;
}

/* ===== Services (services.html) =====
   A numbered list of what we do, borrowed in shape from the reference sites
   the user pointed at (Flying Bisons' 01–09 index, Tonik's expand-in-place
   rows) and rebuilt on our own paper: white, black, Futura, hairline rules,
   no colour and no rounded corners.

   Each row is a native <details>; they share name="service", so the browser
   itself keeps one open at a time — no JS on this page beyond the shared
   header script. Where ::details-content is supported the panel animates its
   own height open and closed; where it isn't (Safari/Firefox today) the panel
   simply snaps, which is the correct degradation, not a bug to chase.

   Same measure as the Join us listing: inside .case, inset by --gallery-inset,
   so the rules line up with the home gallery's edges. */
.services {
  /* Page-scoped, like the contact page's link ink — a rule this heavy has no
     other consumer, and --rule-soft (#eee) disappears under 44px type. */
  --svc-rule: rgba(0, 0, 0, 0.16);
  --svc-num: 7rem;         /* number column; the panel indents to match */

  /* Lets the ::details-content height transition below interpolate to `auto`.
     Inherited, so it is scoped to this section and cannot reach any other
     page's `auto` sizes. */
  interpolate-size: allow-keywords;

  margin: 10vw 0 0;
  padding: 0 var(--gallery-inset);
}

/* Both rules live on the row, never on the section: a border on .services
   would sit outside its 54px padding and run wider than every rule above it. */
.service {
  border-top: 1px solid var(--svc-rule);
}

.service:last-of-type {
  border-bottom: 1px solid var(--svc-rule);
}

/* The clickable row. Grid rather than flex so the panel below can indent by
   exactly one --svc-num and land under the name. */
.service-row {
  display: grid;
  grid-template-columns: var(--svc-num) minmax(0, 1fr) auto;
  align-items: center;
  padding: 2.2rem 0;
  cursor: pointer;
  list-style: none;   /* Firefox's disclosure triangle */
}

.service-row::-webkit-details-marker {
  display: none;      /* Safari's */
}

/* Chrome/Edge keep the row focusable; give keyboard users the same box the
   pointer gets rather than a triangle-shaped outline. */
.service-row:focus-visible {
  outline: 2px solid var(--fg);
  outline-offset: 4px;
}

.service-index {
  font-size: 1.05rem;
  font-weight: 500;
  line-height: 1;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.04em;
}

/* One notch under .project-block-title (2.75rem): eight of these stack up, and
   at the block-title size the longest name ("AI Assisted Prototyping…") turns
   over to two lines well before the tablet breakpoint. */
.service-name {
  font-size: 2.4rem;
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -0.01em;
}

/* Arrow, drawn in the same hairline language as .project-nav-icon. Points down
   when closed, and rotates a half-turn into an up-arrow once the row is open,
   so its direction always describes what the click will do next. */
.service-toggle {
  flex: none;
  width: 26px;
  height: 26px;
  margin-left: 2rem;
  transition: transform var(--dur-slow) var(--ease-out),
              opacity var(--dur-snap) ease;
}

.service-toggle svg {
  display: block;
  width: 100%;
  height: 100%;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.4;
  stroke-linecap: square;
}

.service[open] .service-toggle {
  transform: rotate(180deg);
}

/* Hover fades the number and the arrow the same 0.5 the project pagination
   uses — but NOT the title, which stays solid black. Same rule .role-title
   follows on Join us: the row name is the thing being read, and greying it out
   on approach makes the list look disabled under the cursor. The index and the
   arrow carry the affordance on their own. */
.service-index {
  transition: opacity var(--dur-snap) ease;
}

.service-row:hover .service-index,
.service-row:hover .service-toggle {
  opacity: 0.5;
}

/* Animated reveal. ::details-content is the box the browser wraps around
   everything after the <summary>; it ships `content-visibility: hidden` while
   closed, which is why the discrete transition has to travel with the height —
   without it the panel would pop in at full size on the last frame. */
.service::details-content {
  block-size: 0;
  overflow: hidden;
  transition: block-size var(--dur-slow) var(--ease-out),
              content-visibility var(--dur-slow) allow-discrete;
}

.service[open]::details-content {
  block-size: auto;
}

/* Copy left, capability list right, on two even tracks. Even rather than
   weighted on purpose: the tracks *are* the measure here — no max-width on the
   paragraph — so the copy fills its half instead of stopping short and leaving
   a dead channel down the middle of the panel. At 1600 that lands the body at
   ~64 characters, the same measure the case-study blocks run. */
.service-panel-inner {
  display: grid;
  /* The list track is deliberately narrower than the copy track. Right-aligning
     the items alone only moves the right-hand stack to the panel edge; pulling
     the track in is what carries the left-hand stack rightwards with it, so the
     two read as one block hung off the right margin. */
  grid-template-columns: minmax(0, 1fr) minmax(0, 0.72fr);
  gap: 4vw;
  /* The top padding is the gap between the row title and the panel. It has to
     live here rather than on .service-row, whose padding also sets the height
     of every CLOSED row — growing that would space out the whole list. */
  padding: 2.2rem 0 3.4rem var(--svc-num);
}

/* Two thirds the size of .service-name (2.4rem) so the row title stays the
   loudest thing in the panel — at 1.9rem the two read as competing headings.
   No max-width: the grid column is the measure. An 18ch cap used to turn every
   claim over after four or five words while most of the column sat empty. */
.service-claim {
  margin: 0 0 1.5rem;
  font-size: 1.6rem;
  font-weight: 700;
  line-height: 1.25;
  letter-spacing: -0.005em;
}

/* The site's body paragraph, a hair under .project-block-body (1.35rem). */
.service-body {
  margin: 0;
  font-size: 1.3rem;
  font-weight: 400;
  line-height: 1.65;
  letter-spacing: 0.002em;
}

.service-list {
  list-style: none;
  margin: 0;
  padding: 0;
  /* Bottom-aligned, not top: the list is always shorter than the copy beside
     it, and hanging it from the top left a ragged gap under it. Sharing a
     baseline with the last line of the paragraph closes the panel cleanly. */
  align-self: end;
  columns: 2;
  column-gap: 2.5vw;
  font-size: 1.15rem;
  font-weight: 400;
  line-height: 2.1;
  /* Each of the two stacks right-aligns inside its own column box, so the
     right one hugs the panel edge and the left one hugs the column gap.
     Overridden back to left in the mobile block, where the list is a single
     column under the copy and a ragged left edge would read as a mistake. */
  text-align: right;
}

.service-list li {
  break-inside: avoid;
}

/* Ornament, not a bullet: it reads as part of the phrase ("# Product roadmap")
   rather than a list marker, which is why it lives in the text flow instead of
   ::marker. The escape is a non-breaking space — a plain one in `content` can
   collapse or let the hash fall to the end of a line on its own. */
.service-list li::before {
  content: "#\00a0";
}

/* Two right-aligned stacks only survive a wide viewport. Measured: the longest
   items ("# Customer journey mapping", "# Information architecture") run
   ~235px at 18.4px Futura, and a two-column split of the narrowed 0.72fr track
   drops under that below roughly 1440 — at 821 the item was breaking over
   THREE lines. So below 1400 the list track goes back to matching the copy and
   the stacks collapse to one. Right-alignment and the # ornament stay; only the
   column count and the track ratio move. */
@media (max-width: 1400px) {
  .service-panel-inner {
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  }

  .service-list {
    columns: 1;
  }
}

/* ===== Services — closing CTA =====
   Centred, like every other closing line on the site. Reads at the job-title
   scale rather than the 53px hero, so the eight rules above stay the loudest
   thing on the page. */
.services-cta {
  margin: 8vw 0 10vw;
  padding: 0 var(--gutter);
  text-align: center;
}

.services-cta-lead {
  margin: 0 auto;
  max-width: 24ch;
  font-size: 2.75rem;
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -0.01em;
}

/* The link sits on its own line rather than inline in the sentence: at this
   size an inline link breaks mid-phrase on a narrow screen and the underline
   turns over with it. Same underline treatment as the Join us hero's mailto,
   one step down in scale so it reads as the action, not a second statement. */
.services-cta-link {
  display: inline-block;
  margin-top: 2.2rem;
  font-size: 1.5rem;
  font-weight: 500;
  color: inherit;
  text-decoration: underline;
  text-decoration-thickness: 2px;
  text-underline-offset: 0.2em;
  transition: opacity var(--dur-fast) var(--ease-out);
}

.services-cta-link:hover {
  opacity: 0.5;
}

@media (prefers-reduced-motion: reduce) {
  .service::details-content,
  .service-toggle {
    transition: none;
  }
}

/* ===== Lab1 Nexus — hero artwork (lab1-nexus.html) =====
   Not a new hero mechanism: the section wears .page-hero--pattern as well, so
   the full-bleed breakout, the 100dvh box, the lopsided padding that optically
   centres the copy and the white ink all come from the services hero above.
   Only the artwork, the scrim and the pre-decode colour are page-specific.

   Selector is tripled to clear .page-hero.page-hero--pattern at both
   breakpoints — that rule is doubled for the same reason (see its note), and a
   plain .page-hero--nexus would tie with it and lose on source order inside
   the mobile block.

   The scrim is load-bearing, not decoration. The artwork runs pale cream on the
   left third and near-black on the right, so white copy over the raw image
   would read at ~1.6:1 on the left. The gradient is angled the same way the
   image is (light → dark, left → right) and weighted to cancel it: 0.82 black
   over the cream lands the hero lead at ~11:1, the far right at ~15:1, and the
   amber ribbons still come through on the right half where nothing sits on top.

   background-color is the artwork's dark brown. The beat before the WebP
   decodes is covered by the artwork's own preview layer now (see "Soft image
   loading"), so what this colour is really for is the value the adaptive-ink
   script reads, since it looks at backgroundColor and is blind to
   background-image. The section pins data-header-theme="dark" anyway. */
.page-hero.page-hero--pattern.page-hero--nexus {
  /* Named once so the mobile artwork swap can restate the image layer without
     keeping a second copy of the gradient in step with this one. */
  --nexus-scrim: linear-gradient(
    96deg,
    rgba(18, 9, 4, 0.82) 0%,
    rgba(18, 9, 4, 0.66) 38%,
    rgba(18, 9, 4, 0.44) 72%,
    rgba(18, 9, 4, 0.32) 100%
  );

  background-color: #2a170d;
  background-image: var(--nexus-scrim), url("assets/images/nexus/hero.webp"),
    var(--preview-nexus-hero, none);
}

/* ===== Services — fluid hero (services.html, 2026-10-31) =====
   Replaces the triangle artwork on the services hero, which lost the copy once
   the lead went thin (Neue Montreal Light). Same mechanism as the Nexus hero:
   the section keeps .page-hero--pattern and this modifier only swaps the image.
   The triangles are NOT deleted — drop `page-hero--fluid` from the section in
   services.html and the base .page-hero--pattern rule takes them back.

   Photo: deep teal water, Unsplash (unsplash.com/photos/G9i_plbfDgk, Unsplash
   License) — picked over the first try (blue fluid waves, Pawel Czerwinski,
   f4SwwVbM_IY; still on disk as services/hero.webp + hero-mobile.webp) because
   the waves' pale ribbons ran through the thin lead. This one has no lines at
   all: mean luminance ~17/255 under the copy, so the scrim is only a whisper
   that keeps the one lighter patch, top centre, off the headline. */
.page-hero.page-hero--pattern.page-hero--fluid {
  --fluid-scrim: linear-gradient(
    96deg,
    rgba(0, 10, 12, 0.35) 0%,
    rgba(0, 10, 12, 0.15) 60%,
    rgba(0, 10, 12, 0) 100%
  );

  background-color: #001b21;
  background-image: var(--fluid-scrim), url("assets/images/services/hero-deep.webp"),
    var(--preview-services-hero-deep, none);
}

/* ===== Scope includes (lab1-nexus.html) =====
   The original page sets five headings in a 3-across grid, each underlined,
   with the body copy broken one sentence to a line. Same grid and same reading
   order here, rebuilt on our paper: the underline becomes the hairline rule the
   services rows use, and the sentences run as an ordinary paragraph. The 01–05
   index is the one addition — it ties the section to the numbered services
   list, which is the sibling page a reader arrives from.

   Same measure as that list and the home gallery (--gallery-inset), so the
   rules line up with every other grid edge on the site. */
.scope {
  /* Page-scoped for the same reason .services scopes --svc-rule: --rule-soft
     (#eee) disappears as a divider once there is type this size above it. */
  --scope-rule: rgba(0, 0, 0, 0.16);

  margin: 10vw 0 0;
  padding: 0 var(--gallery-inset);
}

.scope-title {
  margin: 0 0 3.5vw;
  font-size: 2.75rem;      /* the .project-block-title / section-heading step */
  font-weight: 700;
  line-height: 1.15;
  letter-spacing: -0.01em;
}

/* An <ol> — the phases have an order, and the numbers are real content, so the
   list markers are replaced by .scope-index rather than faked with ::before. */
.scope-grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  /* Horizontal gap is the home gallery's 32px so the three columns sit on the
     same rhythm; the row gap is fluid because a row here is a text block, not
     a fixed-ratio thumb, and 32px between two of them reads as one slab. */
  gap: 4vw var(--gallery-gap-x);
}

/* The rule goes on the item, never on the grid: a border on .scope would sit
   outside its 54px padding and run wider than every rule and image edge above. */
.scope-item {
  border-top: 1px solid var(--scope-rule);
  padding-top: 1.5rem;
}

.scope-index {
  display: block;
  margin-bottom: 1.1rem;
  font-size: 1rem;
  font-weight: 500;
  line-height: 1;
  letter-spacing: 0.06em;
  font-variant-numeric: tabular-nums;
}

.scope-name {
  margin: 0 0 1.1rem;
  font-size: 1.6rem;
  font-weight: 700;         /* Bold, like every other heading — 600 Demi read too thin */
  line-height: 1.25;
  letter-spacing: -0.005em;
}

/* One step under .project-block-body (1.35rem): three of these sit side by side
   on a third of the measure, so the body has to give the headings room. */
.scope-body {
  margin: 0;
  font-size: 1.15rem;
  font-weight: 400;
  line-height: 1.6;
  letter-spacing: 0.002em;
}

/* ===== Client marquee (lab1-nexus.html) =====
   The names the framework has shipped, running edge to edge under a small
   caption — the closing band of the original page.

   CSS only. The track holds the same list twice and slides exactly -50%, which
   lands copy two where copy one started, so the loop has no seam and no JS has
   to measure anything. Copy two is aria-hidden, so a screen reader gets the
   four names once. Every group carries its own trailing bullet plus a
   padding-right equal to the inner gap, which makes the gap across the seam
   identical to the gaps inside a group — the usual giveaway that a marquee is
   two copies of a list.

   Full-bleed out of the centred 1600 .case: a ticker that stops short of the
   screen edges reads as a widget instead of a band. */
.marquee {
  width: 100vw;
  margin: 10vw calc(50% - 50vw);
}

.marquee-caption {
  margin: 0 0 2.5rem;
  padding: 0 var(--gutter);
  text-align: center;
  font-size: 1.0625rem;    /* the flat footer size — this is a label, not copy */
  font-weight: 500;
  letter-spacing: 0.01em;
}

.marquee-viewport {
  overflow: hidden;
}

.marquee-track {
  display: flex;
  width: max-content;
  animation: marquee-slide 34s linear infinite;
}

.marquee-group {
  --marquee-gap: clamp(2rem, 3.4vw, 4.5rem);

  list-style: none;
  margin: 0;
  padding: 0 var(--marquee-gap) 0 0;
  display: flex;
  align-items: center;
  gap: var(--marquee-gap);

  /* Smaller than the original page sets it (user, 2026-08-27) — and the
     line-height is a hard floor, not taste. Chrome sizes the line box off
     hhea, and this font reports ascent 982 / descent -300 on a 1000 upem, so
     its content area is 1.282em with ink reaching 0.26em below the baseline.
     Anything under line-height 1.202 pushes that ink outside the line box,
     where .marquee-viewport's `overflow: hidden` shears it off — at 1.1 the
     descender of the "g" in "Cognite" lost ~6px. 1.3 clears the deepest ink
     by 0.067em at both ends, so the band cannot clip at any font-size. */
  font-size: clamp(2.5rem, 5.4vw, 5.5rem);
  font-weight: 700;
  line-height: 1.3;
  letter-spacing: -0.02em;
  white-space: nowrap;
}

/* Flex, so the bullet that closes each name is spaced by the same gap as the
   names themselves — the bare text node becomes an anonymous flex item and the
   gap applies to it too. */
.marquee-group li {
  display: flex;
  align-items: center;
  gap: var(--marquee-gap);
}

/* Futura's bullet is a small dot inside a tall box; at the group's own size it
   would out-weigh the letters, and box-centring parks it a touch under the
   optical middle of the caps, hence the nudge. U+2022 is in the .ttf cmaps
   (checked); U+00B7 is NOT, so don't "tidy" this into a middot. */
.marquee-dot {
  font-size: 0.62em;
  line-height: 1;
  transform: translateY(-0.06em);
}

@keyframes marquee-slide {
  from { transform: translateX(0); }
  to   { transform: translateX(-50%); }
}

/* No motion: the band stops being a band. The duplicate list goes, the single
   remaining one wraps and centres, and its trailing bullet is dropped — a
   bullet at the end of a static list has nothing to separate. */
@media (prefers-reduced-motion: reduce) {
  .marquee-track {
    width: auto;
    animation: none;
    justify-content: center;
  }

  .marquee-track .marquee-group[aria-hidden="true"] {
    display: none;
  }

  .marquee-group {
    flex-wrap: wrap;
    justify-content: center;
    padding-right: 0;
  }

  .marquee-group li:last-child .marquee-dot {
    display: none;
  }
}

/* ===== Mobile nav panel ===== */
.mobile-nav {
  display: none;
}

/* ===== Responsive ===== */
@media (max-width: 820px) {
  :root {
    --gutter: 7vw;
    --indent: 7vw;
  }

  .header-inner {
    justify-content: space-between;
    align-items: center;
    min-height: 0;
    padding: 1.4rem var(--gutter);
    z-index: 2;   /* keeps logo + X above the full-screen panel below */
  }

  .brand {
    position: static;
    top: auto;
    left: auto;
  }

  .brand img {
    width: auto;
    height: 22px;
  }

  .nav {
    display: none;
  }

  .nav-toggle {
    display: flex;
  }

  body.nav-open .nav-toggle span:nth-child(1) {
    transform: translateY(5px) rotate(45deg);
  }
  body.nav-open .nav-toggle span:nth-child(2) {
    transform: translateY(-5px) rotate(-45deg);
  }

  /* Full-screen menu: an opaque sheet over the whole viewport, links centred.
     It sits inside .site-header, so its z-index is resolved against the
     header's stacking context — 1 here, .header-inner is 2, which is what
     keeps the logo and the X drawn on top of it. */
  .mobile-nav {
    display: flex;
    position: fixed;
    inset: 0;
    z-index: 1;
    align-items: center;
    justify-content: center;
    background: var(--bg);
    opacity: 0;
    visibility: hidden;
    transition: opacity var(--dur-fast) var(--ease-out),
      visibility 0s linear var(--dur-fast);
  }

  body.nav-open .mobile-nav {
    opacity: 1;
    visibility: visible;
    transition: opacity var(--dur-fast) var(--ease-out), visibility 0s;
  }

  body.nav-open {
    overflow: hidden;   /* the page behind the sheet must not scroll */
  }

  /* Over the white sheet the ink is always black — the adaptive script reads
     the content underneath, which is the wrong answer while the menu is up. */
  body.nav-open .brand[data-theme] img {
    filter: invert(0);
  }

  body.nav-open .nav-toggle[data-theme] {
    color: var(--fg);
    filter: none;
  }

  .mobile-nav ul {
    width: 100%;
    margin: 0;
    padding: 0 var(--gutter);
    list-style: none;
    text-align: center;
  }

  .mobile-nav a {
    display: block;
    font-size: clamp(1.9rem, 8.7vw, 2.6rem);
    font-weight: 300;
    line-height: 1.66;
    letter-spacing: 0.005em;
  }

  /* No .is-active rule here on purpose: the sheet marks nothing, like the
     live site. .nav-list a.is-active is the desktop underline only. */

  .intro {
    margin: 12vw 0 10vw;
    padding-right: var(--gutter);
  }

  .intro-eyebrow {
    margin-bottom: 1.5rem;
    font-size: 1.15rem;
  }

  .intro-heading {
    font-size: clamp(1.65rem, 6.5vw, 2.4rem);
    line-height: 1.3;
  }

  .gallery {
    grid-template-columns: 1fr;
    gap: 3rem;
    margin-top: 6vw;
    padding: 0 var(--gutter);   /* align with header/intro (54px desktop → 7vw mobile) */
  }

  .project-title {
    margin-top: 0.85rem;
    font-size: 1.1rem;
  }

  /* Project template — mobile */
  .project-hero {
    margin: 12vw 0 0;
    padding-right: var(--gutter);
  }

  .project-eyebrow,
  .page-eyebrow {
    margin-bottom: 1.5rem;
    font-size: 1.15rem;
  }

  /* keep the symmetry on mobile, where the hero margin drops to 12vw */
  .project-hero + .project-figure {
    margin-top: 12vw;
  }

  .project-figure--mobile {
    display: block;
  }

  /* The wide frame whose phone version is something else entirely (the
     intel carousel) — keep it lazy so the phone never fetches it. */
  .project-figure--desktop {
    display: none;
  }

  .project-lead,
  .page-lead {
    font-size: clamp(1.65rem, 6.5vw, 2.4rem);
    line-height: 1.3;
  }

  .project-block {
    margin: 14vw auto;
    padding: 0 var(--gutter);
  }

  .project-hero + .project-block {
    margin-top: 9vw;
  }

  .project-block-title {
    font-size: 1.9rem;
  }

  .project-block-body p {
    font-size: 1.15rem;
    line-height: 1.55;
  }

  /* The band stops being an overlay here. At phone width the wide crop loses
     nearly all its height, so the <picture> swaps in a square one and the copy
     leaves the overlay to sit above it (user, 2026-08-25) — title and quote
     read as the closing note of the body block above them, on the page's own
     white, and the square artwork lands under them. Left-aligned here (user,
     2026-08-25): with the copy on the page's own full-width column rather than
     in a narrow overlay, the desktop rag left every line starting at a
     different x, so it shares the body block's left edge instead.

     The dark field moves off the section and onto the <picture>: the section
     now backs the copy as well, and only the image still needs a colour under
     it while it decodes. */
  .project-band {
    margin-top: 14vw;
    background: transparent;
  }

  .project-band > picture {
    background: var(--band-field, var(--surface-figure));
  }

  .project-band-copy {
    position: static;
    width: auto;
    padding: 0 var(--gutter) 12vw;
    text-align: left;
    color: var(--fg);
  }

  .project-band-title {
    font-size: 2rem;
  }

  .project-band-text {
    font-size: 1.05rem;
  }

  .project-band-cite {
    justify-content: flex-start;
    margin-top: 1.5rem;
    font-size: 0.95rem;
  }

  .project-band-avatar {
    width: 40px;
    height: 40px;
  }

  /* The quote band has no picture to carry the dark field, so it keeps it on
     the section and stays white-on-dark; one column. */
  .project-band--quote {
    padding: 14vw var(--gutter);
    /* The shared ≤820px .project-band rule above clears the whole background
       shorthand, gradient included, so both layers go back on here. */
    background: var(--band-glow), var(--band-field, var(--surface-figure));
  }

  .project-band--quote .project-band-copy {
    padding: 0;
    color: var(--ink-inverse);
  }

  .project-band--quote blockquote {
    column-count: 1;
  }

  .project-band--quote .project-band-cite {
    margin-top: 2rem;
  }

  /* One column for all three rows: title, then each paragraph, then each
     square, then the foot. Side inset drops to --gutter so the spread lines up
     with the header, intro and body copy like every other full-bleed row. */
  .project-spread {
    margin-top: 20vw;
    padding: 0 var(--gutter);
  }

  .project-spread-head,
  .project-spread-pair {
    display: block;
  }

  .project-spread-title {
    font-size: 1.9rem;
  }

  .project-spread-text {
    margin-top: 1.2rem;
    font-size: 1.05rem;
  }

  .project-spread-pair {
    margin-top: 10vw;
  }

  .project-spread-pair figure + figure {
    margin-top: var(--gallery-gap-x);
  }

  .project-spread-foot {
    margin-top: 7vw;
  }

  .project-spread-logo {
    height: 20px;
  }

  .project-spread-note {
    font-size: 0.85rem;
  }

  /* On a phone the wide shot would collapse to a letterbox strip next to two
     near-square images, so it takes the pair's own box instead — same 1 / 1.1
     ratio, upright source, cropped by object-fit. The three then stack as one
     consistent column. */
  .project-spread-shot {
    margin-top: var(--gallery-gap-x);
    aspect-ratio: 1 / 1.1;
  }

  .project-spread-shot img {
    height: 100%;
    object-fit: cover;
  }

  /* The context section keeps its overlap on a phone — it is the point of the
     composition — but drops to the original's narrow grid: 8 columns, a flat
     24px row instead of the width-derived one, and the pair moves below the
     copy with the back plate shifted right and the front plate left. Inset
     goes to --gutter so it lines up with the header and the body blocks.

     Two deliberate departures from the original's phone grid (user, 2026-08-25):
     the copy runs the full 8 columns instead of stopping at 7 — at this width
     the original's one-column hang just reads as a ragged right edge, not as
     structure — and rows 8-10 are left empty so the photographs get real air
     under the text rather than following it at one 11px gutter. */
  .project-context {
    --context-row: 24px;

    margin-top: 18vw;
    padding: 0 var(--gutter);
    grid-template-columns: repeat(8, minmax(0, 1fr));
    grid-template-rows: repeat(21, minmax(var(--context-row), auto));
  }

  .project-context-title {
    grid-area: 1 / 1 / 3 / 9;
  }

  .project-context-body {
    grid-area: 3 / 1 / 8 / 9;
  }

  .project-context-body p {
    line-height: 1.55;
  }

  .project-context-photo--back {
    grid-area: 11 / 3 / 21 / 9;
  }

  .project-context-photo--front {
    grid-area: 13 / 1 / 19 / 8;
  }

  /* Below the original's 767px breakpoint the pagination doubles its side
     inset to 6vw and its title stops scaling with the viewport *width* —
     it scales with the viewport height instead (capped at 900px), so the
     two titles can't collide on a narrow screen. Both values are the
     original's; only the breakpoint is ours (820px, so it turns over with
     the rest of the mobile layout).

     The vertical band is also re-balanced here: at 3vw the pagination sat
     tight under the last section while the footer's own top padding gave it
     twice the air underneath. The air above it is now the footer's top
     padding (same expression as .site-footer, whose --footer-measure isn't
     global) and the block's own padding is equal top and bottom, so the two
     titles float on an identical gap up to the last section and down to
     "Made by Lab1". */
  .project-nav {
    margin-top: calc(0.0545 * min(94vw, 1800px));
    padding: 6vw;
  }

  .project-nav-title {
    font-size: calc(0.012 * min(100vh, 900px) + 1rem);
  }

  .project-nav-icon {
    margin-right: 16px;
  }

  .project-nav-link--next .project-nav-icon {
    margin-right: 0;
    margin-left: 16px;
  }

  /* Mobile only: the project names wrapped into ragged 2–3 line stacks at
     phone width, so the titles give way to a plain "Previous" / "Next" in the
     same type. Generated content, not markup — every case study shares this
     nav, and the link's aria-label still names the project for screen
     readers, so the visible word is decoration only. */
  .project-nav-title {
    display: none;
  }

  .project-nav-link::after {
    font-size: calc(0.012 * min(100vh, 900px) + 1rem);
    font-weight: 700;
    line-height: 1;
    transition: opacity var(--dur-snap) ease;
  }

  .project-nav-link--prev::after { content: "Previous"; }

  .project-nav-link--next::after {
    content: "Next";
    order: -1;   /* label before the chevron, as the title was */
  }

  .project-nav-link:hover::after {
    opacity: 0.5;
  }

  /* Footer: the original line, but with more air under it — at phone width
     the desktop clamp bottoms out at 24px and the line sat on the screen
     edge. */
  .site-footer {
    padding-bottom: 56px;
  }

  /* Join us — single column, illustration above the copy on every row */
  .page-hero {
    margin: 12vw 0 0;
  }

  .page-lead a {
    text-decoration-thickness: 1px;   /* 2px is heavy at the mobile size */
  }

  /* The old 6vw row padding is folded in here, so the air above the first
     illustration and below the last one is unchanged by the rule below. */
  .roles {
    margin: 16vw 0 20vw;
    padding: 0 var(--gutter);
  }

  .role {
    grid-template-columns: 1fr;
    gap: 2rem;
  }

  /* Single column, so nothing to interleave — instead the space *between*
     two listings has to clearly out-weigh the 2rem *inside* one (image →
     title), or both rows read as one long block. Clamped rather than pure
     vw: 12vw was only ~1.9x the inner gap on a phone, and an unclamped
     value would swing from too tight at 375px to a canyon at 820px. */
  .role + .role {
    margin-top: clamp(5.5rem, 18vw, 7.5rem);
  }

  .role-media,
  .role:nth-child(even) .role-media {
    grid-row: 1;
    grid-column: 1;
  }

  .role-body,
  .role:nth-child(even) .role-body {
    grid-row: 2;
    grid-column: 1;
  }

  .role-title {
    font-size: 1.7rem;
  }

  .role-tags {
    font-size: 1.15rem;
  }

  /* Job posting — single column: title, photo, then the copy */
  .job {
    grid-template-columns: 1fr;
    padding: 0 var(--gutter);
  }

  .job-title {
    grid-column: 1;
    grid-row: 1;
    margin-top: 12vw;
    font-size: clamp(2rem, 9vw, 3.2rem);
  }

  .job-media {
    grid-column: 1;
    grid-row: 2;
    margin: 2.5rem 0 0;
    aspect-ratio: 3 / 2;
  }

  .job-body {
    grid-column: 1;
    grid-row: 3;
    margin-top: 3rem;
    padding-bottom: 14vw;
    font-size: 1.15rem;
    line-height: 1.65;
  }

  /* Contact — the original's mobile engine survives only as the numbers: a flat
     24px row, a 6vw inset and frozen type (19.24 / 23.5). The 8-column grid is
     retired below, because after the 2026-08-27 rebuild every block is
     full-width and the row counts were doing nothing but spacing. Breakpoint is
     820px, not the original's 767, so the page turns over with the nav. */
  .contact {
    --fe-cw: 88vw;
    --fe-row: 24px;
    --fe-pad: 29.7px;
    font-size: 19.24px;
  }

  /* Portrait crop of Długi Targ, re-cut from the same 2500w original as the
     desktop file at the user's framing. It runs at its own ratio — no fixed
     height and no `cover`, so a hand-made crop is never cropped a second time. */
  .contact-media {
    height: auto;
    margin-top: 12vw;
  }

  .contact-media img {
    height: auto;
  }

  /* One column: the blocks stack on an explicit rhythm instead of on leftover
     grid rows. Everything here is a multiple of --fe-pad, so the three gaps
     stay in proportion if that constant ever moves. */
  .contact-body {
    display: block;
    margin-top: calc(var(--fe-pad) * 2.2);
  }

  .contact-where {
    margin-top: calc(var(--fe-pad) * 1.8);
  }

  .contact-where p { max-width: none; }

  .contact-list {
    font-size: 23.5px;
  }

  /* Plates stack in one identical box each — the user asked for the coffee to
     match the building's near-square mobile frame. That is why the coffee has
     its own crop (`coffee-mobile.webp`, 0.85): cover-cropping the 1.8 desktop
     file into this box would cut half the field away and leave the cup on the
     edge. Both boxes come off the same declaration, so they cannot drift.

     They are sized by RATIO, not by engine rows. A row-count height against a
     88vw width is not a shape — it drifts from 0.75 at a 320px phone to 1.93 at
     820, so "near-square" would have held only around 375. `aspect-ratio` holds
     0.85 at every width in the tier, which is also exactly the mobile coffee
     crop, so that photograph is never cropped twice. */
  .contact-visit {
    display: block;
    margin-top: calc(var(--fe-pad) * 2.4);
  }

  .contact-plate--building,
  .contact-plate--coffee {
    height: auto;
    aspect-ratio: 0.85;
  }

  .contact-plate--coffee {
    margin-top: var(--fe-pad);
  }

  /* Services hero — the band keeps bleeding full width; only the air above the
     text and the tile size come down. The mobile header is 1.4rem padding on a
     22px logo (~67px), so 5rem + 10vw leaves a comfortable gap under the nav
     without the desktop's 266px. */
  /* Still a full screen, just less air: the mobile header is 1.4rem padding on
     a 22px logo (~67px), so 5rem clears it. background-size stays `cover` from
     the desktop rule — at a phone's aspect ratio that crops the 16:9 artwork
     hard to the centre, which is the right call for an abstract field. */
  .page-hero.page-hero--pattern {
    padding: 5rem 0 calc(48px + 3rem);
  }

  .page-hero-body {
    margin-top: 1.6rem;
    max-width: none;
    font-size: 1.1rem;
  }

  .page-hero-inner {
    padding: 0 var(--gutter);
  }

  .page-hero--pattern .page-lead {
    max-width: none;
  }

  /* Services — the two-column panel folds to one, the number column narrows,
     and the section swaps the desktop-only 54px inset for the page gutter,
     exactly as the home gallery does. The panel keeps its indent so the copy
     still starts on the name's left edge. */
  .services {
    --svc-num: 2.9rem;
    margin-top: 14vw;
    padding: 0 var(--gutter);
  }

  .service-row {
    padding: 1.5rem 0;
  }

  .service-index {
    font-size: 0.9rem;
  }

  .service-name {
    font-size: clamp(1.35rem, 5.4vw, 1.9rem);
  }

  .service-toggle {
    width: 20px;
    height: 20px;
    margin-left: 1rem;
  }

  .service-panel-inner {
    grid-template-columns: minmax(0, 1fr);
    gap: 1.8rem;
    padding-top: 1.4rem;
    padding-bottom: 2.4rem;
  }

  /* .service-name clamps to 1.35rem at phone widths, so the claim has to come
     down too or the two land on exactly the same size. */
  .service-claim {
    font-size: 1.2rem;
  }

  .service-body {
    font-size: 1.1rem;
  }

  /* Left-aligned again: at this width the list is one column stacked under the
     copy, sharing its left edge — the desktop right-alignment would leave it
     floating with a ragged left. The # ornament stays. */
  .service-list {
    columns: 1;
    font-size: 1.02rem;
    line-height: 1.95;
    text-align: left;
  }

  .services-cta {
    margin: 14vw 0 16vw;
  }

  .services-cta-lead {
    max-width: none;
    font-size: clamp(1.7rem, 7.5vw, 2.4rem);
  }

  .services-cta-link {
    margin-top: 1.8rem;
    font-size: 1.25rem;
  }

  /* Nexus hero — the lighter 1200px artwork. A phone never needs the 2560px
     file, and decoding it costs ~17MB of bitmap for a 390px box. Everything
     else (the scrim, `cover`, the centred position) is inherited: at a phone's
     ratio `cover` crops the landscape artwork hard to its middle, which is the
     right call for an abstract field. */
  .page-hero.page-hero--pattern.page-hero--nexus {
    background-image: var(--nexus-scrim), url("assets/images/nexus/hero-mobile.webp"),
      var(--preview-nexus-hero-mobile, none);
  }
  .page-hero.page-hero--pattern.page-hero--fluid {
    background-image: var(--fluid-scrim), url("assets/images/services/hero-deep-mobile.webp"),
      var(--preview-services-hero-deep-mobile, none);
  }

  /* Scope — one column, and the desktop-only 54px inset gives way to the page
     gutter, exactly as the home gallery and the services rows do. The rule over
     each item now separates stacked blocks rather than heading a column, so the
     row gap tightens to keep rule and heading reading as one unit. */
  .scope {
    margin-top: 14vw;
    padding: 0 var(--gutter);
  }

  .scope-title {
    margin-bottom: 8vw;
    font-size: 2rem;
  }

  .scope-grid {
    grid-template-columns: minmax(0, 1fr);
    gap: 2.6rem;
  }

  .scope-index {
    margin-bottom: 0.9rem;
    font-size: 0.9rem;
  }

  .scope-name {
    font-size: 1.35rem;
  }

  .scope-body {
    font-size: 1.1rem;
  }

  /* Marquee — the clamp already floors the names at 3rem, which is the size
     that still reads as a band on a phone; only the air around it comes down. */
  .marquee {
    margin-top: 16vw;
    margin-bottom: 14vw;
  }

  .marquee-caption {
    margin-bottom: 1.8rem;
    font-size: 0.95rem;
  }

  .marquee-group {
    --marquee-gap: 1.6rem;
    font-size: clamp(2rem, 8vw, 3.25rem);
  }
}

@media (max-width: 480px) {
  .intro-heading,
  .project-lead {
    font-size: 1.7rem;
  }
}


/* ===========================================================================
   CIRCLE K / EV — THE STUDIO HERO  (circle-k-ev-hero.html)

   A full-viewport product shot with nothing photographic in it: the field is
   a stack of gradients, the device is CSS geometry inside one preserve-3d
   group, the hand is an SVG silhouette, and the interface under the glass is
   the Figma frame ("ChargeNow: Available chargers", 375×812) exported with
   its text OUTLINED and inlined as vector. There is no screenshot anywhere in
   the composition, which is the whole point: the UI cannot go soft, because
   it is never rasterised above the size it is displayed at.

   ── The unit ──────────────────────────────────────────────────────────────
   Every length below is written in DEVICE POINTS, the same space the Figma
   frame is drawn in, multiplied by --u (the on-screen size of one point).
   So `calc(46 * var(--u))` really is 46pt of corner radius, and changing --u
   alone rescales the entire rig — device, hand, shadow and glass together —
   with no transform: scale() anywhere in the chain. That matters. A scale()
   on the device would make the browser rasterise the interface at one size
   and paint it at another; laying it out at its true size instead means the
   vector is rasterised once, at exactly the device pixels it occupies.

   The rig is 700×1000pt with the 385×822pt device centred in it. The hand
   SVGs share that viewBox, so a hand coordinate and a device coordinate mean
   the same thing and the two stay registered at every viewport size.
   =========================================================================== */

.ck-stage {
  /* ONE DEVICE POINT, ON SCREEN.
     Driven off the viewport height, because the device's height is what has
     to fit: rolled −11°, a 385×822pt body occupies 880.5pt of vertical space
     (385·sin11 + 822·cos11), so dividing the height budget by 880.5 makes the
     rolled body exactly that tall. The −104px is the air the composition
     keeps above the top corner (the logo's band) and below the bottom one.

     The full screen has to stay readable down to the payment bar, so the
     device is sized to FIT the viewport rather than bleed past it — the
     oversized, immersive read comes from the hand, which does bleed off
     three edges. */
  --u: calc((100svh - 104px) / 880.5);

  /* Placement of the rig's centre, relative to the centre of the viewport.
     --pl-y is a plain pixel nudge because the −104px above already centres
     the body: it slides the whole composition, nothing more. */
  --pl-x: -11vw;
  --pl-y: 8px;
  --pl-r: 0deg;

  position: relative;
  min-height: 100svh;
  overflow: hidden;      /* the composition is meant to run off every edge */
  isolation: isolate;    /* keeps the grain's blend inside the hero */
  background: #c6d0d8;   /* under the field, so there is never a white flash */
}

/* ── The field: a soft studio cove, lit from above and slightly left ─────── */
.ck-stage__field {
  position: absolute;
  inset: 0;
  background:
    /* corner fall-off — this is what stops it reading as a flat CSS gradient */
    radial-gradient(74% 58% at 46% 36%, rgba(17, 30, 44, 0) 34%, rgba(17, 30, 44, .22) 72%, rgba(17, 30, 44, .46) 100%),
    linear-gradient(177deg, #e4ebf0 0%, #cfd9e0 26%, #b0bfca 62%, #8a9aa8 100%);
}

/* ── The key light: the bloom the device is standing in front of ─────────── */
.ck-stage__key {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(38% 44% at 41% 27%, rgba(255, 255, 255, .92) 0%, rgba(255, 255, 255, .40) 38%, rgba(255, 255, 255, 0) 70%),
    radial-gradient(34% 30% at 66% 74%, rgba(150, 174, 195, .40) 0%, rgba(150, 174, 195, 0) 72%);
}

/* ── The rig ─────────────────────────────────────────────────────────────── */
.ck-stage__plate {
  position: absolute;
  left: 50%;
  top: 50%;
  width: calc(700 * var(--u));
  height: calc(1000 * var(--u));
  transform: translate(-50%, -50%) translate(var(--pl-x), var(--pl-y)) rotate(var(--pl-r));
  pointer-events: none;
}

/* A separate box so the entrance (and the pointer parallax) never has to share
   a transform property with the placement above. */
.ck-rig {
  position: absolute;
  inset: 0;
  transform: translate3d(var(--rig-px, 0px), var(--rig-py, 0px), 0);
  animation: ck-settle 1.15s var(--ease-out) backwards;
}

@keyframes ck-settle {
  from { opacity: 0; transform: translate3d(0, calc(26 * var(--u)), 0) scale(1.022); }
  to   { opacity: 1; }
}

/* ── The shadow the device throws onto the cove ──────────────────────────── */
.ck-cast {
  position: absolute;
  left: calc(150 * var(--u));
  top: calc(150 * var(--u));
  width: calc(470 * var(--u));
  height: calc(900 * var(--u));
  border-radius: 50%;
  background: radial-gradient(50% 50% at 50% 50%,
    rgba(28, 43, 58, .46) 0%,
    rgba(28, 43, 58, .20) 52%,
    rgba(28, 43, 58, 0) 76%);
  filter: blur(calc(26 * var(--u)));
  transform: rotate(-11deg) translate(calc(34 * var(--u)), calc(26 * var(--u)));
}

/* ── The device ──────────────────────────────────────────────────────────── */
.ck-device {
  position: absolute;
  left: calc(157.5 * var(--u));   /* (700 − 385) / 2 */
  top: calc(89 * var(--u));       /* (1000 − 822) / 2 */
  width: calc(385 * var(--u));    /* 375pt screen + 5pt of bezel each side */
  height: calc(822 * var(--u));   /* 812pt screen + 5pt of bezel each end */
  transform-style: preserve-3d;
  /* Yawed left-side-on, pitched a few degrees back at the top, and rolled so
     the body runs top-left to bottom-right. Fixed at every breakpoint — the
     hand is drawn to THIS pose, and only the camera (--pl-r) ever rolls. */
  transform:
    perspective(calc(2500 * var(--u)))
    rotateX(6deg)
    rotateY(13.5deg)
    rotateZ(-11deg);
}

/* The left edge, hinged back from the front face. 46pt of titanium — the same
   ratio a 64.2mm-wide phone has at 7.65mm thick. */
/* The slab spans ONLY the straight run of the left edge — from the bottom of
   the top corner arc to the top of the bottom one. A full-height slab hinged
   on the front face's left edge overhangs both rounded corners, and because
   it sits at a different z it is not covered by anything: it shows as a bright
   splinter past the top-left corner. Insetting it by the corner radius is the
   fix; the mask then feathers the last few points so the ends do not butt
   against the curve. */
.ck-device__side {
  position: absolute;
  left: calc(-46 * var(--u));
  top: calc(51 * var(--u));
  width: calc(46 * var(--u));
  height: calc(100% - 102 * var(--u));
  transform-origin: right center;
  transform: rotateY(-90deg);
  /* local right = the front chamfer, local left = the back shell */
  background: linear-gradient(90deg,
    #04060a 0%, #0d1116 18%, #1c222a 46%, #333b45 76%, #191f26 92%, #05080b 100%);
  -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 calc(26 * var(--u)), #000 calc(100% - 26 * var(--u)), transparent 100%);
          mask-image: linear-gradient(to bottom, transparent 0, #000 calc(26 * var(--u)), #000 calc(100% - 26 * var(--u)), transparent 100%);
}

.ck-btn {
  position: absolute;
  left: calc(10 * var(--u));
  width: calc(27 * var(--u));
  border-radius: calc(3 * var(--u));
  background: linear-gradient(90deg, #12171d 0%, #39424c 55%, #10151a 100%);
  box-shadow:
    0 0 0 1px rgba(0, 0, 0, .5),
    inset 0 calc(1 * var(--u)) 0 rgba(255, 255, 255, .10);
}

.ck-btn--action { top: calc(81 * var(--u));  height: calc(31 * var(--u)); }
.ck-btn--volup  { top: calc(145 * var(--u)); height: calc(57 * var(--u)); }
.ck-btn--voldn  { top: calc(217 * var(--u)); height: calc(57 * var(--u)); }

/* The front face: brushed black titanium, lit from the upper left. */
.ck-device__front {
  position: absolute;
  inset: 0;
  border-radius: calc(51 * var(--u));
  background: linear-gradient(102deg,
    #454d57 0%, #1b2027 6%, #0c1014 28%, #080b0e 54%, #0e1216 80%, #262d35 95%, #4a525c 100%);
  box-shadow:
    /* contact darkening where the body meets the cove */
    calc(16 * var(--u)) calc(22 * var(--u)) calc(46 * var(--u)) rgba(20, 33, 46, .40),
    calc(4 * var(--u)) calc(6 * var(--u)) calc(14 * var(--u)) rgba(20, 33, 46, .34),
    /* the polished band the light rides along the top-left edge */
    inset 0 0 0 1px rgba(255, 255, 255, .07),
    inset calc(2 * var(--u)) calc(3 * var(--u)) calc(5 * var(--u)) rgba(255, 255, 255, .10),
    inset calc(-2 * var(--u)) calc(-3 * var(--u)) calc(6 * var(--u)) rgba(255, 255, 255, .05);
}

/* The polished chamfer: a gradient ring drawn between the outer edge and the
   bezel, brightest where the key light and the cove's bounce catch it. */
.ck-device__chamfer {
  position: absolute;
  inset: 0;
  border-radius: inherit;
  padding: calc(2.2 * var(--u));
  background: linear-gradient(126deg,
    rgba(255, 255, 255, .60) 0%,
    rgba(255, 255, 255, .13) 15%,
    rgba(255, 255, 255, .02) 33%,
    rgba(255, 255, 255, .02) 58%,
    rgba(255, 255, 255, .17) 79%,
    rgba(255, 255, 255, .52) 100%);
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
}

/* ── The display ─────────────────────────────────────────────────────────── */
.ck-device__screen {
  position: absolute;
  inset: calc(5 * var(--u));
  border-radius: calc(46 * var(--u));
  overflow: hidden;
  background: #000;
  /* Deliberately bare. No filter, no blur, no backdrop-filter, no opacity —
     anything here would force the interface through an offscreen raster. */
}

.ck-screen {
  display: block;
  width: 100%;
  height: 100%;
}

.ck-device__island {
  position: absolute;
  top: calc(11 * var(--u));
  left: 50%;
  width: calc(126 * var(--u));
  height: calc(37 * var(--u));
  margin-left: calc(-63 * var(--u));
  border-radius: calc(18.5 * var(--u));
  background: #000;
  box-shadow:
    inset 0 0 0 1px rgba(255, 255, 255, .05),
    inset 0 calc(1 * var(--u)) calc(2 * var(--u)) rgba(255, 255, 255, .045);
}

/* the lens, and the one glint that tells you the island is cut glass */
.ck-device__island::after {
  content: "";
  position: absolute;
  right: calc(9 * var(--u));
  top: 50%;
  width: calc(15 * var(--u));
  height: calc(15 * var(--u));
  margin-top: calc(-7.5 * var(--u));
  border-radius: 50%;
  background:
    radial-gradient(38% 38% at 32% 28%, rgba(150, 190, 225, .34), rgba(150, 190, 225, 0) 70%),
    radial-gradient(60% 60% at 50% 50%, #0b0f14, #000 74%);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .05);
}

/* ── The glass over the display ──────────────────────────────────────────── */
/* Everything here is capped at 7.5% white on purpose: enough to read as a
   sheet of glass, never enough to lift the interface's blacks. */
.ck-device__glass {
  position: absolute;
  inset: calc(5 * var(--u));
  border-radius: calc(46 * var(--u));
  background:
    radial-gradient(70% 32% at 17% 11%, rgba(255, 255, 255, .075) 0%, rgba(255, 255, 255, 0) 68%),
    linear-gradient(107deg,
      rgba(255, 255, 255, .052) 0%,
      rgba(255, 255, 255, .012) 17%,
      rgba(255, 255, 255, 0) 34%,
      rgba(255, 255, 255, 0) 67%,
      rgba(255, 255, 255, .018) 85%,
      rgba(255, 255, 255, .048) 100%);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .06);
}

/* ── The hand ────────────────────────────────────────────────────────────── */
.ck-hand {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  overflow: visible;
}

/* ── Film grain. Kills the banding a gradient this large would otherwise
      show on an 8-bit panel, and is most of what sells "photographed". ──── */
.ck-stage__grain {
  position: absolute;
  inset: 0;
  opacity: .05;
  mix-blend-mode: overlay;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='200' height='200' filter='url(%23n)'/%3E%3C/svg%3E");
  background-size: 200px 200px;
}

/* Bottom-centre would land on the body. The composition already runs
   left-of-centre, so the cue goes where the field is genuinely empty. */
.ck-stage .scroll-cue {
  left: auto;
  right: 5vw;
  transform: none;
  z-index: 3;
  animation-name: bob-fixed;
}

@keyframes bob-fixed {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(6px); }
}

@media (prefers-reduced-motion: reduce) {
  .ck-rig { animation: none; }
}

/* Depth of field. The device is the focal plane; everything the hand does is
   either side of it, so the blur is what places each layer in space — the
   grip is nearly sharp, the knuckles are soft, and the foreground is bokeh.
   All of it scales with --u, so the defocus is the same at every viewport. */
.ck-hand__fingers { filter: blur(calc(4 * var(--u))); }
.ck-hand__palm    { filter: blur(calc(9 * var(--u))); }
.ck-hand__thumb   { filter: blur(calc(3.2 * var(--u))); }
.ck-hand__crease  { filter: blur(calc(9 * var(--u))); }
.ck-hand__contact { filter: blur(calc(9 * var(--u))); }
.ck-hand__fore    { filter: blur(calc(20 * var(--u))); }


/* ===========================================================================
   DESIGN PREFERENCES (experiment) — assets/js/prefs.js
   A gear at the end of the footer line opens a small panel for switching
   design options site-wide while they're being compared. Each choice is a
   class on <html>, set before first paint. Remove this block, prefs.js and
   its <script> tags once the options are settled.
   =========================================================================== */
@font-face {
  font-family: "Neue Montreal";
  src: url("assets/fonts/neue-montreal/PPNeueMontreal-Light.otf") format("opentype");
  font-weight: 300;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Neue Montreal";
  src: url("assets/fonts/neue-montreal/PPNeueMontreal-Regular.otf") format("opentype");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* ---- h1: Neue Montreal Regular — the home intro + every case-study title.
   Same spec as home-alt / intel-alt (60px / 68px / -0.02em at 1700). The
   doubled class out-ranks the mobile size overrides, which were set for
   Futura Bold; the clamp's 30px floor takes over from them. */
html.pref-h1-montreal.pref-h1-montreal .intro-heading,
html.pref-h1-montreal.pref-h1-montreal .project-lead,
html.pref-h1-montreal.pref-h1-montreal .page-lead {   /* services + nexus + join-us hero */
  font-family: "Neue Montreal", "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-weight: 300;                       /* back to Light (2026-10-30, after a round of Regular) */
  font-size: clamp(40px, 5.2vw, 75px);    /* same as the home manifesto */
  line-height: 1.03;
  letter-spacing: -0.0175em;
}
html.pref-h1-montreal .project-lead {
  max-width: 1100px;
}

/* The same switch carries the case-study section titles and the prev/next
   names under the last block (spec from intel-alt). The nav keeps its own
   fluid size; only face, weight and tracking change. */
html.pref-h1-montreal.pref-h1-montreal .project-block-title,
html.pref-h1-montreal.pref-h1-montreal .project-band-title,      /* quote / dark bands */
html.pref-h1-montreal.pref-h1-montreal .project-context-title,   /* "Circle K in Norway" */
html.pref-h1-montreal.pref-h1-montreal .project-spread-title,
html.pref-h1-montreal.pref-h1-montreal .services-cta-lead,       /* "Think we'd get along?" CTA */
html.pref-h1-montreal.pref-h1-montreal .project-nav-title,
html.pref-h1-montreal.pref-h1-montreal .role-title,             /* join-us role names */
html.pref-h1-montreal.pref-h1-montreal .service-name,           /* services accordion rows */
html.pref-h1-montreal.pref-h1-montreal .contact-list {           /* contact address block */
  font-family: "Neue Montreal", "Helvetica Neue", Helvetica, Arial, sans-serif;
  font-weight: 300;
}
html.pref-h1-montreal.pref-h1-montreal .project-block-title,
html.pref-h1-montreal.pref-h1-montreal .project-band-title,
html.pref-h1-montreal.pref-h1-montreal .project-context-title,
html.pref-h1-montreal.pref-h1-montreal .project-spread-title,
html.pref-h1-montreal.pref-h1-montreal .services-cta-lead {
  font-size: clamp(30px, 3.5vw, 60px);
  line-height: 1.1;
  letter-spacing: -0.02em;
}
/* Contact address: a step under the section titles */
html.pref-h1-montreal.pref-h1-montreal .contact-list {
  font-size: clamp(26px, 2.9vw, 50px);
  line-height: 1.1;
  letter-spacing: -0.02em;
}
html.pref-h1-montreal.pref-h1-montreal .project-nav-title {
  letter-spacing: -0.01em;
}
html.pref-h1-montreal.pref-h1-montreal .page-lead a {
  text-decoration-thickness: 1px;   /* 2px was set for Futura Bold */
}

/* Phones: scale with the width like the home manifesto (10.5vw ≈ 41px on a
   390 phone) instead of sitting on the clamp floors; h2 keeps the desktop
   60:75 ratio to the h1. */
@media (max-width: 820px) {
  html.pref-h1-montreal.pref-h1-montreal .project-lead,
  html.pref-h1-montreal.pref-h1-montreal .page-lead {
    font-size: clamp(34px, 10.5vw, 72px);
    line-height: 1.04;
  }
  html.pref-h1-montreal.pref-h1-montreal .project-block-title,
  html.pref-h1-montreal.pref-h1-montreal .project-band-title,
  html.pref-h1-montreal.pref-h1-montreal .project-context-title,
  html.pref-h1-montreal.pref-h1-montreal .project-spread-title,
  html.pref-h1-montreal.pref-h1-montreal .services-cta-lead {
    font-size: clamp(28px, 8.4vw, 56px);
    line-height: 1.08;
  }
  html.pref-h1-montreal.pref-h1-montreal .contact-list {
    font-size: clamp(24px, 7vw, 46px);
    line-height: 1.08;
  }
  /* The mobile "Previous" / "Next" are ::after content, not .project-nav-title */
  html.pref-h1-montreal.pref-h1-montreal .project-nav-link::after {
    font-family: "Neue Montreal", "Helvetica Neue", Helvetica, Arial, sans-serif;
    font-weight: 300;
    letter-spacing: -0.01em;
  }
}

/* ---- The switch ---- */
.prefs {
  position: relative;
  display: inline-flex;
  align-self: center;
}

.prefs-toggle {
  display: inline-flex;
  padding: 4px;
  margin: 0;
  border: 0;
  background: none;
  color: inherit;
  opacity: 0.3;      /* deliberately easy to miss */
  cursor: pointer;
  transition: opacity var(--dur-snap) ease;
}

.prefs-toggle:hover,
.prefs-toggle[aria-expanded="true"] {
  opacity: 1;
}

.prefs-panel {
  position: absolute;
  right: -8px;
  bottom: calc(100% + 10px);
  z-index: 50;
  min-width: 220px;
  padding: 14px;
  background: var(--bg);
  color: var(--fg);
  border: 1px solid rgba(0, 0, 0, 0.12);
  border-radius: 10px;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.12);
  text-align: left;
  font-size: 0.875rem;
  line-height: 1.3;
}

.prefs-panel[hidden] {
  display: none;
}

.prefs-group + .prefs-group {
  margin-top: 14px;
}

.prefs-label {
  margin-bottom: 8px;
  font-weight: 500;
}

.prefs-option {
  display: block;
  width: 100%;
  margin: 0;
  padding: 7px 10px;
  border: 0;
  border-radius: 6px;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  white-space: nowrap;
  cursor: pointer;
}

.prefs-option:hover {
  background: var(--surface-thumb);
}

.prefs-option[aria-checked="true"] {
  background: var(--fg);
  color: var(--bg);
}

.prefs-link {
  box-sizing: border-box;
  text-decoration: none;
}

.prefs-link::after {
  content: " \2197";   /* ↗ */
  opacity: 0.5;
}

/* Phones: the gear can sit anywhere on a wrapped footer line, so the panel
   docks to the bottom of the screen instead of hanging off the icon. */
@media (max-width: 600px) {
  .prefs-panel {
    position: fixed;
    left: 16px;
    right: 16px;
    bottom: 16px;
    min-width: 0;
  }
}
