/* ============================================================
   hdw™ — styles

   TWO FONTS. THAT IS THE WHOLE SET.

     Delight        everything you read as words. Headings, body, UI,
                    buttons, forms, client names. One family, one
                    weight. Hierarchy comes from size, never from
                    swapping family or weight.

     Basis Mono     labels only. The small grey words that name a thing
                    rather than say it: DATE, DISCIPLINES, IN SHORT,
                    VIEW 5, 01 / 02 / 03. Always uppercase, always
                    tracked out, always small. If it is a sentence it
                    is not a label, so it stays in Delight.

   There is no third font. No serif. Anything reaching for --serif is a
   bug: the variable no longer exists.

   Everything below is driven from :root. Change it there once and the
   whole site follows, which is the point — micro differences between
   pages come from pages inventing their own numbers.
============================================================ */

@font-face {
  font-family: 'Delight';
  /* woff2 only. The .otf was listed as a fallback for a browser that cannot
     read woff2, and there is no such browser left. It stays on disk because
     scripts/make-work-og.py and scripts/make-og-images.py need a real font
     file to draw the share cards with, but nothing in a browser should ever
     be offered it. */
  src: url('../fonts/Delight-Regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* Basis Grotesque Mono. Digits line up in a column, which is the reason
   it is here: dates, counts and IDs stop dancing between rows. */
@font-face {
  font-family: 'Basis Mono';
  src: url('../fonts/Basis-Mono-Regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}


:root {
  --sans: 'Delight', system-ui, -apple-system, sans-serif;              /* everything */
  --mono: 'Basis Mono', ui-monospace, SFMono-Regular, Menlo, monospace; /* labels only */

  --black: #0a0a0a;       /* text */
  --white: #f7f7f4;       /* surfaces / bg: a hair off pure white, so the page reads as paper rather than screen */
  --grey: #6a6a6a;        /* muted captions / meta          5.41:1 on white */
  /* Was #8a8a8a, which is 3.45:1 and under the 4.5 a small word needs. It is
     not decorative: it carries every form placeholder and the DATE / CLIENT
     labels down the side of every case study, at 9.5px. #767676 is 4.54:1 and
     is the lightest grey that clears it. */
  --grey-soft: #767676;   /* small grey words               4.54:1 on white */
  --line: #e6e6e6;        /* hairlines */
  --card: #ecece8;        /* image placeholder bg, one step under the ground */
  /* Was #d4d4d4: 1.48:1, which is a boundary you cannot see. The border is the
     only thing telling you where an input begins, so it has to clear 3:1 like
     any other control edge. #949494 is 3.03:1 and still reads as a hairline. */
  --field: #949494;       /* form input border              3.03:1 on white */
  --accent: #ff2e00;      /* hdw orange, on a surface */
  /* The orange is 3.72:1 on white, which is fine for a block of colour and not
     fine for a word set in it. So there are two: --accent paints things,
     --accent-ink writes them. Same orange to the eye, and the active nav item
     stops being the least readable word in the header. */
  --accent-ink: #cc2500;  /* orange as TEXT                 5.48:1 on white */

  --pad: 20px;            /* page side padding */
  --gap: 20px;            /* base grid gutter */

  /* ---- type scale: four sizes, and there is no fifth ----
     This used to be nine steps (h1 through h6, body, small, xs) and nine steps
     is not a scale, it is a menu. Sizes two pixels apart cannot be told apart
     on a page, so the difference stopped meaning anything and every new rule
     had to guess which of the nine it wanted. Four steps that are obviously
     different from each other do the same job and can be held in the head.

     Display for the name of the page. Heading for the name of a section.
     Body for everything a person reads. Small for fine print. That is the
     whole hierarchy, and it is size doing the work, the way this site has
     always meant it to. The rendered spec sheet is in docs/type-spec.html.

     The old nine collapsed into these: h1 became display, h2/h3/h4 all became
     heading, h5/h6 became body, xs became small. Nothing new was invented.
     Do not add a step back. If something needs to sit between two of these,
     the answer is spacing or colour, not another size. */
  --t-display: 39px;      /* page titles, cover names */
  --t-heading: 26px;      /* section and block titles */
  --t-body: 14px;         /* everything read as text */
  --t-small: 11.5px;      /* fine print */

  /* Delight sets tight at reading size. A hair of tracking is the difference
     between a paragraph that reads and one that has to be worked at, and it is
     small enough that nothing moves visibly. Display and heading do not take
     it: type opens up on its own as it gets bigger, which is why the cover
     name pulls the other way. */
  --body-track: .01em;

  /* Mono is not on the scale above, and that is the point. Basis Mono renders
     optically larger than Delight at the same pixel size, so a label set at the
     fine-print size reads a step louder than the fine print beside it. Half a
     pixel is what closes that gap. Labels take these, never --t-small. */
  --t-label: 10.5px;      /* every mono label */
  --t-label-sm: 9.5px;    /* the few that sit under one */

  /* ---- the numbers every page has to agree on ----
     These exist because "a little bit off" is always one page picking
     its own value. Nothing should hardcode a top padding again. */
  --header-h: 71px;                        /* real height, JS overwrites on load */
  --page-top: clamp(112px, 13vw, 168px);   /* header to the first word, every page */
  --page-lead-gap: clamp(26px, 3vw, 44px); /* title block to the first section */
  --section-gap: clamp(56px, 7vw, 96px);   /* between sections inside a page */
  --section-gap-lg: clamp(90px, 12vw, 170px); /* before the footer */
  /* Mono labels used to be tracked out to .08em, the usual answer for small
     uppercase. Basis Mono is already a fixed pitch: every character is walking
     to the same width before anything is added, so the extra space came out as
     gaps between letters rather than as air around a word. Zero. */
  --label-track: 0;                        /* mono label tracking */
}

/* ---------- dark mode ---------- */
[data-theme="dark"] {
  --black: #f2f1ec;       /* text becomes light */
  --white: #0d0d0c;       /* surfaces become dark */
  --grey: #9a988f;        /* 6.72:1 */
  --grey-soft: #8a8880;   /* was #6e6c66, 3.70:1. now 5.48:1 */
  --line: #2a2a26;
  --card: #1a1a17;
  --field: #666660;       /* was #3a3a35, 1.70:1. now 3.37:1 */
  /* The orange is already 5.22:1 against the dark surface, so ink and paint
     are the same colour here and the token exists only so nothing has to ask
     which theme it is in. */
  --accent-ink: #ff2e00;
}
/* type scale (phone) — the same four steps, moved down as one */
@media (max-width: 900px) {
  :root {
    --t-display: 28px;
    --t-heading: 22px;
    --t-body: 13px;
    --t-small: 11px;
    --t-label: 10px;
    --t-label-sm: 9.5px;
  }
}

/* ---------- reset ---------- */
* { margin: 0; padding: 0; box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; overflow-x: clip; scroll-behavior: smooth; }   /* kills stray sideways scroll, keeps sticky header working */
body { -webkit-overflow-scrolling: touch; }   /* momentum scroll on iOS */
body {
  font-family: var(--sans);
  color: var(--black);
  background: var(--white);
  font-size: var(--t-body);
  letter-spacing: var(--body-track);
  line-height: 1.45;
  font-synthesis: none;      /* no synthetic bold — one weight only */
  -webkit-font-smoothing: antialiased;
}
a { transition: opacity .2s ease; }
a:hover { opacity: .6; }      /* links lower opacity on hover */
::selection { background: var(--black); color: var(--white); }
/* height:auto matters more than it looks. Every img now carries its real width
   and height so the browser can reserve the right space before the file lands
   and the page stops jumping. Those attributes would also freeze the height at
   full size once max-width shrinks the width, which squashes the picture. This
   hands the height back to the ratio. */
img, video { display: block; max-width: 100%; height: auto; }
/* Client work is not downloadable. No drag to the desktop, no long-press
   sheet on a phone, no download item in the native video menu. The rest of
   the guard is in js/main.js, and the honest note about how far it goes is
   written there. */
img, video, picture, .hdw-player {
  -webkit-user-drag: none;
  -webkit-touch-callout: none;
  user-select: none;
  -webkit-user-select: none;
}
video::-webkit-media-controls-download-button { display: none; }
video::-internal-media-controls-download-button { display: none; }
video::-webkit-media-controls-enclosure { overflow: hidden; }
a { color: inherit; text-decoration: none; }
ul { list-style: none; }
button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; }
/* `font: inherit` is a shorthand, and a shorthand resets every part it does not
   name. letter-spacing is not one of its parts, so a button, an input and a
   select were the three places on the site where the body's tracking silently
   stopped and the words came out set a hair tighter than the words beside
   them. Named after the shorthand, because it has to win against it. */
button, input, textarea, select { letter-spacing: inherit; }

/* ---------- layout helpers ---------- */
.container { width: 100%; padding-inline: var(--pad); }

/* ---------- full bleed, without the scrollbar bug ----------

   THIS is why the case study title never lined up with anything.

   The old way of going full width was `width: 100vw` plus
   `margin-left: calc(50% - 50vw)`. 100vw counts the scrollbar; the element's
   own 50% does not. On a 1280px window with a 15px scrollbar that pulls the
   section 7.5px left of everything else, so a cover title padded by 20px
   landed at 12.5px while the brandmark above it and the sidebar below it
   both sat at 20px. Off by seven and a half pixels, on every case study,
   invisible until you look for it.

   Every one of these sections is already a direct child of a full width
   <main>, so none of them ever needed the trick. Width 100% and nothing
   else. One class, used by all three, so this cannot be reinvented wrong
   on the next full-bleed section. */
.u-bleed, .capabilities, .reel, .cs-hero {
  width: 100%;
  margin-left: 0;
  margin-right: 0;
}

/* ---------- the grid ----------

   Twelve fluid columns, full width, no cap, gutter is the --gap the rest of
   the site already spaces on. Twelve because every split this site uses has
   an exact home in it: halves are 6+6, thirds 4+4+4, quarters 3+3+3+3, and
   the label-and-content pairs are 5+7.

   Two widths, not nine. 600px is the phone, 900px is tablet and below. Every
   section used to pick its own collapse point, which is why two sections
   could disagree about how many columns to be at the same window width.

   The utilities below are unused so far. Sections move onto them one at a
   time rather than all at once, so each move can be checked on its own. */
:root { --grid-cols: 12; --col-gap: clamp(32px, 5vw, 80px); }
.grid {
  display: grid;
  grid-template-columns: repeat(var(--grid-cols), minmax(0, 1fr));
  gap: var(--col-gap);
}
.col-1  { grid-column: span 1; }
.col-2  { grid-column: span 2; }
.col-3  { grid-column: span 3; }
.col-4  { grid-column: span 4; }
.col-5  { grid-column: span 5; }
.col-6  { grid-column: span 6; }
.col-7  { grid-column: span 7; }
.col-8  { grid-column: span 8; }
.col-9  { grid-column: span 9; }
.col-10 { grid-column: span 10; }
.col-11 { grid-column: span 11; }
.col-12 { grid-column: span 12; }
@media (max-width: 900px) { .grid > [class*="col-"] { grid-column: span 12; } }

/* shared display type */
.section-title {
  font-family: var(--sans);
  font-weight: 400;
  font-size: var(--t-heading);
  line-height: 1;
  letter-spacing: 0;
}
.section-head {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
}

/* small underlined link (About us / View All) */
.link-underline {
  font-family: var(--sans);
  font-size: var(--t-body);
  border-bottom: 1px solid currentColor;
  padding-bottom: 2px;
  transition: opacity .2s;
}
.link-underline:hover { opacity: .55; }

/* ============================================================
   LABELS  —  the one rule for every small grey word on the site

   A label names a thing. DATE. DISCIPLINES. IN SHORT. VIEW 5. 01.
   It is Basis Mono, uppercase, tracked out, grey, small. There is no
   second way of setting one, and no page gets to invent its own.

   A hint, a description, a note or an answer is a sentence, not a
   label. Sentences stay in Delight and stay in sentence case, because
   a paragraph in mono caps is unreadable.

   .u-label is the class to reach for on new markup. Everything the
   site already ships is folded into the selector list below so the
   two can never drift apart.
============================================================ */
.u-label,
.page-kicker,
.lc-label,
.cap-label,
.cs-side-row .k,
.cs-side-next .k,
.cs-chapter-k,
.cs-summary-k,
.cs-videos-k,
.cs-posts-k,
.cs-next .lbl,
.contact-info dt,
.cs-block-title em,
.cs-disc em,
.svc-row-n,
.svc-step .n,
.form-group-head .n,
.work-view-toggle,
.update-meta,
.article-meta,
.bd-progress,
.footer-bottom,
.cursor-tag,
.reel-label {
  font-family: var(--mono);
  text-transform: uppercase;
  letter-spacing: var(--label-track);
  font-size: var(--t-label);
  line-height: 1.3;
  /* Two of these labels are h2 now (the logo cloud and Capabilities carry the
     homepage outline), and a heading arrives wanting to be bold. There is no
     bold cut of Basis Mono and font-synthesis is off, but saying 400 here means
     the label looks the same no matter what tag is carrying it. */
  font-weight: 400;
  color: var(--grey);
  font-feature-settings: 'tnum' 1;   /* digits keep one width, so columns line up */
}
/* Mono runs optically larger than Delight at the same px, so the two
   sizes it is allowed are set here rather than per component. They come
   from --t-label, not the reading scale, for the reason written at :root. */
.u-label--sm { font-size: var(--t-label-sm); }
.footer-bottom span,
.cursor-tag { font-size: var(--t-label); }

/* ============================================================
   SCROLL ANIMATIONS
   Elements tagged .reveal / .reveal-y / .reveal-stagger start
   hidden and animate in when they scroll into view (see main.js).
   Initial hidden state only applies once JS marks <html> ready,
   so the page still works fully if JS is disabled.
============================================================ */
@media (prefers-reduced-motion: no-preference) {
  .anim-ready .reveal,
  .anim-ready .reveal-stagger > * {
    opacity: 0;
    transform: translateY(26px);
  }
  .anim-ready .reveal-y { transform: translateY(26px); }

  .reveal, .reveal-y, .reveal-stagger > * {
    transition: opacity .7s ease, transform .7s cubic-bezier(.22,.61,.36,1);
  }
  .reveal.is-visible,
  .reveal-stagger.is-visible > * { opacity: 1; transform: none; }
  .reveal-y.is-visible { transform: none; }

  /* hero intro animation on load */
  .anim-ready .hero-title,
  .anim-ready .hero-thumbs { opacity: 0; transform: translateY(28px); }
  .hero-loaded .hero-title,
  .hero-loaded .hero-thumbs {
    opacity: 1; transform: none;
    transition: opacity .9s ease .15s, transform .9s cubic-bezier(.22,.61,.36,1) .15s;
  }
  .hero-loaded .hero-thumbs { transition-delay: .35s; }
}

/* ============================================================
   HEADER
============================================================ */
.site-header {
  position: fixed;
  top: 0; left: 0;
  width: 100%;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 20px var(--pad);
  color: #fff;
  background: transparent;
  border-bottom: 1px solid transparent;
  transition: background .3s ease, border-color .3s ease, color .3s ease;
}
.site-header.scrolled {
  background: var(--white);
  color: var(--black);
  border-bottom-color: var(--line);
}
.logo {
  font-family: var(--sans);
  font-size: var(--t-body);
  letter-spacing: var(--body-track);
  display: inline-flex;
  align-items: center;
}
/* real hdw brandmark (inherits text color, so it flips white/black per context) */
.brandmark { height: 35px; width: auto; display: block; color: inherit; }
.footer-top .brandmark { height: 28px; }
.main-nav {
  position: absolute;
  right: var(--pad);        /* pinned to the right edge */
  display: flex;
  gap: 26px;                /* spacing between nav items */
}
.main-nav a {
  font-family: var(--sans);
  font-size: var(--t-body);
  letter-spacing: var(--body-track);
  transition: opacity .2s;
}
.main-nav a:hover { opacity: .6; }
/* active page: the header's own ink, underlined. The word used to go orange
   as well, which made the header the one place on the site where the accent
   was a label rather than an accent. The line says it on its own. */
.page-nav a.is-active, .main-nav a.is-active { color: inherit; }
.page-nav a.is-active::after, .main-nav a.is-active::after { background: currentColor; transform: scaleX(1); }
.menu-toggle { display: none; font-family: var(--sans); font-size: var(--t-body); }

/* ============================================================
   HERO
============================================================ */
.hero {
  position: relative;
  height: 100vh;
  height: 100dvh;
  min-height: 100vh;
  min-height: 100dvh;
  overflow: hidden;
  background: #6f7468;
}
.hero-media, .hero-media .hero-img,
.hero-media .hero-poster, .hero-media .hero-video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* The reel, and the frame it starts on.

   Both are stacked in the same box and the poster is simply underneath, rather
   than the video's own poster attribute. That attribute is dropped the instant
   the first frame decodes, which on a slow connection means a picture, then
   nothing, then the picture again. Underneath it never flickers: the video
   fades up over the top when it can actually play, and if it never can, the
   frame is still there and the hero still reads.

   No controls anywhere in the markup, so there is no bar to hide and nothing to
   right-click. Same rule as the rest of the site. */
.hero-video {
  opacity: 0;
  transition: opacity .6s ease;
  pointer-events: none;             /* nothing to click, so nothing invites a click */
}
.hero-video.is-playing { opacity: 1; }

/* Someone who has asked their machine for less movement gets the frame, held.
   A thirty second loop behind a headline is exactly what that setting means. */
@media (prefers-reduced-motion: reduce) {
  .hero-video { display: none; }
}
.hero-img { opacity: 0; transition: opacity .6s ease; }
.hero-img.is-active { opacity: 1; }

/* The line sits low and centred so the photograph gets the whole frame and the
   words stay out of its way. Big type across the top fought the picture; this
   reads as a caption to the work rather than a banner over it. */
/* Dead centre of the frame, both axes. The headline used to sit bottom left,
   above a thumbnail switcher that is no longer there; with the reel behind it
   there is nothing else in the frame to line up with, so the middle is the only
   position that is not arbitrary. */
.hero-overlay {
  position: absolute;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 0 var(--pad);
  color: #fff;
  text-align: center;
  pointer-events: none;             /* never blocks anything underneath */
}
/* The line has to survive whatever photograph is behind it, and the covers run
   from a near-black pizzeria to a bright green swatch board. A shadow alone
   loses on the pale ones. This is the smallest amount of dark that keeps white
   text readable on all of them, and it fades out well before the middle of the
   frame so the picture is still the picture. */
/* The scrim behind the headline.

   It was a bottom gradient, because the headline used to sit at the bottom. A
   centred headline needs cover in the middle of the frame, where a bottom
   gradient has already faded to nothing, so this is flat across the whole thing
   with a little extra weight at the foot to keep the scroll cue readable.

   The number is not taste. The reel runs thirty seconds through shots measuring
   anywhere from 40 to 145 in mean brightness. At the bright end white type on
   its own came out around 3:1, the bare minimum for type this size and no
   margin at all on a picture that keeps moving. At .38 the worst case is near
   6.7:1 and the dark shots are barely touched. */
.hero-overlay::before {
  content: '';
  position: absolute;
  inset: 0;
  background:
    linear-gradient(to top, rgba(0,0,0,.30), transparent 34%),
    rgba(0,0,0,.38);
  pointer-events: none;
}
/* Only the headline. This used to name the scroll cue as well, back when both
   sat in the flex flow together and both needed lifting off the scrim. The cue
   is pinned to the bottom of the frame now, and position:relative here beat its
   own absolute by specificity: it stayed in the flow, and the flex centring
   then split the difference between the two, leaving the headline 17px high of
   the middle it was supposed to be dead centre of. */
.hero-overlay > .hero-title { position: relative; }
.hero-overlay > * { pointer-events: auto; }
/* Display size, but not cover size. A case study cover carries one or two words
   and can take 96px; this is a whole sentence, and at that size it would fill
   the screen and push the thumbs off the bottom. So it goes as large as a
   sentence can go and still break into two or three lines: max-width in em, so
   the measure holds at every size rather than the line count changing with the
   window. Tighter leading and a hair of negative tracking, the same correction
   the cover name makes, because Delight opens up at display size. */
.hero-title {
  font-family: var(--sans);
  font-weight: 400;
  font-size: clamp(28px, 4.4vw, 60px);
  line-height: 1.05;
  letter-spacing: -0.005em;
  max-width: 15em;
  text-shadow: 0 1px 24px rgba(0,0,0,.4);   /* holds up over a bright photo */
}

/* ---------- the nudge ----------
   A hairline that breathes, so a screen-filling page reads as having
   more below it without a button asking to be pressed.

   Every full-height opener on the site gets one: the home hero and
   every case study cover. .hero-scroll is the home hero's instance and
   .scroll-nudge is the shared one, so there is a single animation and
   a single set of numbers behind both. */
.hero-scroll, .scroll-nudge {
  width: 1px;
  height: 34px;
  background: currentColor;
  opacity: .55;
  transform-origin: top;
  pointer-events: none;
}
/* Pinned to the foot of the frame, not hung off the headline. The headline is
   centred now, so a nudge sitting 18px under it would read as an underline
   rather than as an invitation to scroll. Same position the covers use. */
.hero-scroll {
  position: absolute;
  left: 50%;
  bottom: clamp(26px, 4vh, 46px);
  transform: translateX(-50%);
  z-index: 2;
}
/* On a cover the words sit left and low, so the nudge is pinned to the
   bottom centre of the frame instead of hanging off the headline. */
.scroll-nudge {
  position: absolute;
  left: 50%;
  bottom: clamp(26px, 4vh, 46px);
  z-index: 2;
  margin-left: -.5px;
  color: #fff;   /* it always sits over a photograph, in either theme */
}
@media (prefers-reduced-motion: no-preference) {
  .hero-scroll, .scroll-nudge { animation: heroNudge 2.4s ease-in-out infinite; }
}
@keyframes heroNudge {
  0%, 100% { transform: scaleY(.35); opacity: .25; }
  50%      { transform: scaleY(1);   opacity: .7; }
}

.hero-thumbs {
  display: flex;
  gap: 10px;
  position: absolute;               /* image switcher pinned bottom-left */
  left: var(--pad);
  bottom: 22px;
}
.hero-thumb {
  width: 80px; height: 58px;
  overflow: hidden;
  opacity: 1;
  transition: opacity .25s, transform .25s;
  border: none;
}
.hero-thumb:hover { transform: translateY(-3px); }
.hero-thumb img { width: 100%; height: 100%; object-fit: cover; object-position: center; }
.hero-thumb.is-active { opacity: .45; }

.hero-buy { position: absolute; right: var(--pad); bottom: 22px; }

/* ============================================================
   INTRO
============================================================ */
.intro {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--gap);
  padding-block: clamp(120px, 14vw, 210px);
}
.intro-text { grid-column: 2 / 4; }
.intro-text p {
  font-family: var(--sans);
  font-size: var(--t-heading);
  line-height: 1.04;
  letter-spacing: 0;
  margin-bottom: 26px;
}

/* ============================================================
   LATEST WORK
============================================================ */
/* padding-top so the heading is not sitting on the logo cloud's bottom rule */
.work { padding-top: clamp(56px, 7vw, 96px); padding-bottom: clamp(120px, 14vw, 210px); }
.work-grid {
  margin-top: clamp(28px, 3.5vw, 48px);
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  column-gap: var(--gap);
  row-gap: 40px;
  align-items: start;
}
.work-card {
  position: relative;
  display: block;
  overflow: hidden;
  background: var(--card);
}
.work-card img, .work-card video { width: 100%; height: 100%; object-fit: cover; }
.ratio-1x1  { aspect-ratio: 1 / 1; }
.ratio-16x9 { aspect-ratio: 16 / 9; }
.ratio-4x3  { aspect-ratio: 4 / 3; }

.work-card img, .work-card video { transition: transform .6s ease; }
.work-card:hover img, .work-card:hover video { transform: scale(1.04); }

/* Readability scrim under any card with a caption laid over it. White type on
   a photograph is a coin toss without it, and the covers run from a near-black
   pizzeria to a bright green swatch board.

   Named for the one place that carries a caption over the picture rather than
   put on every .work-card, because the work index puts its names UNDER the
   picture and a scrim there would darken a photograph for no reason. */
.work-grid .work-card::after {
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 50%;
  background: linear-gradient(to top, rgba(0,0,0,.5), rgba(0,0,0,0));
  pointer-events: none;
  z-index: 1;
}
.work-caption {
  position: absolute;
  left: 18px; bottom: 16px;
  z-index: 2;
  color: #fff;
  display: flex;
  flex-direction: column;
  gap: 2px;
  text-shadow: 0 1px 12px rgba(0,0,0,.35);
}
.work-caption .t { font-family: var(--sans); font-weight: 400; font-size: var(--t-body); }
.work-caption .s { font-family: var(--sans); font-weight: 400; font-size: var(--t-body); opacity: .85; }

/* ============================================================
   CAPABILITIES (full-bleed)
============================================================ */
.capabilities {
  position: relative;
  overflow: hidden;
  color: #fff;
  padding: clamp(80px, 10vw, 150px) 0;
  min-height: 86vh;
  display: flex;
  align-items: center;
}
.cap-bg { position: absolute; inset: 0; }
.cap-img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity .7s ease;
}
.cap-img.is-active { opacity: 1; }
.cap-bg::after { content: ''; position: absolute; inset: 0; background: rgba(0,0,0,.4); }

.cap-inner { position: relative; z-index: 1; width: 100%; }
.cap-label {
  position: absolute;          /* pinned to the top-left of the section */
  top: clamp(34px, 5vw, 70px);
  left: var(--pad);
  z-index: 2;
  color: #fff;                 /* sits over the photograph, not over the page */
  opacity: .8;
}
.cap-list { width: min(560px, 92%); margin-left: auto; text-align: right; }
/* No rules between the rows. The + is the only thing saying these open, and
   one clear control beats a control plus a rule plus an underline all trying
   to say the same thing. */
.cap-item { padding: 12px 0; }
/* The capability names are h3s in the outline; the button inside is still the
   visual object and sets its own face and size, so the heading box only needs
   to stop being bold. */
.cap-item h3 { font-weight: 400; }
.cap-item:not(.is-active) { opacity: .55; transition: opacity .3s; }
.cap-item:not(.is-active):hover { opacity: 1; }
/* Its own size, not --t-display. These three sit on the same page as the hero, and
   the hero is a clamp that slides from 28px to 60px with the window. A fixed
   token cannot stay under a moving target: at 60px they matched it exactly, and
   on a phone they were larger than it. Sliding on the same axis at a lower rate
   keeps them below the headline at every width. */
.cap-head {
  font-family: var(--sans);
  font-size: clamp(26px, 3vw, 40px);
  line-height: 1.08;
  letter-spacing: 0;
  color: #fff;                 /* always light, sits over the image */
  text-align: right;
  width: auto;                 /* click target hugs the text, doesn't span the row */
  display: inline-flex;
  align-items: center;
  gap: clamp(14px, 2vw, 26px);
  flex-direction: row-reverse; /* sign sits left of the word on a right-aligned list */
  cursor: pointer;
}
/* The click cue, and the only one. A + that turns into an x. Set at a
   fraction of the heading size so it reads as a control rather than as part
   of the word, and given a fixed box so the word does not shift a pixel when
   it rotates. No underline on hover: the + already says this opens. */
.cap-head::after {
  content: '+';
  display: inline-block;
  width: 1em;
  text-align: center;
  font-size: var(--t-body);
  line-height: 1;
  opacity: .7;
  transition: transform .35s cubic-bezier(.22,.61,.36,1), opacity .2s;
}
.cap-head:hover::after { opacity: 1; }
.cap-item.is-active .cap-head::after { transform: rotate(45deg); opacity: 1; }
.cap-head:hover { text-decoration: none; }
.cap-body {
  max-height: 0;
  overflow: hidden;
  opacity: 0;
  text-align: right;
  transition: max-height .55s cubic-bezier(.22,.61,.36,1), opacity .4s ease;
}
.cap-item.is-active .cap-body { opacity: 1; }
.cap-desc {
  font-family: var(--sans);
  font-size: var(--t-body);
  line-height: 1.5;
  max-width: 44ch;
  margin: 16px 0 22px auto;
}
.cap-body .link-underline { color: #fff; }
.cap-related {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 14px;
  max-width: 640px;
  margin-bottom: 22px;
}
.cap-related a { display: block; }
.cap-related img { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; transition: opacity .2s; }
.cap-related a:hover img { opacity: .8; }
.cap-related span { font-family: var(--sans); font-size: var(--t-small); display: block; margin-top: 8px; }
.cap-body .link-underline { color: #fff; }
@media (max-width: 600px) {
  .cap-related { grid-template-columns: repeat(2, 1fr); }
}

/* ============================================================
   LATEST UPDATES (carousel)
============================================================ */
.updates { padding-block: clamp(70px, 9vw, 130px); }
.carousel-arrows { display: flex; gap: 8px; }
.arrow {
  width: 38px; height: 38px;
  border: 1px solid var(--line);
  border-radius: 0;
  font-size: var(--t-body);
  display: grid;
  place-items: center;
  transition: background .2s, color .2s;
}
.arrow:hover { opacity: .6; }

.updates-track {
  margin-top: 28px;
  display: flex;
  gap: var(--gap);
  overflow-x: auto;
  scroll-behavior: smooth;
  scroll-snap-type: x mandatory;
  scrollbar-width: none;
  padding-bottom: 4px;
}
.updates-track::-webkit-scrollbar { display: none; }
.update-card {
  flex: 0 0 400px;
  max-width: 80vw;
  scroll-snap-align: start;
}
.update-card img {
  width: 100%;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  transition: opacity .2s;
}
.update-card:hover img { opacity: .85; }
.update-meta {
  display: flex;
  justify-content: space-between;
  margin: 14px 0 8px;
}
.update-title {
  font-family: var(--sans);
  font-weight: 400;
  font-size: var(--t-body);
  line-height: 1.25;
  max-width: 30ch;
}

/* ============================================================
   CTA
============================================================ */
.cta {
  position: relative;
  padding-top: clamp(120px, 14vw, 210px);
  padding-bottom: clamp(120px, 14vw, 210px);
}
.cta-text p {
  font-family: var(--sans);
  font-size: var(--t-heading);
  line-height: 1;
  letter-spacing: 0;
  max-width: 14ch;
}
.cta-link {
  display: inline-block;
  margin-top: 4px;
  font-family: var(--sans);
  font-size: var(--t-heading);
  line-height: 1;
  letter-spacing: 0;
  border-bottom: 2px solid currentColor;
}

/* ============================================================
   FOOTER
============================================================ */
.site-footer { padding-bottom: 26px; border-top: 1px solid var(--line); padding-top: 30px; }
.footer-top {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--gap);
  align-items: start;   /* keep the brandmark level with the other columns */
}
.footer-top .logo { margin-top: -4px; }
.footer-contact p { font-size: var(--t-body); margin-bottom: 4px; }
.footer-contact .muted { color: var(--grey); }
.footer-nav { display: flex; flex-direction: column; gap: 4px; }
.footer-nav a { font-size: var(--t-body); transition: opacity .2s; }
.footer-nav a:hover { opacity: .55; }

/* 130px flat meant the gap above the small print looked right on a laptop and
   like a mistake on a wide screen. It scales now, same as every other piece of
   vertical space on the site. */
.footer-bottom {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--gap);
  margin-top: clamp(64px, 9vw, 130px);
}
.footer-social a:hover { color: var(--black); }

/* ============================================================
   RESPONSIVE
============================================================ */
@media (max-width: 900px) {
  .menu-toggle { display: block; }
  .intro-text { grid-column: 1 / 4; }
  .work-grid { grid-template-columns: repeat(2, 1fr); }
  .cap-list { left: auto; right: var(--pad); text-align: right; }
}
@media (max-width: 600px) {
  .work-grid { grid-template-columns: 1fr; }
  .footer-top { grid-template-columns: 1fr; gap: 30px; }
  .footer-bottom { grid-template-columns: 1fr; gap: 10px; margin-top: 60px; }
  .hero-thumb { width: 44px; height: 32px; }
  .hero-thumbs { gap: 7px; bottom: 16px; }
}

/* ============================================================
   INNER PAGES
   Shared by about / works / news / careers / contact / privacy.
   Keeps the hdw system: white + black, square edges, outlines.
============================================================ */

/* header used on every inner page — transparent at top, white on scroll */
.page-header {
  position: fixed;
  top: 0; left: 0;
  width: 100%;
  z-index: 100;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 18px var(--pad);
  background: transparent;
  border-bottom: 1px solid transparent;
  color: var(--black);
  transition: background .3s ease, border-color .3s ease, color .3s ease;
}
.page-header.scrolled {
  background: var(--white);
  border-bottom-color: var(--line);
  color: var(--black);
}
/* over a full-bleed cover image the bar is see-through and the mark is white */
body.cover-page .page-header { color: #fff; }
body.cover-page .page-header.scrolled { color: var(--black); }
.page-header .logo { color: inherit; }
.page-nav {
  position: absolute;
  right: var(--pad);
  display: flex;
  gap: 26px;
}
.page-nav a { font-family: var(--sans); font-size: var(--t-body); letter-spacing: var(--body-track); transition: opacity .2s; }
.page-nav a:hover { opacity: .55; }

/* ---------- how every inner page starts ----------

   ONE block, ONE set of numbers. Work, Services, Agency, Careers,
   Contact, FAQ and Privacy all open with .page-hero and none of them
   is allowed a top padding of its own. That is the entire fix for
   "the pages start differently": there is now only one place a page
   can start from. If a page looks wrong here, change --page-top, not
   the page. */
.page-hero {
  padding-top: var(--page-top);
  padding-bottom: var(--page-lead-gap);
}
.page-kicker {
  display: block;
  margin-bottom: 18px;
}
/* Its own size rather than --t-display. That token is also carrying .cap-head,
   .cs-title and the brand-discovery intro, which are headings inside the flow of
   a page and not hero type at all. Shrinking the token would have taken those
   down with it. */
.page-title {
  font-family: var(--sans);
  font-weight: 400;
  font-size: clamp(30px, 3.6vw, 46px);
  line-height: 1.04;
  letter-spacing: 0;
}
.page-lead {
  font-family: var(--sans);
  font-size: var(--t-heading);
  line-height: 1.1;
  letter-spacing: 0;
  max-width: 22ch;
  margin-top: 28px;
}

/* generic prose (about / privacy / article body) */
.prose { max-width: 68ch; }
.prose p { font-size: var(--t-body); line-height: 1.6; margin-bottom: 22px; color: var(--black); }
.prose h2 {
  font-family: var(--sans);
  font-weight: 400;
  font-size: var(--t-heading);
  line-height: 1.05;
  letter-spacing: 0;
  margin: 48px 0 18px;
}
.prose a { border-bottom: 1px solid currentColor; }
/* The reset takes the bullets off every ul on the site, which is right for a
   nav and wrong for the one place that has an actual list in it: the privacy
   page naming each thing the browser stores. A rule rather than a disc, set in
   the hairline grey the rest of the site rules with, so a list reads as a list
   without importing a bullet the design does not otherwise use. */
.prose ul { margin: 0 0 22px; display: grid; gap: 10px; }
.prose li {
  font-size: var(--t-body);
  line-height: 1.6;
  color: var(--black);
  padding-left: 22px;
  position: relative;
}
.prose li::before {
  content: '';
  position: absolute;
  left: 0; top: .72em;
  width: 12px; height: 1px;
  background: var(--grey);
}
.prose li strong { font-weight: 400; font-family: var(--mono); font-size: var(--t-label); text-transform: uppercase; letter-spacing: var(--label-track); }

/* ---------- Q&A (FAQ page + the short FAQ on contact) ----------
   The old markup was plain .prose, capped at 68ch and left-aligned, so on a
   laptop the questions sat in a narrow column with the whole right half of the
   page empty. Two tracks instead: question left, answer right, hairline
   between. Fills the measure and reads like an index. */
/* No rule per question. One line under every row turned a page of answers into
   a ledger, and it was the single biggest source of clutter on the site. The
   gap does the separating instead. */
.qa-row {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: var(--col-gap);
  padding: clamp(20px, 2.6vw, 32px) 0;
  align-items: start;
}
.qa-q {
  font-family: var(--sans);
  font-weight: 400;
  font-size: var(--t-heading);
  line-height: 1.15;
  letter-spacing: 0;
  margin: 0;
  text-wrap: balance;
}
.qa-a { font-size: var(--t-body); line-height: 1.6; color: var(--grey); }
/* The roster heading is followed by the intro paragraph again, and that
   paragraph's own margin does the spacing down to the first name, so the
   heading does not need to carry it. */
.qa-a p + p { margin-top: 14px; }
.qa-a a { color: var(--black); border-bottom: 1px solid currentColor; }
@media (max-width: 900px) {
  .qa-row { grid-template-columns: 1fr; gap: 12px; padding: 24px 0; }
}

/* The short FAQ that closes the contact page. A named block rather than four
   inline styles on a bare <section>, because inline numbers are exactly how
   two pages end up disagreeing about the same spacing. */
.faq-teaser {
  border-top: 1px solid var(--line);
  padding-top: var(--section-gap);
  padding-bottom: var(--section-gap-lg);
}
.faq-teaser .page-kicker { margin-bottom: clamp(18px, 2.4vw, 30px); }
.faq-teaser .qa-row:first-child { padding-top: 0; }
.faq-teaser-more { margin-top: clamp(26px, 3.4vw, 40px); }

/* ---------- the last thing on a page ----------

   Every page used to write its own bottom padding as an inline style, and
   they disagreed: 130px here, 120px there, 40px on the two short ones. One
   attribute now marks the last section and the number lives here. */
[data-tail] { padding-bottom: var(--section-gap-lg); }
.page-actions { margin-top: 40px; display: flex; flex-wrap: wrap; gap: 22px; }
/* The 404 page's one line for machines: where the sitemap, the llms.txt and
   the markdown twin of the homepage live. A mono label like every other small
   note on the site, just far enough under the human links to read as a footer
   to them. */
.nf-agents { margin-top: 34px; }
.nf-agents a { text-decoration: underline; text-underline-offset: 2px; }
.page-updated { margin-top: 2.2em; }
/* a closing call to action that is ruled off from what came before it */
.svc-cta--ruled { border-top: 1px solid var(--line); }
/* ---------- the agency page's one line ----------
   What five sections used to say, said once. It is offset into the right half
   the way the home page's opening statement is, so a short page keeps a
   rhythm instead of stacking two narrow blocks against the left margin.
   .page-lead would have been the obvious reuse, but that runs at a 22ch
   measure, which is right for a two line lead and wrong for six disciplines
   in a row. */
.about-line {
  display: grid;
  grid-template-columns: repeat(var(--grid-cols), minmax(0, 1fr));
  gap: var(--col-gap);
  padding-bottom: clamp(48px, 7vw, 90px);
}
.about-line p {
  grid-column: 6 / -1;
  font-family: var(--sans);
  font-size: var(--t-heading);
  line-height: 1.1;
  letter-spacing: 0;
}
@media (max-width: 900px) {
  .about-line { display: block; padding-bottom: clamp(40px, 8vw, 64px); }
}

/* ---------- people (agency page) ----------
   Tall photo on the left, founders line at the top right, the roster sitting
   at the bottom right so the column reads top and bottom rather than middle. */
.people { padding: clamp(48px, 7vw, 90px) 0 clamp(80px, 11vw, 150px); }
.people-title {
  font-family: var(--sans);
  font-weight: 400;
  font-size: var(--t-heading);
  line-height: 1;
  letter-spacing: 0;
  margin-bottom: clamp(24px, 3.5vw, 44px);
}
.people-grid {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: var(--col-gap);
  align-items: stretch;
}
.people-photo { margin: 0; background: var(--card); }
.people-photo img { display: block; width: 100%; height: 100%; object-fit: cover; }

.people-copy { display: flex; flex-direction: column; justify-content: space-between; gap: clamp(48px, 8vw, 110px); }
.people-lead { font-size: var(--t-body); line-height: 1.45; max-width: 46ch; }
.people-roster h3 {
  font-family: var(--sans);
  font-weight: 400;
  font-size: var(--t-heading);
  line-height: 1.05;
  letter-spacing: 0;
  margin-bottom: 14px;
}
.people-roster > p { font-size: var(--t-body); line-height: 1.6; color: var(--grey); max-width: 52ch; margin-bottom: clamp(22px, 3vw, 32px); }

/* one line per person: name in full contrast, title stepped back beside it */
.team-member { padding: 5px 0; }
.tm-name { font-family: var(--sans); font-size: var(--t-body); line-height: 1.5; }
/* --grey, not --grey-soft: soft only reaches 3.5:1 against the page and the
   titles stop being readable. --grey clears 4.5:1 in both themes. */
.tm-role { font-family: var(--sans); font-size: var(--t-body); line-height: 1.5; color: var(--grey); }

@media (max-width: 900px) {
  .people-grid { grid-template-columns: 1fr; }
  .people-photo { aspect-ratio: 3 / 4; }
  .people-copy { gap: clamp(32px, 6vw, 56px); }
}

/* The lead is back inside .people-copy, which is a flex column with its own
   gap, so the lead does not carry a bottom margin of its own. */


/* ---------- 2. the photo ----------

   The grid was align-items: stretch against a column that held a lead and
   a paragraph, so both sides came out about the same height. Beside a
   fifteen-row roster, stretch pulls the figure to the list's full height
   and object-fit: cover crops the founders out of their own portrait.
   Start, plus the ratio the photo was actually shot at. */
.people-grid { align-items: start; }
.people-photo { aspect-ratio: 3 / 4; }


/* ---------- 3. one line per person ----------

   One running line: "Name - Role", both at body size, the role stepped back
   in colour only. Not two columns and not two sizes. The hyphen is what
   joins them, so it comes from ::before rather than from the markup.

   --grey, not --grey-soft: soft only reaches 3.5:1 against the page and the
   titles stop being readable. --grey clears 4.5:1 in both themes. */
.tm-role::before { content: ' - '; }

/* No hairline between rows. A rule under every name turns a roster into a
   ledger, which is the note already sitting above .qa-row in style.css.
   The padding does the separating. */


/* ---------- 4. desktop ----------

   Half and half gave the photo a 594px column, so at its own 3:4 it stood
   793px tall and became the section. Three quarters of that is enough for
   the portrait and hands the width to the names, which are the point.

   The roster stays one running line per person, so there is no second
   track here. Capping the list keeps the line length readable and leaves
   the white space the rest of the site is built on instead of stretching
   to the edge. */
@media (min-width: 901px) {
  .people-grid { grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); }
  .team-list { max-width: 34em; }
}


/* ---------- 5. phone ----------

   821px is the width the .people-grid already collapses at, so this rides
   the breakpoint that is there rather than adding another one. */
@media (max-width: 900px) {
  /* One column means the portrait would otherwise run the full width and
     stand 447px tall at 375px, pushing the roster most of a screen away
     from the line that explains it. It is a full-length shot, so it
     cannot be cropped shorter without losing a head. Narrower instead,
     which shortens it and leaves the white space the rest of the site
     is built on. The px cap is for the 760 to 820 band, where a single
     column is wide enough that a percentage alone puts the photo back
     over 600px tall. */
  .people-photo { max-width: min(62%, 300px); }
  .people-grid { gap: clamp(28px, 6vw, 44px); }
}

/* ---------- showreel (home) ---------- */
.reel {
  position: relative;
  height: 100vh; height: 100dvh;
  overflow: hidden;
  background: #000;
  color: #fff;
}
.reel-stage { position: absolute; inset: 0; }
.reel-slide {
  position: absolute; inset: 0;
  background-size: cover; background-position: center;
  opacity: 0;
  transition: opacity 1.1s ease;
  will-change: opacity, transform;
}
.reel-slide.is-active { opacity: 1; animation: reelKB 4.8s ease-out both; }
@keyframes reelKB { from { transform: scale(1.02); } to { transform: scale(1.14); } }
.reel::after {
  content: ''; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: linear-gradient(180deg, rgba(0,0,0,.35) 0%, rgba(0,0,0,0) 28%, rgba(0,0,0,0) 55%, rgba(0,0,0,.6) 100%);
}
.reel-overlay {
  position: absolute; inset: 0; z-index: 2;
  display: flex; flex-direction: column; justify-content: flex-end;
  padding-bottom: clamp(54px, 9vh, 110px);
  pointer-events: none;
}
.reel-overlay a { pointer-events: auto; }
.reel-label {
  position: absolute; top: clamp(82px, 13vh, 140px); left: var(--pad);
  color: #fff; opacity: .8;
}
.reel-caption { overflow: hidden; }
.reel-name { display: block; font-family: var(--sans); font-size: clamp(44px, 8.5vw, 120px); line-height: .96; letter-spacing: 0; }
.reel-cat { display: block; font-family: var(--sans); font-size: var(--t-body); opacity: .85; margin-top: 12px; }
.reel-caption.swap .reel-name { animation: reelCap .7s cubic-bezier(.22,.61,.36,1) both; }
.reel-caption.swap .reel-cat { animation: reelCap .7s cubic-bezier(.22,.61,.36,1) .07s both; }
@keyframes reelCap { from { opacity: 0; transform: translateY(28px); } to { opacity: 1; transform: none; } }
.reel-cta { display: inline-block; margin-top: 26px; font-family: var(--sans); font-size: var(--t-body); color: #fff; border-bottom: 1px solid currentColor; }
.reel-progress {
  position: absolute; top: clamp(82px, 13vh, 140px); right: var(--pad); z-index: 2;
  display: flex; gap: 6px; width: min(240px, 42vw);
}
.reel-progress span { flex: 1; height: 2px; background: rgba(255,255,255,.28); overflow: hidden; }
.reel-progress span i { display: block; height: 100%; width: 0; background: #fff; }
.reel-progress span.done i { width: 100%; }
.reel-progress span.is-active i { animation: reelBar var(--reel-dur, 3500ms) linear forwards; }
@keyframes reelBar { from { width: 0; } to { width: 100%; } }
@media (prefers-reduced-motion: reduce) {
  .reel-slide.is-active { animation: none; }
  .reel-progress { display: none; }
}

/* ---------- services pages ---------- */
.svc-lead {
  font-family: var(--sans);
  font-size: var(--t-heading);
  line-height: 1.15;
  letter-spacing: 0;
  max-width: 24ch;
  margin-bottom: clamp(40px, 6vw, 80px);
}
/* Space between sections, not a rule. Stacked up the page they read as a form
   rather than as writing. */
.svc-section { padding: clamp(40px, 6vw, 72px) 0 0; }
.svc-section > h2 {
  font-family: var(--sans);
  font-weight: 400;
  font-size: var(--t-heading);
  letter-spacing: 0;
  margin-bottom: clamp(20px, 3vw, 32px);
}
.svc-step {
  display: grid;
  grid-template-columns: 56px 1fr;
  gap: 22px;
  padding: 14px 0;
}
.svc-step:first-of-type { padding-top: 0; }
/* The step number is a label, so it is mono. Set one step up from the
   other labels because it carries a 56px column on its own. */
.svc-step .n { font-size: var(--t-body); line-height: 1.25; }
.svc-step h3 { font-family: var(--sans); font-size: var(--t-body); margin-bottom: 7px; }
.svc-step p { font-family: var(--sans); font-size: var(--t-body); line-height: 1.6; color: var(--black); max-width: 60ch; }
.svc-list { columns: 2; column-gap: 40px; max-width: 720px; }
.svc-list li { font-family: var(--sans); font-size: var(--t-body); line-height: 1.5; padding: 8px 0; break-inside: avoid; }
.svc-price { font-family: var(--sans); font-size: var(--t-body); line-height: 1.7; color: var(--black); max-width: 60ch; }
.svc-cta { padding: clamp(40px, 6vw, 80px) 0; }
.svc-cta a { font-family: var(--sans); font-size: var(--t-heading); letter-spacing: 0; }
/* ---------- services hub rows ----------
   Three thin cards side by side told you nothing and left the page empty. Each
   service is now a full-width row: the pitch and what's in it on the left, real
   work from that discipline on the right. Seeing the work IS the argument. */
.svc-row {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: var(--col-gap);
  padding: clamp(44px, 6vw, 78px) 0 0;
  align-items: start;
}
.svc-row-n { display: block; margin-bottom: 14px; }
.svc-row h2 {
  font-weight: 400;
  font-size: var(--t-heading);
  line-height: 1;
  letter-spacing: 0;
  margin-bottom: 18px;
}
.svc-row-desc { font-size: var(--t-body); line-height: 1.55; color: var(--grey); max-width: 46ch; margin-bottom: 22px; }
.svc-row-inc { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 26px; }
.svc-row-inc li {
  font-size: var(--t-small);
  line-height: 1;
  color: var(--black);
  border: 1px solid var(--line);
  padding: 8px 12px;
}
/* The shots in a services row are the same card as the home page: the client's
   name laid over the picture rather than set in grey underneath it. The
   discipline is the heading on the left, so the card only carries the name.
   The 3:4 lives on the card, not on the image, so three shots in a row are
   the same height whatever shape the photographs were shot at. */
.svc-row-shots { display: grid; grid-template-columns: repeat(3, 1fr); gap: clamp(10px, 1.4vw, 18px); }
.svc-row-shots.is-pair { grid-template-columns: repeat(2, 1fr); max-width: 66%; }
.svc-row-shots.is-quad { grid-template-columns: repeat(4, 1fr); }
/* two shot rows stacked inside one section need air between them */
.svc-row-shots + .svc-row-shots,
.svc-section .svc-row-shots { margin-bottom: 26px; }
/* The card is the link on the work page and a div inside it here, so the
   shape goes on the inner one and the grid cell on the outer. */
.svc-row-shots .work-index-card { display: block; }
.svc-row-shots .work-card { display: block; aspect-ratio: 3 / 4; }
.svc-row-shots .work-card img { width: 100%; height: 100%; object-fit: cover;
  transition: transform .8s cubic-bezier(.22,.61,.36,1); }
@media (max-width: 900px) {
  .svc-row { grid-template-columns: 1fr; gap: 26px; padding: 30px 0; }
  .svc-row-shots.is-pair { max-width: none; }
}
/* Three across on a phone leaves 105px thumbnails, which is too small to tell
   one project from another. Two across, third drops below. */
@media (max-width: 600px) {
  .svc-row-shots, .svc-section .svc-row-shots { grid-template-columns: repeat(2, 1fr) !important; }
}

/* services hub cards (legacy, still used by the capabilities block) */
.svc-hub { display: grid; grid-template-columns: repeat(3, 1fr); gap: clamp(24px, 3vw, 40px); padding-bottom: clamp(80px,10vw,150px); }
.svc-card { display: block; border-top: 2px solid var(--black); padding-top: 18px; }
.svc-card h2 { font-family: var(--sans); font-weight: 400; font-size: var(--t-heading); letter-spacing: 0; margin-bottom: 12px; }
.svc-card p { font-family: var(--sans); font-size: var(--t-body); line-height: 1.55; color: var(--grey); margin-bottom: 16px; }
.svc-card .more { font-family: var(--sans); font-size: var(--t-body); transition: opacity .2s; }
/* The card is one big <a>, so the global a:hover rule was fading the whole
   block — heading, copy and all. Hold the card at full opacity and let only
   the Explore link react. */
.svc-card:hover { opacity: 1; }
.svc-card:hover .more { opacity: .55; }
@media (max-width: 900px) {
  .svc-hub { grid-template-columns: 1fr; }
  .svc-list { columns: 1; }
  .svc-step { grid-template-columns: 40px 1fr; gap: 14px; }
}

/* ---------- WORKS INDEX PAGE ---------- */
.works-page { padding-bottom: clamp(90px, 11vw, 170px); }
.works-list {
  counter-reset: work;
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: clamp(36px, 4.5vw, 70px) clamp(20px, 3vw, 44px);
}
.works-list.cols-1 { grid-template-columns: 1fr; }
.works-list.cols-2 { grid-template-columns: repeat(2, 1fr); }
.works-list.cols-3 { grid-template-columns: repeat(3, 1fr); }
.works-list.cols-4 { grid-template-columns: repeat(4, 1fr); }
/* The column count used to come only from the View control, so with that gone
   the grid had nothing telling it to narrow and would have stayed three across
   on a phone. Three, two, then two, which keeps a pair of thumbnails side by
   side rather than one enormous card per screen. */
@media (max-width: 900px) {
  .works-list, .works-list.cols-3, .works-list.cols-4 { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 600px) {
  .works-list, .works-list.cols-3, .works-list.cols-4 { grid-template-columns: repeat(2, 1fr); }
  .works-list { gap: clamp(20px, 5vw, 30px) 12px; }
}
.work-index-card { counter-increment: work; display: block; }
.works-list .work-card {
  aspect-ratio: 5 / 4;
  background: var(--card);
}
.works-list .work-card img { transition: transform .8s cubic-bezier(.22,.61,.36,1); }
.work-index-card:hover .work-card img { transform: scale(1.05); }

.work-row {
  display: none;            /* web: thumbnails only, name shows on hover (cursor tag) */
  align-items: baseline;
  gap: 14px;
  margin-top: 14px;
}
@media (max-width: 900px) { .work-row { display: flex; } }   /* phone: names shown */
/* Delight, not Exposure. These are captions under a grid of images, not
   headlines, and at display size they were competing with the work. */
.work-row .t {
  font-family: var(--sans);
  font-size: var(--t-body);
  letter-spacing: var(--body-track);
  transition: opacity .25s;
}
.work-row .cat { display: none; }   /* name only, per request (kept in markup as filter data) */
.work-index-card:hover .work-row .t { opacity: .55; }

/* ---------- WORK GRID: cards that move ----------
   The clip sits over the still, not instead of it. It fades in once it is
   actually playing, so a slow connection, Low Power Mode or a browser that
   refuses to autoplay all land on the photograph rather than on a black box.
   Nothing shifts either way: both fill the same card. */
.work-clip {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  opacity: 0;
  transition: opacity .45s ease;
  pointer-events: none;      /* the card is the link, the clip is decoration */
}
.work-index-card.is-playing .work-clip { opacity: 1; }
/* the still scales on hover, so the clip has to travel with it or the two
   separate as the card grows */
.work-index-card:hover .work-clip { transform: scale(1.05); transition: opacity .45s ease, transform .8s cubic-bezier(.22,.61,.36,1); }

/* ---------- WORK GRID: the card that leaves the site ----------
   Two clients have real work but not enough of it to carry a case study, so
   their card goes to the client's own Instagram. Every other card on this page
   opens a page here, so the difference has to be readable before the click
   rather than discovered after it.

   A mark and not a line of copy, because the names are already hidden on
   desktop: .work-row is display:none above 700px and the name arrives on the
   cursor tag instead. The image is the only thing both layouts show, so a mark
   on the image is the one signal that survives at every width.

   Same orange chip as .cursor-tag, so the mark and the hover tag read as one
   idea rather than two decorations. */
.work-out {
  position: absolute;
  top: 10px;
  right: 10px;
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  background: var(--black);
  color: var(--white);
  font-family: var(--sans);
  font-size: var(--t-small);
  line-height: 1;
  transition: transform .3s cubic-bezier(.22,.61,.36,1);
}
/* the image scales under it on hover, so the mark travels the way it points
   rather than sitting still while everything beneath it grows */
.work-index-card:hover .work-out { transform: translate(2px, -2px); }

/* ---------- CASE STUDY ---------- */
.cs-hero {
  position: relative;
  height: 100vh;
  height: 100dvh;
  overflow: hidden;
  background: #111;
}
.cs-hero img { width: 100%; height: 100%; object-fit: cover; }
/* Readability scrim, so the name stays legible on a light picture.

   This used to be a 95deg gradient, dark on the left and clear by 80%, because
   the name used to sit on the left rail. The name is in the middle of the
   screen now and that gradient left it sitting on the clear end, over whatever
   the photograph happened to be doing there. A centred name needs a centred
   scrim: darkest under the word, easing out to the corners, so it works the
   same whichever way the picture is lit. */
.cs-hero::after {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(ellipse 90% 70% at 50% 50%,
              rgba(0,0,0,.5), rgba(0,0,0,.28) 55%, rgba(0,0,0,.12) 100%);
  pointer-events: none;
}
/* ---------- the client's name on a case study cover ----------

   The cover is the name and nothing else now. It used to carry a tagline under
   it explaining the work in a sentence, which meant the first thing on the page
   was a claim rather than a client, and the sentence is already in the summary
   two screens down. One word, dead centre, over the picture.

   Centred on both axes rather than on the left rail. There is nothing else on
   this screen to line up with, so the rail was aligning the name to a column
   that does not appear until the next scroll. The middle of the screen is the
   only mark a full-bleed cover actually has. */
.cs-cover {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  text-align: center;
  padding: 0 var(--pad);
  color: #fff;
  text-shadow: 0 1px 20px rgba(0,0,0,.35);
}
.cs-cover-name {
  font-family: var(--sans);
  font-weight: 400;
  /* Was floored at 46 to stay bigger than a two line tagline underneath it.
     The tagline is gone, so the floor is free to come down and the name is
     free to be the only thing here. */
  font-size: clamp(42px, 7vw, 96px);
  line-height: .96;
  letter-spacing: -0.005em;   /* Delight opens up at display size; this closes it back */
  max-width: 16ch;            /* a long client name breaks rather than touching the edges */
}
/* A campaign's cover says whose campaign it is, above the name. Without it the
   page opens on two words with no owner: Wedding Season could belong to anyone,
   and the client is not named again until the sidebar, below the fold. Set as a
   label rather than a sentence, so it reads as a tag on the name and never
   competes with it. */
.cs-cover-kicker {
  display: block;
  /* .u-label carries var(--grey) with it, which is right on white and wrong
     here: this sits on a photograph and the cover sets everything else to #fff.
     Without this the kicker came out mid-grey on a dark picture at .85 opacity,
     which is close to invisible. */
  color: inherit;
  margin-bottom: clamp(10px, 1vw, 14px);
  opacity: .85;
}

/* cover pages keep the standard solid white sticky header (brandmark always visible) */
.cs-head {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: var(--gap);
  padding: clamp(50px, 7vw, 100px) 0 clamp(40px, 5vw, 70px);
}
.cs-title {
  grid-column: 1 / 2;
  font-family: var(--sans);
  font-weight: 400;
  font-size: var(--t-display);
  line-height: .98;
  letter-spacing: 0;
}
.cs-summary { grid-column: 2 / 4; }
.cs-summary p {
  font-family: var(--sans);
  font-size: var(--t-heading);
  line-height: 1.12;
  letter-spacing: 0;
  margin-bottom: 30px;
}
.cs-meta {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 24px;
  font-family: var(--sans);
  font-size: var(--t-body);
}
.cs-meta .k { color: var(--grey); display: block; margin-bottom: 8px; font-size: var(--t-body); }
/* mosaic gallery — column count set by the View toggle */
/* A grid, not CSS columns. Columns fill the first column all the way down
   before starting the second, so the work read top to bottom on the left and
   the order made no sense to anyone scanning across. A grid reads the way it
   is written: left to right, then the next row.

   Items keep their own proportions and sit to the top of their row, so a
   portrait reel next to a landscape photograph leaves space under the shorter
   one rather than cropping either. */
/* Mason. Fixed columns, each picture at its full height, so the columns end at
   different points and the bottom edge is ragged. Nothing is ever cropped,
   which is why it was chosen: a tall reel and a wide photograph both arrive
   whole.

   Worth knowing: CSS columns fill the first column top to bottom before
   starting the second, so the reading order runs down rather than across. That
   is the trade this layout makes. --cols is what the View control changes. */
.cs-gallery {
  --cols: 4;                 /* a feed, not three wide strips */
  --gutter: 12px;
  display: grid;
  grid-template-columns: repeat(var(--cols), minmax(0, 1fr));
  align-items: start;
  gap: var(--gutter);
  padding-bottom: clamp(70px, 9vw, 130px);
}
/* js/masonry.js builds these and fills them across rather than down. Before it
   has run, and with no JavaScript at all, the pictures sit in the same number
   of columns as a plain grid with the rows lined up at the top: same width,
   same order, only the packing missing. The gallery never changes shape, it
   only closes up. */
.mason-col { min-width: 0; display: flex; flex-direction: column; gap: var(--gutter); }
/* the rest of a campaign, behind Show more: the fold lives in js/masonry.js.
   Three classes deep on purpose: the gallery item rule further down is two
   classes deep and later in the file, and would otherwise win. */
.cs-campaign .cs-gallery > .is-folded { display: none; }
/* ---------- the short version first ----------
   A case study opens with the problem. The approach and the outcome sit behind
   one control, because most people scanning the work want the shape of it, not
   all of it. The collapse only applies once js/readall.js has marked the page,
   so with no JavaScript every paragraph is simply there, the way it was. */
.readall-ready .cs-story .cs-chapter { display: none; }
.readall-ready.readall-open .cs-story .cs-chapter { display: grid; }
.readall {
  font-family: var(--sans);
  font-size: var(--t-body);
  color: var(--black);
  border-bottom: 1px solid currentColor;
  padding: 0 0 2px;
  margin-bottom: clamp(44px, 6vw, 80px);
  transition: opacity .2s;
}
.readall:hover { opacity: .55; }



/* ---------- campaigns ----------
   A named piece of work inside a client relationship: a pop-up, a season, a
   Valentine week, a menu. It is not a client and it is not a discipline, so it
   gets a block of its own at the foot of the client page and appears nowhere
   else on the site. Not the work grid, not the home page, not a filter. The
   only route to a campaign is through the client it belongs to.

   That is the whole argument for the block existing. A three day pop-up sitting
   in the work grid beside a two year client tells a visitor the two are the
   same size of thing, and they are not.

   Two sizes. A small one keeps its pictures here and opens them in place. A big
   one has a page. Both look identical down to the control, so promoting one
   costs no redesign. Set smaller than a discipline heading on purpose: the
   hierarchy is client, then discipline, then campaign. */
.cs-campaigns { padding-bottom: clamp(70px, 9vw, 130px); }
.cs-campaign { margin-top: clamp(30px, 4vw, 52px); }
.cs-campaigns .cs-campaign:first-of-type { margin-top: clamp(18px, 2vw, 26px); }
.cs-campaign + .cs-campaign { margin-top: clamp(48px, 6vw, 84px); }
.cs-campaign-title {
  font-family: var(--sans);
  font-weight: 400;
  font-size: var(--t-body);
  line-height: 1.2;
  letter-spacing: var(--body-track);
}
.cs-campaign-line {
  font-family: var(--sans);
  font-size: var(--t-body);
  line-height: 1.55;
  color: var(--grey);
  max-width: 52ch;
  margin: 6px 0 clamp(16px, 2vw, 24px);
}

/* A campaign follows the View like every other gallery on the page, and shows
   exactly one row of it: as many pictures as there are columns, the rest
   behind Show more. js/masonry.js decides the row, because it is the one thing
   that knows the column count at the moment the columns are built. With no
   JavaScript every picture is simply there and the control is not, which is
   the right fallback: nothing is lost but the fold.

   The rest fold inside the same gallery as the first row rather than in a
   second one below it, so opening fills the grid already on screen instead of
   starting another underneath with its own top edge. */
.cs-campaigns .cs-campaign .cs-gallery { padding-bottom: 0; }
.cs-campaign-more { display: none; }               /* the old two-gallery shape */

/* One control, two jobs. Show more opens the rest in place; on a campaign that
   has a page of its own the same control leaves instead. Deliberately the same
   shape, because the two are the same kind of thing at different sizes. */
.cs-campaign-btn {
  display: inline-block;
  font-family: var(--sans);
  font-size: var(--t-body);
  line-height: 1.3;
  color: var(--black);
  background: none;
  border: 0;
  border-bottom: 1px solid currentColor;
  border-radius: 0;
  padding: 0 0 2px;
  margin-top: clamp(16px, 2vw, 24px);
  cursor: pointer;
  transition: opacity .2s;
}
.cs-campaign-btn:hover { opacity: .55; }
/* Show more exists only when there is more. The fold in js/masonry.js marks it
   needed when a campaign holds more pictures than one row, so a campaign of
   two pictures on a three column page shows two pictures and no button. */
.cs-campaign-btn--more { display: none; }
.cs-campaign-btn--more.is-needed { display: inline-block; }

/* The sidebar index. A campaign at the foot of a long page cannot be seen from
   the top of it. These are plain links, not scroll-spy targets like the
   disciplines above them, so they sit at full strength rather than waiting to
   be lit by a block coming into view. */
.cs-side-campaigns a.cs-disc { opacity: 1; }
.cs-side-campaigns a.cs-disc:hover { opacity: .55; }

/* ---------- featured in ----------
   Where a client's work got written about. The win is the client's, not ours,
   so this sits inside their case study as evidence the work landed, and never
   on the home page or the work grid as a credential.

   Three rules hold it away from the "As seen in" bar every skincare brand has.
   Five marks at the absolute most, because a sixth reads as a trust badge no
   matter how quiet the type is. Left aligned to the same measure as the body
   copy with a fixed gap, never spread across the full width, so the row stops
   short and reads as a footnote rather than a wall. And small: about twice the
   cap height of the body text, nowhere near a logo lockup.

   Every mark also carries its publication name as real text, hidden from sight
   but not from a screen reader or a search engine. A path in an SVG cannot tell
   anyone who covered the work. On a narrow phone the marks drop out entirely and
   that same text becomes the row, because a masthead below about 18px is a
   smudge and a four-item horizontal scroll loses the last two. */
.cs-press { margin: 0 0 clamp(48px, 6vw, 84px); }
.cs-press-k { display: block; margin-bottom: clamp(16px, 2vw, 24px); }
.cs-press-row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: clamp(26px, 4vw, 54px);
  list-style: none;
  margin: 0;
  padding: 0;
}
.cs-press-row a {
  display: block;
  border: 0;
  color: var(--black);
  transition: opacity .2s;
}
.cs-press-row a:hover { opacity: .55; }

/* Height from the mark's proportions, never a flat height for all of them. A
   six-to-one wordmark set to the same height as a square emblem covers six
   times the area and eats the row. The factor damps a wide mark down towards
   0.8 and leaves a squarer one at full size, with a floor at half so nothing
   disappears. --nudge is the hand correction on top, because no formula
   survives contact with two real mastheads: it is set per mark and judged by
   eye against how dark each one reads, not by measuring it. */
.cs-press-mark {
  /* The floor is high because the damping factor cuts into it: at a 19px base a
     wide wordmark came out 16px, which is under the size a masthead stays
     readable at. A mark this small barely wants to scale anyway. */
  --cap: clamp(22px, 2vw, 26px);
  --ratio: calc(var(--w) / var(--h));
  --factor: min(calc(1.1 - 0.1 * var(--ratio)), 1);
  --nudge: 1;
  display: block;
  width: auto;
  height: calc(max(calc(var(--cap) / 2), calc(var(--cap) * var(--factor))) * var(--nudge));
  aspect-ratio: var(--w) / var(--h);
}

/* Real text, out of sight. Not decorative: it is the only part of the row a
   screen reader or a crawler can read, so it is never alt="". */
.cs-press-name {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
/* The marks still fit on a normal phone: two of them and a gap come to about
   160px inside a 335px column, so the fallback is for genuinely tiny screens
   only. Set at 400px first, which meant no iPhone ever saw a masthead. */
@media (max-width: 340px) {
  .cs-press-mark { display: none; }
  .cs-press-name {
    position: static;
    width: auto;
    height: auto;
    margin: 0;
    overflow: visible;
    clip-path: none;
    font-family: var(--sans);
    font-size: var(--t-body);
    border-bottom: 1px solid currentColor;
  }
  .cs-press-row { gap: 12px 26px; }
}

/* Who a campaign belongs to, in the sidebar of the campaign's own page. Every
   other value in that column is plain text, so a rule and an arrow made this
   one row shout. It is the name of a client, not a call to action. Still a
   link, and it still lifts on hover, but at rest it reads as the answer to the
   question in the label beside it. */
.cs-side-row .v a.cs-client {
  border-bottom: 0;
  transition: opacity .2s;
}
.cs-side-row .v a.cs-client:hover { opacity: .55; }

/* Clips sit in the same grid as the photographs. A reel is social media output,
   not a separate discipline, so pushing it into a Film section at the bottom of
   the page said the opposite of what the work actually is. */
.cs-gallery img, .cs-gallery video, .cs-gallery .cs-shot, .cs-gallery .hdw-player {
  width: 100%;
  height: auto;
  display: block;
  background: var(--card);
}
/* A clip carries its width and height in the markup, the same as a picture,
   and js/video.js writes that shape onto the wrapper it builds, so the tile is
   the right shape before anything has loaded. A clip written without a size
   holds a reel's shape until js/masonry.js has measured its poster. */
.cs-gallery .hdw-player, .cs-gallery > video:not([width]) { aspect-ratio: 9 / 16; }

/* ---------- CASE STUDY CLIPS: hdw's own player (js/video.js) ----------
   The tile is the player. The picture fills the wrapper's shape edge to edge,
   and the controls sit over its foot in white, with a shadow rather than a
   scrim behind them: a hairline and three small marks stay readable over any
   frame without darkening the work. They are there on hover, on keyboard
   focus, and on the clip that was pressed, and they step aside while it runs.

   The progress mark is the only orange on the site. That is deliberate: it
   is the one thing anywhere that carries information rather than a command,
   so it is the one thing that gets the accent. */
.hdw-player {
  position: relative;
  overflow: hidden;
  /* the View toggle changes how wide a clip is without the window changing at
     all, so the bar has to answer to the clip's width, not the viewport's */
  container-type: inline-size;
}
.hdwp-stage { position: absolute; inset: 0; display: block; font-size: 0; }
.hdw-player video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  margin: 0;
  cursor: pointer;      /* the whole frame is the play target, on a phone especially */
}

/* ---------- the mark over a paused clip ----------
   The bar underneath already has Play, but on a phone the frame is the target
   and a poster with no mark on it reads as an image, not a clip. Solid black
   square, white glyph: it has to survive a white frame and a black one, so it
   cannot be a tint of either. */
.hdwp-cue {
  position: absolute;
  inset: 0;
  display: none;
  align-items: center;
  justify-content: center;
  pointer-events: none;      /* the click belongs to the clip underneath */
}
.hdw-player.is-paused .hdwp-cue { display: flex; }
.hdwp-cue svg {
  width: 52px;
  height: 52px;
  padding: 17px;
  background: #0a0a0a;       /* not var(--black): the mark stays this way in both themes */
  fill: #fff;
  opacity: .88;
}

/* ---------- the bar ---------- */
.hdwp-bar {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  gap: 2px;
  padding: 0 2px;
  color: #fff;
  /* white marks over an unknown frame: a soft shadow, never a scrim */
  filter: drop-shadow(0 1px 2px rgba(0,0,0,.6));
  transition: opacity .25s ease;
}
.hdwp-btn {
  flex: 0 0 auto;
  width: 44px;               /* thumb sized, and the same at every column count */
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: inherit;
  background: none;
  border: 0;
  padding: 0;
  transition: opacity .2s ease;
}
.hdwp-btn:hover { opacity: .6; }
.hdwp-btn svg { width: 15px; height: 15px; fill: currentColor; }

/* the row is 44px tall so a thumb can find it, the mark inside is 2px so the
   eye reads a hairline rather than a slider */
.hdwp-scrub {
  flex: 1 1 auto;
  min-width: 32px;
  height: 44px;
  display: flex;
  align-items: center;
  padding-inline: 6px;
  cursor: pointer;
  touch-action: none;        /* a drag along this bar is a scrub, not a page scroll */
}
.hdwp-track {
  display: block;
  position: relative;
  width: 100%;
  height: 2px;
  background: rgba(255,255,255,.45);
  transition: height .15s ease;
}
.hdwp-fill { display: block; height: 100%; width: 0; background: var(--accent); }
.hdwp-scrub:hover .hdwp-track,
.hdwp-scrub:focus-visible .hdwp-track { height: 4px; }

.hdwp-time {
  font-family: var(--sans);
  font-size: var(--t-small);
  font-variant-numeric: tabular-nums;   /* the row must not shuffle as the seconds tick */
  color: inherit;
  padding-inline: 4px;
  min-width: 4ch;
  text-align: right;
}

/* ---------- a narrow clip ----------
   Two columns on a 375px phone leaves a clip about 157px wide. One row of five
   controls needs about 230px before the buttons have to shrink below a thumb,
   so under that the timeline and the clock take the first line and the three
   buttons keep their full size on the second.

   The basis is what forces that split rather than luck: the timeline claims
   everything but one button's width, so the clock still fits beside it and the
   first button never can. It holds at any width, so there is no second number
   here to keep in step with the first. */
@container (max-width: 230px) {
  .hdwp-bar { flex-wrap: wrap; }
  .hdwp-scrub { order: -2; flex: 0 1 calc(100% - 44px); min-width: 0; height: 34px; }
  .hdwp-time { order: -1; font-size: var(--t-small); }
  .hdwp-cue svg { width: 40px; height: 40px; padding: 13px; }
}

/* The story block carries a bottom margin for the case where everything is
   shown. Collapsed, that margin would leave a hole above the button. */
.readall-ready .cs-story { margin-bottom: 0; }
.readall-ready.readall-open .cs-story { margin-bottom: clamp(26px, 3.5vw, 44px); }

/* ---------- when the row shows ----------
   A gallery is a page of work. A clip sitting in it wears nothing at all but
   the play mark: the whole tile is the play target, and a row of controls on
   every thumbnail turns a portfolio into a media library. The row arrives on
   hover, on keyboard focus, or once the clip has been pressed, and while the
   clip runs with nothing moving over it, it steps out of the way. Full screen
   is offered only while the clip is running: a still has nothing to make
   bigger. */
.hdwp-bar { opacity: 0; pointer-events: none; }
.hdw-player:hover .hdwp-bar,
.hdw-player:focus-within .hdwp-bar,
.hdw-player.is-open .hdwp-bar { opacity: 1; pointer-events: auto; }
.hdw-player.is-idle .hdwp-bar { opacity: 0; pointer-events: none; }
.hdwp-full { display: none; }
.hdw-player.is-playing .hdwp-full { display: flex; }

/* ---------- full screen ----------
   The wrapper goes full screen rather than the clip, so these controls stay
   and the browser's own never appear. Black surround in both themes, because
   full screen is a cinema and not a page. Safari before 16.4 needs its own
   selector: one unknown selector in a list would throw the whole rule away, so
   these are written out twice on purpose. */
.hdw-player:fullscreen { background: #000; container-type: normal; }
.hdw-player:fullscreen video { object-fit: contain; }
.hdw-player:fullscreen .hdwp-bar { padding-inline: 8px; }
.hdw-player:-webkit-full-screen { background: #000; container-type: normal; }
.hdw-player:-webkit-full-screen video { object-fit: contain; }
.hdw-player:-webkit-full-screen .hdwp-bar { padding-inline: 8px; }

@media (prefers-reduced-motion: reduce) {
  .hdwp-btn, .hdwp-track { transition: none; }
}

/* The View control sets these. Grid tracks now, not column counts. */
.cs-gallery.cols-1 { --cols: 1; }
.cs-gallery.cols-2 { --cols: 2; }
.cs-gallery.cols-3 { --cols: 3; }
.cs-gallery.cols-4 { --cols: 4; }
.cs-gallery.cols-5 { --cols: 5; }
/* A three across grid on a phone is three thumbnails too small to read, so
   anything above two is pulled back to two here.

   What this rule used to do was pull EVERY count back to two, cols-1 included.
   That is why the View control was dead on a phone: whatever it was set to, the
   gallery answered two, and the only thing that ever changed was the label on
   the button. One and two are both honoured now, and the control offers exactly
   those two on a phone, so what it shows is what you get. */
@media (max-width: 900px) {
  .cs-gallery, .cs-gallery.cols-3,
  .cs-gallery.cols-4, .cs-gallery.cols-5 { --cols: 2; }
}
.cs-next {
  display: flex;
  justify-content: space-between;
  align-items: baseline;
  border-top: 1px solid var(--line);
  padding: 40px 0 clamp(70px,9vw,120px);
}
.cs-next .lbl { display: block; margin-bottom: 8px; }
.cs-next .nm {
  font-family: var(--sans);
  font-size: var(--t-heading);
  letter-spacing: 0;
  line-height: 1;
}
.cs-next a:hover .nm { opacity: .55; }

/* ---------- NEWS LIST ---------- */
.news-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 50px var(--gap);
  padding-bottom: clamp(80px, 10vw, 150px);
}

/* ---------- ARTICLE ---------- */
.article-head { padding: clamp(50px,7vw,90px) 0 36px; max-width: 22ch; }
.article-meta { display: flex; gap: 20px; margin-bottom: 22px; }
.article-hero { width: 100%; aspect-ratio: 16/9; object-fit: cover; background: var(--card); margin-bottom: clamp(40px,5vw,70px); }

/* ---------- FORMS (careers + contact) ---------- */
.form-page { padding-bottom: clamp(80px,10vw,150px); }
.form-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 28px var(--gap);
  max-width: 860px;
}
.field { display: flex; flex-direction: column; gap: 10px; }
.field.full { grid-column: 1 / -1; }
.field label {
  font-family: var(--sans);
  font-size: var(--t-body);
  color: var(--black);
}
.field .hint { font-family: var(--sans); font-size: var(--t-small); color: var(--grey); }
.field input:not([type="checkbox"]):not([type="radio"]):not([type="file"]),
.field select,
.field textarea {
  font-family: var(--sans);
  font-size: var(--t-body);
  color: var(--black);
  background: var(--white);
  border: 1px solid var(--field);
  border-radius: 0;                 /* square edges (hdw) */
  padding: 14px 15px;
  width: 100%;
  transition: border-color .2s;
}
.field textarea { resize: vertical; min-height: 130px; }
.field input::placeholder, .field textarea::placeholder { color: var(--grey-soft); }
/* The border darkening is a nice touch on top of the focus ring. It was
   written instead of one: `outline: none` here is more specific than the
   :focus-visible rule further down, so every text field, select and textarea
   on the site had no ring at all, and a keyboard user filling in the contact
   form could not see which box they were in. A 1px border going from light
   grey to black is not a focus state; it is the same shape as before.

   :focus:not(:focus-visible) is the narrow version of what was meant: a mouse
   click does not draw a ring, a Tab key does. */
.field input:not([type="checkbox"]):not([type="radio"]):focus,
.field select:focus,
.field textarea:focus { border-color: var(--black); }
.field input:not([type="checkbox"]):not([type="radio"]):focus:not(:focus-visible),
.field select:focus:not(:focus-visible),
.field textarea:focus:not(:focus-visible) { outline: none; }
.file-drop {
  border: 1px solid var(--field);
  padding: 14px 15px;
  font-family: var(--sans);
  font-size: var(--t-body);
  color: var(--grey);
  cursor: pointer;
  transition: border-color .2s, color .2s;
}
.file-drop:hover { opacity: .6; }
.radio-row { display: flex; flex-wrap: wrap; gap: 22px; }
.radio-row label { display: flex; align-items: center; gap: 8px; font-size: var(--t-body); color: var(--black); }
.btn-submit {
  font-family: var(--sans);
  font-size: var(--t-body);
  background: none;
  color: var(--black);
  border: 0;
  border-bottom: 1px solid currentColor;
  border-radius: 0;
  padding: 4px 0;
  align-self: start;
  transition: opacity .2s;
}
.btn-submit:hover { opacity: .6; }
/* The submit is a text link by design, which left a 32px tap target on the one
   control that matters most. This grows the hit area to ~52px without moving a
   pixel of the underline. */
.btn-submit { position: relative; }
.btn-submit::after { content: ''; position: absolute; left: -8px; right: -8px; top: -10px; bottom: -10px; }
.form-note { font-family: var(--sans); font-size: var(--t-small); color: var(--grey); margin-top: 18px; }

/* contact two-column */
.contact-grid {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: var(--col-gap);
  padding-bottom: clamp(80px,10vw,150px);
  align-items: start;
}
/* The details stay put while the form scrolls, so the email and WhatsApp are
   reachable at any point in a long form instead of only at the very top. */
.contact-info { position: sticky; top: clamp(96px, 11vh, 132px); }
.contact-info dl { margin-top: 8px; }
.contact-info dt { margin-top: 30px; }
.contact-info dl > dt:first-child { margin-top: 0; }
.contact-info dd {
  font-family: var(--sans);
  font-size: var(--t-heading);
  letter-spacing: 0;
  margin: 4px 0 0;
}
.contact-info dd a { border-bottom: 1px solid currentColor; }
/* Three links on one line, so they step down a size from the email and the
   phone number. A class rather than an inline font-size on the <dd>, which
   is how one page ends up with a number no other page knows about. */
/* ---------- the open roles, on the careers page ----------
   These used to exist only inside the Opening dropdown, fetched after the page
   had loaded. A search engine cannot open a dropdown and Google will not show
   a job posting whose text is not visible on the page, so the roles are listed
   properly now and written in at build time by scripts/build-openings.py.

   They sit in the same column as Studio and Your application, so they take
   that column's size rather than inventing one. The role name is the loud
   part; a description or a location under it is quieter, the way the sidebar
   on a case study works. */
.openings { display: grid; gap: 10px; margin-top: 2px; }
.opening { display: grid; gap: 2px; }
.opening-title { font-family: var(--sans); font-size: var(--t-body); color: var(--black); }
.opening-desc,
.opening-meta { font-family: var(--sans); font-size: var(--t-small); color: var(--grey); line-height: 1.45; }
.opening-none { color: var(--grey); }

.contact-info .dd-social { font-size: var(--t-body); letter-spacing: var(--body-track); display: flex; flex-wrap: wrap; gap: 18px; }

@media (max-width: 900px) {
  .menu-toggle { display: block; }

  /* keep the brandmark + Close above the full-screen menu overlay (the overlay
     is a child of the header, so without this the logo/Close hide behind it) */
  .site-header .logo, .page-header .logo, .menu-toggle { position: relative; z-index: 95; }

  /* full-page mobile menu (home + inner pages) — the white panel rises from
     the bottom to cover the page, then the links fade + slide up one by one */
  .main-nav, .page-nav {
    position: fixed;
    inset: 0;
    z-index: 90;
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: flex-start;
    gap: 6px;
    background: var(--white);
    padding: 86px var(--pad) 40px;   /* reserve room for the header so links never sit under the brandmark/Close */
    transform: translateY(100%);
    visibility: hidden;
    transition: none;   /* no animation until the page is ready (kills the open-on-load flash) */
  }
  body.nav-ready .main-nav, body.nav-ready .page-nav {
    transition: transform .55s cubic-bezier(.76,0,.24,1), visibility .55s;
  }
  .main-nav.is-open, .page-nav.is-open {
    transform: translateY(0);
    visibility: visible;
  }
  .main-nav a, .page-nav a {
    font-family: var(--sans);
    font-size: clamp(44px, 13vw, 76px);
    line-height: 1.12;
    letter-spacing: 0;
    color: var(--black);
    opacity: 0;
    transform: translateY(26px);
    transition: opacity .5s ease, transform .55s cubic-bezier(.22,.61,.36,1);
  }
  .main-nav.is-open a, .page-nav.is-open a { opacity: 1; transform: none; }
  .main-nav.is-open a:nth-child(1), .page-nav.is-open a:nth-child(1) { transition-delay: .22s; }
  .main-nav.is-open a:nth-child(2), .page-nav.is-open a:nth-child(2) { transition-delay: .30s; }
  .main-nav.is-open a:nth-child(3), .page-nav.is-open a:nth-child(3) { transition-delay: .38s; }
  .main-nav.is-open a:nth-child(4), .page-nav.is-open a:nth-child(4) { transition-delay: .46s; }
  .main-nav.is-open a:nth-child(5), .page-nav.is-open a:nth-child(5) { transition-delay: .54s; }
  .main-nav a::after, .page-nav a::after { display: none; }

  /* while the menu is open, force the header solid so the mark + Close are visible */
  body.menu-open { overflow: hidden; }
  body.menu-open .site-header,
  body.menu-open .page-header {
    background: var(--white);
    color: var(--black);
    border-bottom-color: transparent;
  }

  .cs-head { grid-template-columns: 1fr; gap: 30px; }
  .cs-title, .cs-summary { grid-column: auto; }
  .news-grid { grid-template-columns: repeat(2, 1fr); }
  .contact-grid { grid-template-columns: 1fr; gap: clamp(48px, 11vw, 76px); }
}
@media (max-width: 600px) {
  .news-grid { grid-template-columns: 1fr; }
  .form-grid { grid-template-columns: 1fr; }
  .cs-gallery.two { grid-template-columns: 1fr; }
  .page-lead { margin-left: 0; }
}

/* ============================================================
   ACCESSIBILITY + POLISH PASS
============================================================ */

/* keyboard focus — visible, in the page's own ink, square */
:focus-visible {
  outline: 2px solid currentColor;
  outline-offset: 3px;
  border-radius: 0;
}
/* mouse users don't get the ring */
:focus:not(:focus-visible) { outline: none; }

/* skip link (injected by main.js) */
.skip-link {
  position: fixed;
  top: 10px; left: 10px;
  z-index: 200;
  background: var(--black);
  color: var(--white);
  font-family: var(--sans);
  font-size: var(--t-body);
  padding: 10px 16px;
  transform: translateY(-160%);
  transition: transform .2s ease;
}
.skip-link:focus { transform: translateY(0); }

/* animated underline for nav + footer links (restrained) */
.main-nav a, .page-nav a, .footer-nav a {
  position: relative;
}
.main-nav a::after, .page-nav a::after, .footer-nav a::after {
  content: '';
  position: absolute;
  left: 0; bottom: -3px;
  width: 100%; height: 1px;
  background: currentColor;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform .3s cubic-bezier(.22,.61,.36,1);
}
.main-nav a:hover::after, .page-nav a:hover::after,
.footer-nav a:hover::after { transform: scaleX(1); }

/* fix: cover-page mobile menu — dark links on the white dropdown */
.page-nav.is-open a { color: var(--black); }
.page-nav.is-open a::after { background: var(--black); }

/* respect reduced-motion: kill hover/scroll motion entirely */
@media (prefers-reduced-motion: reduce) {
  * { animation: none !important; transition: none !important; scroll-behavior: auto !important; }
  .work-card:hover img, .work-card:hover video { transform: none; }
  .work-index-card:hover .work-out { transform: none; }
}

/* ---------- link-or-upload toggle (careers) ---------- */
.lou-tabs { display: flex; gap: 20px; margin-bottom: 10px; }
.lou-tab {
  font-family: var(--sans);
  font-size: var(--t-body);
  color: var(--grey);
  padding-bottom: 4px;
  border-bottom: 1px solid transparent;
  transition: color .2s, border-color .2s;
}
.lou-tab.is-active { color: var(--black); border-bottom-color: var(--black); }
.lou-tab:hover { opacity: .6; }

/* ---------- contact conditional questions ---------- */
.reveal-q[hidden] { display: none; }
.reveal-q {
  animation: qIn .35s cubic-bezier(.22,.61,.36,1);
}
@keyframes qIn {
  from { opacity: 0; transform: translateY(-6px); }
  to   { opacity: 1; transform: none; }
}

/* ---------- work page filter — a sub-nav bar that sticks under the header on scroll ----------

   The seam. Parked at exactly --header-h the bar left a hairline strip of
   page showing between it and the header, because the header's measured
   height and the sticky offset round independently on a fractional device
   pixel ratio. One pixel up closes it, and the box-shadow paints that pixel
   back in the page colour so nothing is clipped.

   The other half of the problem was two hairlines: the header draws one when
   it is scrolled, the bar draws another when it is stuck, and stacked
   together they read as a gap rather than as an edge. While the bar is
   stuck the header gives its rule up, so the pair reads as one bar with a
   single line under it. */
.work-filter-bar {
  position: sticky;
  top: calc(var(--header-h, 71px) - 1px);
  z-index: 50;
  /* Breaks out by the page's own padding, not 100vw. 100vw counts the
     scrollbar, so the bar was pulled about 8px further left than the grid
     underneath it and the filters never lined up with the work. */
  margin-inline: calc(var(--pad) * -1);
  padding: 9px var(--pad);
  margin-bottom: clamp(34px, 4vw, 56px);
  border-bottom: 1px solid transparent;
  background: transparent;
  transition: background .3s ease, border-color .3s ease;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 18px;
}
.work-filter-bar.is-stuck {
  background: var(--white);
  border-bottom-color: var(--line);
  box-shadow: 0 -2px 0 var(--white);   /* fills the pixel the -1px offset exposes */
}
body:has(.work-filter-bar.is-stuck) .page-header.scrolled,
body:has(.work-filter-bar.is-stuck) .site-header.scrolled {
  border-bottom-color: transparent;
}
.work-filter {
  display: flex;
  gap: 24px;
  flex-wrap: wrap;
}
/* ---------- brand discovery form ---------- */
.bd-page { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); gap: var(--col-gap); padding: clamp(48px, 8vw, 120px) 0 clamp(60px, 8vw, 120px); align-items: start; }
.bd-intro { position: sticky; top: clamp(40px, 9vw, 110px); }
.bd-intro h1 { font-family: var(--sans); font-weight: 400; font-size: var(--t-display); line-height: 1.02; letter-spacing: 0; }
.bd-progress { margin-top: 28px; }
.bd-progress b { color: var(--black); }
.bd-step { display: none; }
.bd-step.is-active { display: block; animation: bdIn .4s ease both; }
@keyframes bdIn { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }
.bd-step .field { margin-bottom: 30px; }
/* The open-roles list is gone from the careers page: the roles live in the
   Opening dropdown, filled from hdwOS, so there is one list instead of two.
   Its nine rules went with it rather than sitting here unused. */

/* the brand discovery form runs one question at a time, full width */
.bd-page .form-grid { grid-template-columns: 1fr; max-width: none; }
.bd-q { display: block; font-family: var(--sans); font-size: var(--t-body); color: var(--black); margin-bottom: 6px; }
.bd-hint { font-family: var(--sans); font-size: var(--t-small); color: var(--grey); line-height: 1.5; margin-top: 8px; }
.bd-step input[type="text"], .bd-step input[type="email"], .bd-step textarea {
  width: 100%; font-family: var(--sans); font-size: var(--t-body); color: var(--black);
  background: var(--white); border: 1px solid var(--field); border-radius: 0; padding: 13px 14px; transition: border-color .2s;
}
.bd-step textarea { resize: vertical; min-height: 110px; }
.bd-step input:focus, .bd-step textarea:focus { border-color: var(--black); }
.bd-step input:focus:not(:focus-visible), .bd-step textarea:focus:not(:focus-visible) { outline: none; }
/* segmented scale */
.scale { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px; }
.scale-opt { position: relative; }
.scale-opt input { position: absolute; opacity: 0; pointer-events: none; }
.scale-opt span {
  display: block; font-family: var(--sans); font-size: var(--t-small); color: var(--black);
  border: 1px solid var(--field); padding: 9px 13px; cursor: pointer; transition: background .15s, color .15s, border-color .15s; white-space: nowrap;
}
.scale-opt:hover span { border-color: var(--black); }
.scale-opt input:checked + span { background: var(--black); border-color: var(--black); color: var(--white); }
.scale-opt input:focus-visible + span { outline: 2px solid currentColor; outline-offset: 2px; }
/* slider scale (0-5 likert) */
.bd-scale { margin-top: 16px; }
.bd-range { -webkit-appearance: none; appearance: none; width: 100%; height: 2px; background: var(--field); outline: none; margin: 14px 0 14px; cursor: pointer; }
.bd-range::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 18px; height: 18px; background: var(--black); border: 0; border-radius: 0; cursor: pointer; }
.bd-range::-moz-range-thumb { width: 18px; height: 18px; background: var(--black); border: 0; border-radius: 0; cursor: pointer; }
.bd-range:focus-visible { outline: 2px solid currentColor; outline-offset: 6px; }
.bd-scale-ticks { display: flex; justify-content: space-between; gap: 6px; }
.bd-scale-ticks span { flex: 1; text-align: center; font-family: var(--sans); font-size: var(--t-small); color: var(--grey); transition: color .15s; }
.bd-scale-ticks span:first-child { text-align: left; }
.bd-scale-ticks span:last-child { text-align: right; }
.bd-scale-ticks span.on { color: var(--black); }
/* checkbox grid */
.bd-checks { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px 18px; margin-top: 12px; }
.bd-checks label { display: flex; align-items: center; gap: 9px; font-family: var(--sans); font-size: var(--t-body); color: var(--black); cursor: pointer; }
.bd-checks input { accent-color: var(--black); width: auto; }
/* nav buttons */
.bd-nav { display: flex; justify-content: space-between; align-items: center; gap: 16px; margin-top: 40px; }
.bd-back { font-family: var(--sans); font-size: var(--t-body); color: var(--black); transition: opacity .2s; }
.bd-back:hover { opacity: .6; }
.bd-back[hidden] { visibility: hidden; }
.bd-next { font-family: var(--sans); font-size: var(--t-body); color: var(--white); background: var(--black); border: 0; padding: 13px 30px; cursor: pointer; transition: opacity .2s; margin-left: auto; }
.bd-next:hover { opacity: .85; }
.bd-note { font-family: var(--sans); font-size: var(--t-small); color: var(--grey); margin-top: 16px; }
@media (max-width: 900px) {
  .bd-page { grid-template-columns: 1fr; gap: 30px; padding-top: clamp(90px, 22vw, 120px); }
  .bd-intro { position: static; }
}

/* ---------- branded dropdown (replaces native select) ---------- */
.hdw-dd { position: relative; }
.hdw-dd select { position: absolute; inset: 0; opacity: 0; pointer-events: none; }   /* kept for value/validation/focus */
.hdw-dd-btn {
  width: 100%; text-align: left; display: flex; justify-content: space-between; align-items: center; gap: 12px;
  font-family: var(--sans); font-size: var(--t-body); color: var(--black);
  background: var(--white); border: 1px solid var(--field); border-radius: 0; padding: 14px 15px; cursor: pointer;
  transition: border-color .2s;
}
.hdw-dd.open .hdw-dd-btn { border-color: var(--black); }
/* :focus-visible keeps its ring here. It used to be the one selector that
   removed it, which meant the one control on the form that is not a native
   field was also the one with nothing to show for being focused. */
.hdw-dd-btn:focus-visible { border-color: var(--black); }
.hdw-dd-btn.placeholder { color: var(--grey-soft); }
.hdw-dd-btn::after { content: ''; width: 7px; height: 7px; border-right: 1.5px solid currentColor; border-bottom: 1.5px solid currentColor; transform: rotate(45deg) translateY(-2px); transition: transform .2s; flex: none; }
.hdw-dd.open .hdw-dd-btn::after { transform: rotate(-135deg) translateY(-1px); }
.hdw-dd-list {
  position: absolute; top: calc(100% - 1px); left: 0; right: 0; z-index: 40;
  background: var(--white); border: 1px solid var(--black); max-height: 280px; overflow: auto;
}
.hdw-dd-opt {
  display: block; width: 100%; text-align: left; font-family: var(--sans); font-size: var(--t-body);
  color: var(--black); padding: 12px 15px; cursor: pointer; transition: background .12s, color .12s;
}
.hdw-dd-opt:hover, .hdw-dd-opt.on { background: var(--card); color: var(--black); }

/* The View control lives in the page rather than in the sticky bar, because
   the bar follows you down the whole grid and only the filters need to. */
.works-toolbar {
  display: flex;
  justify-content: flex-end;
  margin-bottom: clamp(14px, 2vw, 22px);
}
/* sizing and states are shared with .cs-view-toggle, further down this file */

.filter-tab {
  font-family: var(--sans);
  font-size: var(--t-body);
  color: var(--grey);
  position: relative;
  transition: color .2s, opacity .2s;
}
.filter-tab.is-active { color: var(--black); }
.filter-tab:hover { opacity: .6; }

/* animated filtering: the grid fades + lifts away, the new set slides up */
.works-list .work-index-card { transition: opacity .5s ease, transform .6s cubic-bezier(.22,.61,.36,1); }
.works-list.is-swapping .work-index-card { opacity: 0; transform: translateY(20px); }
@media (max-width: 900px) {
  .work-filter { justify-content: flex-start; gap: 18px; }
  .work-filter-bar { margin-top: 4px; margin-bottom: clamp(30px, 8vw, 44px); padding-block: 11px; }
  /* tighter, more balanced header on phone */
  .site-header, .page-header { padding: 13px var(--pad); }
}

/* ---------- contact: multi-select + option lists ---------- */
.checkbox-col, .radio-col { display: flex; flex-direction: column; gap: 14px; margin-top: 8px; }
/* checkbox absolutely placed so the long label text wraps instead of overflowing */
.checkbox-col label, .radio-col label {
  display: block;
  position: relative;
  padding-left: 30px;
  font-family: var(--sans); font-size: var(--t-body); line-height: 1.45; color: var(--black); cursor: pointer;
}
.checkbox-col label input, .radio-col label input {
  position: absolute; left: 0; top: 3px;
  width: auto;
  accent-color: var(--black);
}
.radio-row label input { width: auto; accent-color: var(--black); }

/* Option 3 — refined radio list for budget / timeline groups */
.radio-col { gap: 0 !important; margin-top: 10px; }
.radio-col label { padding: 13px 0 13px 30px; border-top: 1px solid var(--line); line-height: 1.5; }
.radio-col label:last-child { border-bottom: 1px solid var(--line); }
.radio-col label input { top: 15px; }
.rc-price { display: inline-block; min-width: 175px; padding-right: 20px; vertical-align: top; }
.rc-incl { display: inline-block; font-size: var(--t-small); color: var(--grey); line-height: 1.5; vertical-align: top; max-width: 320px; }
@media (max-width: 600px) {
  .rc-price { display: block; min-width: 0; margin-bottom: 2px; }
  .rc-incl { display: block; max-width: none; }
}

/* contact: capabilities listing (left column) */
.cap-listing { margin-top: 52px; }
.cap-listing-label { font-family: var(--sans); font-size: var(--t-body); color: var(--grey); display: block; margin-bottom: 16px; }
.cap-listing ul { display: flex; flex-direction: column; gap: 3px; }
.cap-listing li { font-family: var(--sans); font-size: var(--t-body); letter-spacing: var(--body-track); line-height: 1.25; }

/* ---------- case study gallery: single cycling View button (right) ---------- */
/* Only used on a case study with no chapters, where there is no heading to sit
   on. Kept tight to the gallery so it reads as belonging to it. */
.cs-toolbar {
  display: flex;
  justify-content: flex-end;
  margin-bottom: 10px;
}
/* The control lives on the first chapter heading. auto margin puts it at the
   far end of that line, at every width now: the reason it used to give that up
   on a phone was that "View 3" is a wide thing to push to the far end of a
   short line, and it kept wrapping onto a line of its own. A 24px mark does
   not have that problem. */
.cs-block-title.has-view .cs-view-toggle { margin-left: auto; }

/* ---------- the View mark, on a case study and on the work index ----------
   Both are the same control and they are drawn by the same function in main.js,
   so they are styled once here.

   44px is the box, 24px is the mark. The difference is the part a thumb finds
   and the eye never sees. The negative block margin keeps that box from
   pushing the heading's line apart: the control is on the line, not in it. */
.cs-view-toggle,
.work-view-toggle {
  width: 44px;
  height: 44px;
  padding: 0;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  align-self: center;
  margin-block: -12px;
  /* The box is 44 and the mark inside it is 24, so a box whose edge sits on the
     right rail puts the mark 10px inside it, floating short of every other
     thing on the page. The box is pulled out by that difference: the mark ends
     on the rail, and the 44px a thumb needs is still there, hanging over the
     margin where there is nothing to hit anyway. */
  margin-right: -10px;
  color: var(--grey);
  transition: color .2s ease, opacity .2s ease;
  -webkit-tap-highlight-color: transparent;
}
.cs-view-toggle svg,
.work-view-toggle svg { display: block; }
.cs-view-toggle:hover,
.cs-view-toggle:focus-visible,
.work-view-toggle:hover,
.work-view-toggle:focus-visible { color: var(--black); opacity: 1; }
/* Nothing draws the mark but JavaScript, so with JavaScript off there is no
   control rather than a dead one. The grid still has its own sensible count. */
.cs-view-toggle:empty,
.work-view-toggle:empty { display: none; }

/* ---------- contact form: single column (matches the real site) ---------- */
.contact-grid .form-grid { grid-template-columns: repeat(2, 1fr); max-width: none; }
.checkbox-col label, .radio-col label { min-width: 0; }
.checkbox-col label span, .radio-col label span { min-width: 0; }

/* The honeypot input. Off screen rather than display:none, because a bot that
   is worth trapping checks for display:none and skips the field. It is also
   aria-hidden and out of the tab order in the markup, so nobody using a screen
   reader or a keyboard ever meets it. */
.hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

/* ---------- form groups ----------
   One flat run of eleven fields read as a wall. Three named steps instead, each
   with a rule and a number, so it is obvious how much is left. */
.form-group {
  grid-column: 1 / -1;
  display: grid;
  grid-template-columns: subgrid;
  gap: 28px var(--gap);
  padding-top: clamp(26px, 3vw, 40px);
  border-top: 1px solid var(--line);
}
.form-group:first-of-type { padding-top: 0; border-top: 0; }
.form-group > .form-group-head {
  grid-column: 1 / -1;
  display: flex;
  align-items: baseline;
  gap: 12px;
  margin-bottom: -6px;
}
.form-group-head .n { font-size: var(--t-small); }
.form-group-head h2 { font-weight: 400; font-size: var(--t-body); letter-spacing: var(--body-track); }
@supports not (grid-template-columns: subgrid) {
  .form-group { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 900px) {
  .contact-grid .form-grid, .form-group { grid-template-columns: 1fr; }
  .contact-info { position: static; }
}

/* ---------- floating corner toolbar: the theme control ----------

   A 44px box carrying a 17px mark, the same arrangement the View control uses:
   the box is what a thumb finds and the mark is what the eye sees.

   Which means the mark sits 13.5px inside its own box, so the toolbar is pulled
   out by exactly that. The marks land on the same rail every other thing on the
   page ends on, and the 44px hangs over the margin, where there is nothing else
   to hit. 4px out is as wrong as 40px out: it reads as not quite aligned rather
   than as a margin. */
.fixed-toolbar {
  position: fixed;
  right: calc(var(--pad) - 13.5px);
  bottom: 4px;
  z-index: 120;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 0;
}
/* These used to be white with a difference blend, on the theory that white
   inverted against a light page reads black. It does, until the toolbar sits
   over anything that is not the page background, and a case study is one long
   picture. Over a mid photograph the difference of white is a mid colour and
   the two controls disappear; over the light theme they stayed pale grey.
   So they take the page's own ink instead, which is black in light and off
   white in dark, and is right in both without depending on what is behind. */
.theme-toggle {
  width: 44px;
  height: 44px;
  color: var(--black);
  background: none;
  border: 0;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: opacity .2s;
  -webkit-tap-highlight-color: transparent;
}
.theme-toggle svg { display: block; }
.theme-toggle:hover { opacity: .6; }

/* cursor name tag (Work grid hover) */
.cursor-tag {
  position: fixed; z-index: 130; pointer-events: none;
  background: var(--black); color: var(--white);
  padding: 5px 9px 4px; white-space: nowrap;
  transform: translate(12px, 12px) scale(.9); transform-origin: top left;
  opacity: 0; transition: opacity .15s ease, transform .15s ease;
}
.cursor-tag.on { opacity: 1; transform: translate(14px, 14px) scale(1); }
@media (hover: none) { .cursor-tag { display: none; } }

/* looping word (intro) */
.text-loop { display: inline-block; color: var(--black); will-change: transform, opacity, filter;
  transition: transform .4s cubic-bezier(.22,.61,.36,1), opacity .4s ease, filter .4s ease; }
html { background: var(--white); transition: background .3s ease; }

/* ---------- case study: dual-track layout (sticky meta left, visuals right) ---------- */
.cs-dual {
  display: grid;
  grid-template-columns: minmax(0, 3fr) minmax(0, 9fr);
  gap: var(--col-gap);
  padding: clamp(48px, 7vw, 100px) 0 clamp(40px, 5vw, 70px);
}
.cs-side { align-self: start; position: sticky; top: 92px; }
/* Four rows, four rules, on every case study. The sidebar is four short facts,
   and ruling each one off made it look like a table of contents. */
.cs-side-row { padding: 0 0 22px; }
.cs-side-row:first-child { padding-top: 0; }
/* ---------- the sidebar's own hierarchy ----------
   Two tiers, not four. Mono label above, everything it introduces at body size
   below. Date, Disciplines, Deliverables, Campaigns and the Instagram link are
   one list of facts and they now look like one list.

     label   --t-label-sm, softest grey   the word DATE, not the date
     value   --t-body                     the date, the disciplines, the lot

   Deliverables was dropped to the fine-print size and greyed on the argument
   that a long comma list should read as texture. It read as an afterthought
   instead: one row quieter than the four around it, for a reason no reader can
   see. It is the same fact as the rest and it is set the same. */
.cs-side-row .k {
  display: block;
  margin-bottom: 9px;
  font-size: var(--t-label-sm);
  color: var(--grey-soft);
}
.cs-side-row .v { font-family: var(--sans); font-size: var(--t-body); line-height: 1.5; }
.cs-side-deliverables .v { line-height: 1.6; }
/* the client's own Instagram: the only link in the sidebar, so it says so */
.cs-side-row .v a { border-bottom: 1px solid currentColor; }
.cs-side-next { margin-top: 28px; }
.cs-side-next .k { display: block; margin-bottom: 9px; }
.cs-side-next a { font-family: var(--sans); font-size: var(--t-body); letter-spacing: var(--body-track); }
/* wrap is the fallback, not the plan: "Ongoing · since Jan 2023" fits beside a
   discipline name at every width the sidebar is ever given. If a longer name
   ever pushes it out, the date drops to its own line rather than squeezing. */
.cs-disc { display: flex; justify-content: space-between; align-items: baseline; gap: 2px 12px; padding: 7px 0; flex-wrap: wrap; }
.cs-disc span { font-family: var(--sans); font-size: var(--t-body); line-height: 1.25; transition: color .25s; }
.cs-disc em { font-style: normal; white-space: nowrap; }
/* scroll-spy: dim disciplines, light the one whose work is on screen */
a.cs-disc { opacity: .4; transition: opacity .25s; }
a.cs-disc:hover { opacity: .75; }
a.cs-disc.is-active { opacity: 1; }
a.cs-disc.is-active span { color: var(--black); }

/* per-discipline content blocks (multi-discipline case studies) */
.cs-block { scroll-margin-top: 96px; margin-bottom: clamp(48px, 6vw, 90px); }
.cs-block:last-child { margin-bottom: 0; }
.cs-block-title {
  font-family: var(--sans);
  font-weight: 400;
  font-size: var(--t-heading);
  line-height: 1;
  letter-spacing: 0;
  margin-bottom: 24px;
  display: flex;
  align-items: baseline;
  gap: 14px;
  flex-wrap: wrap;
}
.cs-block-title em { font-style: normal; }

.cs-main .cs-summary { margin-bottom: clamp(34px, 4vw, 56px); }
/* deeper case-study chapters (The challenge lead, then approach / outcome) */
/* ---------- case study video ----------
   The client's own reels, transcoded for web. Controls rather than autoplay:
   these have sound design, and a page that starts making noise on its own is
   a page people close. Poster frames mean the grid looks finished before a
   single byte of video is fetched. */
.cs-videos { margin-bottom: clamp(44px, 6vw, 80px); }
.cs-videos-k {
  display: block;
  margin-bottom: clamp(14px, 2vw, 20px);
}
/* Masonry columns, same as .cs-gallery. A fixed grid left ragged gaps because
   the reels mix 9:16 and 16:9, and forcing one aspect ratio would crop framing
   the client paid to get right. Columns let each clip keep its own shape. */
.cs-videos-grid {
  display: block;
  column-gap: var(--gap);
  column-count: 3;
}
.cs-videos-grid video {
  width: 100%;
  height: auto;
  display: block;
  margin: 0 0 var(--gap);
  break-inside: avoid;
  background: var(--card);
}
@media (max-width: 900px) { .cs-videos-grid { column-count: 2; } }
@media (max-width: 600px) { .cs-videos-grid { column-count: 1; } }

/* ---------- selected posts ----------
   The posts Swayam picked, mirrored from hdwOS rather than pulled from
   Instagram. Square by definition since that is how the feed crops them.
   Deliberately smaller than .cs-shot: these are references, the photography
   above is the work. */
.cs-posts { margin-bottom: clamp(44px, 6vw, 80px); }
.cs-posts-k {
  display: block;
  margin-bottom: clamp(14px, 2vw, 20px);
}
.cs-posts-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
  gap: clamp(8px, 1.2vw, 14px);
}
.cs-post { display: block; background: var(--card); }
.cs-post img { width: 100%; aspect-ratio: 1; object-fit: cover; display: block; }
.cs-post:hover { opacity: 1; }
.cs-post:hover img { opacity: .72; transition: opacity .2s; }

/* ---------- one metric, as a sentence ----------
   This used to be three giant stacked numbers. They read like a client
   dashboard, made every project look identical, and gave the biggest type on
   the page to a follower count. One number that means something, set at
   reading size, inside a sentence that says what it was. */
.cs-metric {
  margin-bottom: clamp(44px, 6vw, 80px);
  padding-top: clamp(24px, 3vw, 36px);
  border-top: 1px solid var(--line);
}
.cs-metric p { font-family: var(--sans); font-size: var(--t-body); line-height: 1.7; max-width: 54ch; color: var(--black); }
/* Marks the number, nothing else. */
.cs-metric b { font-weight: 400; box-shadow: inset 0 -2px 0 currentColor; }

.cs-story { display: grid; gap: clamp(26px, 3.5vw, 44px); margin-bottom: clamp(44px, 6vw, 80px); }
.cs-chapter { display: grid; gap: 9px; }
.cs-chapter-k, .cs-summary-k { display: block; }
/* The opening paragraph is the whole project in two or three sentences: where
   the client was, what we did, where it got to. The approach and the outcome
   sit behind Read more as the long version, so somebody scanning the work is
   never made to read all of it to find out what happened. */
.cs-summary-k { display: block; margin-bottom: 14px; }
.cs-chapter p { font-family: var(--sans); font-size: var(--t-body); line-height: 1.7; max-width: 64ch; color: var(--black); }

/* ---------- logo cloud (brands strip, auto-scroll marquee) ---------- */
.logo-cloud { padding: clamp(44px, 6vw, 84px) 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
/* The fade sits here rather than on the section. On the section it also caught
   the label, which starts 20px from the left and so fell inside the transparent
   8% and vanished. */
.lc-viewport { overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); }
.lc-label { display: block; padding: 0 var(--pad); margin-bottom: clamp(26px, 3vw, 44px); }
/* No CSS animation. Changing animation-duration on hover made the track jump,
   because the browser recomputes position as a fraction of the new duration
   against the same start time. js/main.js moves it instead and eases the speed. */
/* No side padding. The loop distance is one set plus one gap, and padding
   on the track is neither, so it showed up as a stutter at every seam. The
   gap does the spacing on its own. */
.lc-track { display: flex; align-items: center; gap: clamp(50px, 6vw, 100px); width: max-content; padding: 0; will-change: transform; }
/* These are links to the case study now, not decoration, so they have to look
   clickable. The hover preview was already here doing half the job. */
.lc-item { font-family: var(--sans); font-size: var(--t-heading); letter-spacing: 0; color: var(--black); opacity: .62; white-space: nowrap; cursor: pointer; transition: opacity .3s; }
.lc-item:hover { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .lc-track { flex-wrap: wrap; transform: none !important; } }
/* floating brand image preview on hover */
.lc-preview { position: fixed; z-index: 130; pointer-events: none; opacity: 0; transform: translate(16px, -50%) scale(.96); transition: opacity .2s ease, transform .2s ease; }
.lc-preview.on { opacity: 1; transform: translate(18px, -50%) scale(1); }
/* No fixed height and no object-fit. A locked 220x150 box cropped every
   portrait shot in half — the preview now keeps whatever shape the image is
   and only caps how big it can get. */
.lc-preview img { display: block; width: auto; height: auto; max-width: 260px; max-height: 300px; }
@media (max-width: 900px) { .lc-preview { display: none; } }
.cs-main .cs-summary p {
  font-family: var(--sans);
  font-size: var(--t-heading);
  line-height: 1.18;
  letter-spacing: 0;
  margin-bottom: 22px;
  max-width: none;
}
@media (max-width: 900px) {
  .cs-dual { grid-template-columns: 1fr; gap: 30px; padding-top: clamp(40px,8vw,70px); }
  .cs-side { position: static; }
}

/* ---------- sections on the twelve ----------

   A 5fr/7fr split and a span of 5 out of 12 are not the same width. The split
   divides what is left after one gutter; the span divides what is left after
   eleven. At 1280 that is 490px against 479.3px. Both are internally
   consistent, but only the span lands on the same track as everything else,
   which is the whole point of having a grid.

   So above the collapse these six sit on the real twelve tracks and their
   children span columns. Each one has exactly two children, checked across
   all 37 instances on the site, so first and second child is safe here.

   Below 900 they are single column already, further up this file, and those
   rules are left alone. Between 900 and 901, which only a fractional viewport
   can hit, the 5fr/7fr further up still applies as a sane fallback. */
@media (min-width: 901px) {
  .qa-row, .people-grid, .svc-row, .contact-grid, .bd-page, .cs-dual {
    grid-template-columns: repeat(var(--grid-cols), minmax(0, 1fr));
    gap: var(--col-gap);
  }
  .qa-row       > :first-child,
  .people-grid  > :first-child,
  .svc-row      > :first-child,
  .contact-grid > :first-child,
  .bd-page      > :first-child  { grid-column: span 5; }

  .qa-row       > :nth-child(2),
  .people-grid  > :nth-child(2),
  .svc-row      > :nth-child(2),
  .contact-grid > :nth-child(2),
  .bd-page      > :nth-child(2) { grid-column: span 7; }

  /* The case study sidebar is narrower than a half: three columns, not five. */
  .cs-dual > :first-child  { grid-column: span 3; }
  .cs-dual > :nth-child(2) { grid-column: span 9; }
}

/* Careers has no facts column beside the form any more. Without this the form
   becomes the grid's first child and takes the five columns the sidebar used
   to have. It keeps the seven it was always drawn in. */
@media (min-width: 901px) {
  .contact-grid.contact-grid--solo > :first-child { grid-column: 6 / -1; }
}
