/* ==========================================================================
   Nashine Architects
   Two colours. One weight. Photography is the only colour in the system.
   Type scale: Major Third (1.25) from a 17px base.
   ========================================================================== */

:root{
  /* the only two colours, plus one accent reserved for the cursor */
  --ink:#000000;
  --paper:#fffffc;
  --cursor-accent:#c4432c;
  /* rgb components, so the cursor can be painted at a partial alpha */
  --ink-rgb:0,0,0;
  --cursor-accent-rgb:196,67,44;
  /* Alpha, not transparency: .95 = 5% transparent. One number for all five
     circles — the two trailing ones take a fraction of it, so the tail still
     fades away behind the leader. */
  --cursor-alpha:.95;

  /* spacing scale — 5 8 10 11 13 17 25 50 59 84 101 134 168 174 */
  --s-5:5px;   --s-8:8px;   --s-10:10px; --s-11:11px; --s-13:13px;
  --s-17:17px; --s-25:25px; --s-50:50px; --s-59:59px; --s-84:84px;
  --s-101:101px; --s-134:134px; --s-168:168px; --s-174:174px;

  --wrap:1440px;
  --gut:clamp(25px,3.4vw,50px);
  --measure:560px;

  --f:'Outfit','Inter','Geist','General Sans',-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;
}

*,*::before,*::after{box-sizing:border-box}
/* The browser's own rule for [hidden] is weak enough that any explicit
   display later in this file beats it. Scripts here hide things by setting
   .hidden, so it has to actually hold. */
[hidden]{display:none!important}
html{
  -webkit-text-size-adjust:100%;
  scroll-padding-top:88px;   /* keyboard focus clears the sticky nav strip */
}

body{
  margin:0;
  background:var(--paper);
  color:var(--ink);
  font-family:var(--f);
  font-weight:400;
  font-size:20px;
  line-height:1.5;
  -webkit-font-smoothing:antialiased;
  overflow-x:hidden;
}

img{display:block;max-width:100%;height:auto;border:0;border-radius:0}
a{color:inherit;text-decoration:none}
button{font:inherit;color:inherit;background:none;border:0;cursor:pointer;border-radius:0}
h1,h2,h3,h4,p,ul,ol,figure,figcaption,blockquote{margin:0}
ul{list-style:none;padding:0}
::selection{background:var(--ink);color:var(--paper)}

/* --------------------------------------------------------------- type scale
   caption      13 / 1.5
   body-sm      17 / 1.5
   body         20 / 1.5
   subheading   25 / 1.15
   heading-sm   34 / 1.1  / -0.01em
   heading      50 / 1.06 / -0.012em
   heading-lg  109 / 1.0  / -0.03em
   display     134 / 1.0  / -0.03em
   display-lg  166 / 0.8  / -0.05em
   display-xl  201 / 0.8  / -0.07em
   Every role is weight 400. Weight 700 is reserved for the logo.
   -------------------------------------------------------------------------- */

.t-caption   {font-size:13px;line-height:1.5}
.t-body-sm   {font-size:17px;line-height:1.5}
.t-body      {font-size:20px;line-height:1.5}
.t-sub       {font-size:clamp(21px,2.2vw,25px);line-height:1.15}

.t-heading-sm{font-size:clamp(26px,3.4vw,34px);line-height:1.1;letter-spacing:-.01em}
.t-heading   {font-size:clamp(32px,5vw,50px);line-height:1.06;letter-spacing:-.012em}
.t-heading-lg{font-size:clamp(38px,7.4vw,109px);line-height:1;letter-spacing:-.03em}
.t-display   {font-size:clamp(42px,9vw,134px);line-height:1;letter-spacing:-.03em}
.t-display-lg{font-size:clamp(44px,11vw,166px);line-height:.85;letter-spacing:-.05em}
.t-display-xl{font-size:clamp(46px,13.4vw,201px);line-height:.85;letter-spacing:-.07em}

h1,h2,h3,h4{font-weight:400}
.meta{font-size:17px;line-height:1.5}
.meta-sm{font-size:13px;line-height:1.5}

/* ------------------------------------------------------------------ layout
   One left edge for everything. One vertical scale. Section padding lives
   here, never inline, so the rhythm cannot drift page to page.
   -------------------------------------------------------------------------- */
.wrap{max-width:var(--wrap);margin:0 auto;padding-inline:var(--gut)}

.sec{padding-block:var(--s-84)}
.sec-lg{padding-block:var(--s-134)}
.sec-tight{padding-block:var(--s-50)}
.sec + .sec{padding-top:0}          /* consecutive sections don't double up */

/* ------------------------------------------------------------------ phones
   The vertical scale was drawn for a desktop screen. On a phone the same
   figures leave a third of every screen empty before anything is read, so
   the rhythm steps down rather than being carried over wholesale.
   -------------------------------------------------------------------------- */
@media(max-width:760px){
  .sec{padding-block:var(--s-50)}
  .sec-lg{padding-block:var(--s-59)}
  .sec-tight{padding-block:var(--s-25)}
  .duo{gap:var(--s-25)}
  .t-body{font-size:18px}
  .t-sub{font-size:19px}
  .headline,.headline--wide{max-width:none}
}
@media(max-width:480px){
  .t-body{font-size:17px}
  .sec-lg{padding-block:var(--s-50)}
}

/* a text column, flush with the headings above it */
.block{max-width:var(--measure)}
.block > * + *{margin-top:var(--s-25)}
.block--wide{max-width:760px}

/* headline, then content beside it rather than beneath it */
.duo{display:grid;gap:var(--s-50);align-items:start}
@media(min-width:1000px){
  .duo{grid-template-columns:minmax(0,.85fr) minmax(0,1.15fr);gap:var(--s-84)}
}
/* Headings sit beside their text, so they run at a size that keeps the
   authored line breaks intact inside a half-width column. */
.duo__head .t-heading-lg{font-size:clamp(32px,4.4vw,62px);line-height:1.02}
.duo__head .t-display{font-size:clamp(34px,4.8vw,70px);line-height:1.02}
.duo__head .t-heading{font-size:clamp(28px,3.4vw,46px)}
.duo__body > * + *{margin-top:var(--s-25)}
/* Body text in a full-width duo ran to about 100 characters a line at
   iPad landscape and wider, past comfortable reading. Headings, lists
   and media are left alone; only running prose is capped. */
@media(min-width:1000px){
  .duo__body > p.t-body,.duo__body > p.t-body-sm{max-width:74ch}
}

/* stacked headline block for section openings that carry no body beside them */
.headline{max-width:16ch}
.headline--wide{max-width:26ch}

/* ------------------------------------------------------- rotating cuff hero
   One cylinder of photographs turning on its axis, in place of the three
   drifting bands. The component paints into a shadow root and works its own
   geometry out from the box it is given, so all it needs from us is a height
   and a ground that matches the page.

   The height is deliberately generous: the band reads as a curve only when it
   has room, and at less than about 320px it flattens into a strip.
   -------------------------------------------------------------------------- */
rotating-cuff{
  display:block;width:100%;
  height:clamp(340px, 46vw, 780px);
  background:var(--paper);
}
@media(max-width:760px){
  rotating-cuff{height:clamp(320px, 62vw, 440px)}
}
/* Before the component upgrades, the element is an unknown inline tag with no
   height — this keeps the page from jumping when it does. */
html:not(.js) rotating-cuff{display:none}

/* ----------------------------------------------------------- marquee hero
   Three bands of photography drifting sideways at different speeds, the
   middle one against the other two. Each track holds its images twice over,
   so translating exactly -50% returns to an identical frame and the loop is
   seamless. Pure CSS, so reduced-motion stops it for free.
   -------------------------------------------------------------------------- */
.marq{width:100%;overflow:hidden;background:var(--paper)}
.marq-row{overflow:hidden;height:clamp(132px,19vw,268px)}
.marq-row + .marq-row{margin-top:2px}
.marq-track{
  display:flex;gap:2px;height:100%;width:max-content;
  will-change:transform;
  animation:marq-run linear infinite;
}
.marq-row:nth-child(1) .marq-track{animation-duration:78s}
.marq-row:nth-child(2) .marq-track{animation-duration:96s;animation-direction:reverse}
.marq-row:nth-child(3) .marq-track{animation-duration:64s}
/* Each cell states its own proportion, so the track has its full width the
   instant the page lays out — before a single photograph has arrived. Sizing
   the cells from the images instead is what made the bands stall, jump and
   sometimes show nothing at all on a phone. */
.marq-track figure{
  margin:0;height:100%;aspect-ratio:3/2;width:auto;flex:0 0 auto;
  overflow:hidden;background:var(--ink);
}
.marq-track img{
  width:100%;height:100%;max-width:none;object-fit:cover;
  transition:transform .6s cubic-bezier(.16,.84,.44,1);
}
.marq-track figure:hover img{transform:scale(1.05)}
@keyframes marq-run{from{transform:translate3d(0,0,0)}to{transform:translate3d(-50%,0,0)}}
/* On a phone the bands run at half the tile count. Hiding every other cell
   removes the same positions from both halves of the doubled run, so the
   -50% loop still lands on an identical frame. */
@media(max-width:760px){
  .marq-track figure:nth-child(odd){display:none}
  .marq-row:nth-child(1) .marq-track{animation-duration:46s}
  .marq-row:nth-child(2) .marq-track{animation-duration:56s}
  .marq-row:nth-child(3) .marq-track{animation-duration:38s}
}
@media(prefers-reduced-motion:reduce){
  .marq-track{animation:none}
}

/* full-bleed: reserved for page heroes only */
.bleed{width:100%;margin:0;overflow:hidden}
.bleed img{
  width:100%;
  height:clamp(280px,64vw,780px);
  object-fit:cover;
  object-position:center;
  transform:scale(1.14);
}
.bleed--tall img{height:clamp(340px,76vw,880px)}

/* ------------------------------------------------------------- navigation */
.nav-bar{
  position:sticky;top:0;z-index:60;
  background:var(--ink);color:var(--paper);
  padding:var(--s-17) var(--gut);
}
.nav-in{
  max-width:var(--wrap);margin:0 auto;
  display:flex;align-items:center;justify-content:space-between;gap:var(--s-25);
}
.nav-logo{display:flex;align-items:center;gap:var(--s-11);flex:0 0 auto}
.nav-logo img{height:22px;width:auto}
.nav-logo span{font-size:17px;font-weight:700;line-height:1}
.nav-links{display:flex;align-items:center;gap:var(--s-25)}
.nav-links a{
  font-size:17px;font-weight:400;line-height:1;
  padding-bottom:3px;border-bottom:1px solid transparent;
}
.nav-links a:hover{border-bottom-color:var(--paper)}
.nav-links a[aria-current="page"]{border-bottom:1px solid var(--paper)}

.nav-toggle{display:none;width:34px;height:26px;flex:0 0 auto;padding:0;position:relative;background:transparent;border:0}
/* The bars stay 34x26 so the header is unchanged; the pseudo-element
   below gives the thumb a 44x44 target centred on them, which is the
   accepted minimum and this is the primary navigation control on a
   phone. Growing the button itself would have moved the header. */
.nav-toggle::after{content:'';position:absolute;top:50%;left:50%;width:44px;height:44px;transform:translate(-50%,-50%)}
.nav-toggle i{display:block;height:1px;background:var(--paper);margin:7px 0;transition:transform .25s,opacity .2s}
.nav-open .nav-toggle i:nth-child(1){transform:translateY(8px) rotate(45deg)}
.nav-open .nav-toggle i:nth-child(2){opacity:0}
.nav-open .nav-toggle i:nth-child(3){transform:translateY(-8px) rotate(-45deg)}

@media(max-width:940px){
  .nav-toggle{display:block}
  .nav-links{
    position:absolute;top:100%;left:0;right:0;
    background:var(--ink);
    flex-direction:column;align-items:flex-start;gap:0;
    padding:0 var(--gut);
    max-height:0;overflow:hidden;
    transition:max-height .3s ease,padding .3s ease;
  }
  /* overflow-y matters: at 44px per link under (pointer:coarse) the seven
     links exceed 70vh on any phone in landscape, and without this the last
     two are clipped with no way to scroll to them. */
  .nav-open .nav-links{max-height:70vh;overflow-y:auto;padding:0 var(--gut) var(--s-25)}
  .nav-links a{width:100%;padding:var(--s-13) 0;font-size:20px;border-bottom:0}
  .nav-links a[aria-current="page"]{border-bottom:0;text-decoration:underline;text-underline-offset:5px}
}

/* ---------------------------------------------------------------- buttons */
.btn{
  display:inline-block;
  font-size:17px;font-weight:400;line-height:1.5;
  color:var(--ink);background:var(--paper);
  border:1px solid var(--ink);border-radius:0;
  padding:var(--s-8) var(--s-17);
  transition:background .18s,color .18s;
}
.btn:hover{background:var(--ink);color:var(--paper)}
.on-ink .btn,.btn--invert{color:var(--paper);background:var(--ink);border-color:var(--paper)}
.on-ink .btn:hover,.btn--invert:hover{background:var(--paper);color:var(--ink)}
.btn-row{display:flex;flex-wrap:wrap;gap:var(--s-13)}

/* ------------------------------------------------------------ ink section */
.on-ink{background:var(--ink);color:var(--paper)}

/* ------------------------------------------------------------ project grid
   Every project, signature and secondary, in one grid. Four columns on a
   desktop screen; the tile image grows on hover.
   -------------------------------------------------------------------------- */
.tiles{
  display:grid;
  grid-template-columns:repeat(auto-fill,minmax(min(100%,290px),1fr));
  gap:var(--s-50) var(--s-17);
}
.tile{
  display:block;position:relative;z-index:1;
  transform-origin:center center;
  transition:transform .45s cubic-bezier(.16,.84,.44,1);
}
.tile figure{margin:0;overflow:hidden;background:var(--ink)}
.tile img{width:100%;aspect-ratio:3/2;object-fit:cover}
/* the whole tile grows and rises above the ones around it */
.tile:hover,.tile:focus-visible{transform:scale(1.06);z-index:5}
.tile-name{display:block;font-size:20px;line-height:1.3;margin-top:var(--s-11);min-height:52px}
.tile-meta{display:block;font-size:13px;line-height:1.5;margin-top:var(--s-5)}

/* On a phone the auto-fill grid falls to one tile per row, which turns
   twenty-four projects into twenty screens of scrolling. Two columns halve
   that, and the names step down so a title still fits on two lines. */
@media(max-width:760px){
  .tiles{grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--s-25) var(--s-11)}
  .tile-name{font-size:16px;line-height:1.25;margin-top:var(--s-8);min-height:0}
  .tile-meta{font-size:12px;margin-top:var(--s-5)}
  /* A tile is a link, so the tap target is the photograph plus its caption —
     comfortably past 44px. The hover lift is dropped: on touch it fires on
     tap and shoves the neighbouring tile. */
  .tile:hover,.tile:focus-visible{transform:none}
}
@media(max-width:380px){
  .tile-name{font-size:15px}
}

/* ---------------------------------------------------------- gallery grid
   A project's photographs, in the same grid. Click enlarges to full size.
   -------------------------------------------------------------------------- */
.shots{
  display:grid;
  grid-template-columns:repeat(auto-fill,minmax(min(100%,290px),1fr));
  gap:var(--s-17);
}
.shots figure{margin:0;overflow:hidden;background:var(--ink);cursor:zoom-in}
.shots img{
  width:100%;aspect-ratio:3/2;object-fit:cover;
  transition:transform .7s cubic-bezier(.16,.84,.44,1);
}
.shots figure:hover img,.shots figure:focus-visible img{transform:scale(1.09)}

/* portrait imagery keeps its proportion */
.people-grid{
  display:grid;gap:var(--s-50) var(--s-25);
  grid-template-columns:repeat(auto-fit,minmax(min(100%,260px),1fr));
}
.person figure{margin:0 0 var(--s-17);overflow:hidden;background:var(--ink)}
.person img{width:100%;aspect-ratio:3/4;object-fit:cover;object-position:center 22%}
.person__name{font-size:25px;line-height:1.15;margin-bottom:var(--s-5)}
.person__role{font-size:17px;line-height:1.5;margin-bottom:var(--s-11)}
.person__creds{font-size:13px;line-height:1.6}

/* ------------------------------------------------------------- data lists */
.entry-name{font-size:20px;line-height:1.3}
.entry-meta{font-size:13px;line-height:1.5;margin-top:var(--s-5)}
.record-list{
  display:grid;
  grid-template-columns:repeat(auto-fill,minmax(min(100%,285px),1fr));
  column-gap:var(--s-50);
}
.record-list > li{padding-block:var(--s-13);break-inside:avoid}

/* The label above each block of the record. */
.rec-lbl{font-size:13px;line-height:1.5;opacity:.55;margin-bottom:var(--s-13)}
.rec-lbl + .record-list{margin-bottom:var(--s-50)}
.record-list + .rec-lbl{margin-top:var(--s-25)}

/* A project that opens its own page is marked by an arrow rather than by an
   underline. A border-bottom draws a rule under every line of a wrapped
   name, which in a narrow column is most of them; one arrow says the same
   thing at any width. The arrow is bound to the last word with &nbsp; in the
   markup, so it can never be left stranded on a line of its own. */
.record-list .go{
  font-style:normal;display:inline-block;
  font-size:.72em;line-height:1;transform:translateY(-.18em);opacity:.55;
  transition:opacity .2s ease,transform .2s cubic-bezier(.16,.84,.44,1);
}
.record-list a:hover .go,
.record-list a:focus-visible .go{opacity:1;transform:translate(.1em,-.28em)}
/* The underline comes back where there is a pointer to hover with. */
@media (hover:hover){
  .record-list a:hover .entry-name,
  .record-list a:focus-visible .entry-name{
    box-shadow:inset 0 -1px 0 0 currentColor;
  }
}

/* On a phone the auto-fill grid falls to one column, and fifty-one project
   names in single file is a very long scroll for what is a reference list.
   CSS columns rather than a grid, so items flow to fill each column instead
   of every row being as tall as its tallest entry. The type steps down to
   suit a 106px column; below 380px it drops to two columns, where three
   would start breaking names mid-word. */
@media(max-width:760px){
  .record-list{display:block;columns:3;column-gap:var(--s-11)}
  /* The line-height has to be set on the list item and on the link, not only
     on the name span: an inline span inherits the block's 30px strut, so a
     wrapped name was leaving a 30px gap between its own two lines. */
  .record-list > li,.record-list a{font-size:13px;line-height:1.25}
  .record-list > li{padding-block:var(--s-8);margin-bottom:var(--s-5)}
  /* A one-line project name is 16px of text; the padding alone leaves it at
     32px, short of the 44px a finger needs. min-height only lifts the short
     ones — names that already wrap to two lines are past it and unaffected. */
  .record-list a{padding-block:var(--s-8);min-height:44px}
  .entry-name{font-size:13px;line-height:1.25;hyphens:auto}
  .entry-meta{font-size:11px;line-height:1.3;margin-top:3px;opacity:.72}
}
@media(max-width:380px){
  .record-list{columns:2;column-gap:var(--s-13)}
  .record-list > li,.record-list a,.entry-name{font-size:15px;line-height:1.3}
  .entry-meta{font-size:12px}
}

/* figures set as headings, two columns beside their headline */
.figures{
  display:grid;gap:var(--s-25) var(--s-50);
  grid-template-columns:repeat(auto-fit,minmax(min(100%,190px),1fr));
}
/* Eight figures one under another is most of a phone screen. Two up halves
   it, and the numbers still read at a glance. */
@media(max-width:760px){
  .figures{grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--s-25) var(--s-17)}
  .figures .t-heading{font-size:clamp(28px,7.4vw,38px)}
  .figures .t-body-sm{font-size:13px;line-height:1.4}
}
.figures b{font-weight:400}

/* long lists run in two columns rather than one very long single file */
/* long lists run in columns rather than one very long single file */
.list-2col{columns:2;column-gap:var(--s-50)}
.list-3col{columns:3;column-gap:var(--s-50)}
.list-2col > li,.list-3col > li{
  break-inside:avoid;-webkit-column-break-inside:avoid;margin-bottom:var(--s-25);
}
@media(max-width:1200px){.list-3col{columns:2}}
@media(max-width:760px){.list-2col,.list-3col{columns:1}}

/* ---------------------------------------------------------- numbered grid
   A real grid rather than CSS columns, so every item begins on the same
   line and the headings sit on one baseline across each row. The title
   reserves two lines whether it needs them or not.
   -------------------------------------------------------------------------- */
.numbered{
  display:grid;
  grid-template-columns:repeat(3,minmax(0,1fr));
  column-gap:var(--s-50);
  row-gap:var(--s-50);
}
.numbered > li{
  display:flex;flex-direction:column;
  border-top:1px solid currentColor;
  padding-top:var(--s-13);
}
.n-num{
  display:block;font-size:13px;line-height:1.5;
  margin-bottom:var(--s-11);opacity:.6;
  font-variant-numeric:tabular-nums;
}
.n-title{
  display:block;
  font-size:clamp(20px,1.9vw,25px);line-height:1.15;
  min-height:2.3em;                 /* two lines reserved, so bodies align */
  margin-bottom:var(--s-11);
}
.n-desc{display:block;font-size:17px;line-height:1.5}
.n-list{margin-top:var(--s-11);font-size:13px;line-height:1.6;opacity:.75}
.n-list li{padding-block:1px}
@media(max-width:1100px){.numbered{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:640px){
  .numbered{grid-template-columns:1fr;row-gap:var(--s-25)}
  .n-title{min-height:0}
}

/* A photograph shown whole — nothing cropped, and never blown up past the
   size the file can actually carry. The caption sits beside its foot. */
.shot-full{margin:0;display:grid;gap:var(--s-17) var(--s-50);align-items:end}
@media(min-width:900px){
  .shot-full{grid-template-columns:minmax(0,auto) minmax(0,1fr)}
}
.shot-full img{width:auto;max-width:100%;height:auto;max-height:min(780px,84vh)}
.shot-full figcaption{font-size:17px;line-height:1.5;max-width:34ch}
/* stacked variant: photograph over its caption, for a narrow column */
.shot-full--stack{grid-template-columns:1fr!important;gap:var(--s-13);align-items:start}
.shot-full--stack img{width:100%;max-height:none}
.shot-full--stack figcaption{font-size:13px;max-width:none}

/* ---------------------------------------------------------- brand partners */
.partners{
  display:grid;grid-template-columns:repeat(3,minmax(0,1fr));
  column-gap:var(--s-50);row-gap:var(--s-50);
}
.partners > li{border-top:1px solid currentColor;padding-top:var(--s-13)}
.partner-name{display:block;font-size:clamp(22px,2.6vw,34px);line-height:1.1;letter-spacing:-.01em;min-height:2.2em}
.partner-meta{display:block;font-size:13px;line-height:1.5;opacity:.6;margin-top:var(--s-8)}
@media(max-width:900px){.partners{grid-template-columns:repeat(2,minmax(0,1fr))}}
@media(max-width:760px){
  .partners{grid-template-columns:repeat(2,minmax(0,1fr));
    column-gap:var(--s-17);row-gap:var(--s-25)}
  .partner-name{font-size:clamp(17px,4.6vw,22px);min-height:2.6em}
  .partner-meta{font-size:12px;line-height:1.4;margin-top:var(--s-5)}
}

/* ------------------------------------------------------------------ roles */
.roles{
  display:grid;
  grid-template-columns:repeat(2,minmax(0,1fr));
  column-gap:var(--s-50);row-gap:var(--s-50);
}
.roles > li{
  display:flex;flex-direction:column;
  border-top:1px solid currentColor;padding-top:var(--s-13);
}
.role-title{display:block;font-size:clamp(21px,2.2vw,25px);line-height:1.15}
.role-tag{display:block;font-size:13px;line-height:1.5;opacity:.6;margin-top:var(--s-5)}
.role-desc{display:block;font-size:17px;line-height:1.5;margin-top:var(--s-11);flex:1 1 auto}
.role-act{margin-top:var(--s-17)}
@media(max-width:760px){.roles{grid-template-columns:1fr;row-gap:var(--s-25)}}

/* the how-to-apply steps run two-up rather than three */
/* ---- the ten disciplines ------------------------------------------------
   Four across on a desktop, two on a phone. Placed after the general
   .numbered breakpoints so these win at the same specificity. The block runs
   the full width of the page rather than sitting in a half column, because
   four titles will not fit in seven hundred pixels.
   ------------------------------------------------------------------------- */
.numbered--4{grid-template-columns:repeat(4,minmax(0,1fr));column-gap:var(--s-25)}
.numbered--4 .n-title{font-size:clamp(18px,1.5vw,22px);min-height:2.4em}
.numbered--4 .n-desc{font-size:15px;line-height:1.45}
@media(max-width:1100px){
  .numbered--4{grid-template-columns:repeat(3,minmax(0,1fr))}
}
@media(max-width:760px){
  .numbered--4{grid-template-columns:repeat(2,minmax(0,1fr));
    column-gap:var(--s-13);row-gap:var(--s-25)}
  .numbered--4 .n-title{font-size:16px;min-height:0;margin-bottom:var(--s-8)}
  .numbered--4 .n-desc{font-size:13px;line-height:1.4}
  .numbered--4 .n-num{margin-bottom:var(--s-5)}
}

.numbered.steps-2{grid-template-columns:repeat(2,minmax(0,1fr))}
@media(max-width:760px){.numbered.steps-2{grid-template-columns:1fr}}

/* the label that opens every CO Architects entry, so no one can skim the page
   and mistake whose building they are looking at */
.co-tag{
  display:inline-block;font-size:13px;line-height:1.4;letter-spacing:.02em;
  border:1px solid currentColor;padding:var(--s-5) var(--s-11);
}

/* --------------------------------------------------------- attribution
   Work by another practice appears here only with its authorship stated on
   the same screen as the image. These two blocks carry that credit.
   -------------------------------------------------------------------------- */
.credit-line{
  font-size:13px;line-height:1.6;opacity:.72;max-width:62ch;
  border-left:2px solid currentColor;padding-left:var(--s-13);
  margin-top:var(--s-17);
}
.credit-box{
  border:1px solid currentColor;padding:var(--s-25);max-width:820px;
}
.credit-box .t-body{max-width:78ch}
/* The page-foot attribution: a legal note, sized like one. It sits at the
   bottom because the per-project credit lines above already state authorship
   where the images are — this is the summary, not the first mention. */
.credit-note{
  font-size:13px;line-height:1.65;opacity:.62;max-width:88ch;
  padding-top:var(--s-13);border-top:1px solid currentColor;
}
@media(max-width:760px){.credit-note{font-size:12px;line-height:1.6}}

/* ------------------------------------------------------------- fact table
   Every project states its facts plainly, in a table a search engine — and a
   client skimming — can both read at a glance.
   -------------------------------------------------------------------------- */
.facts{margin:0}
.facts > div{
  display:grid;grid-template-columns:minmax(0,9em) minmax(0,1fr);
  gap:var(--s-17);
  border-top:1px solid currentColor;padding-block:var(--s-11);
}
.facts > div:last-child{border-bottom:1px solid currentColor}
.facts dt{font-size:13px;line-height:1.6;opacity:.6}
.facts dd{margin:0;font-size:17px;line-height:1.5}
@media(max-width:520px){
  .facts > div{grid-template-columns:1fr;gap:var(--s-5)}
}

/* ------------------------------------------------------------- breadcrumb */
.crumbs{padding-top:var(--s-25);font-size:13px;line-height:1.5}
.crumbs span{opacity:.5;padding-inline:var(--s-5)}
.crumbs a{border-bottom:1px solid transparent}
.crumbs a:hover{border-bottom-color:currentColor}
.crumbs [aria-current]{opacity:.6}

/* long prose runs two-up rather than as one very deep column */
.cols-2{columns:2;column-gap:var(--s-50)}
.cols-2 > p{break-inside:avoid;margin:0 0 var(--s-25)}
.cols-2 > p:last-child{margin-bottom:0}
@media(max-width:900px){.cols-2{columns:1}}

.link-u{border-bottom:1px solid currentColor;padding-bottom:1px}

/* ---------------------------------------------------------------- footer
   Horizontal. Five vertical columns pushed the last one onto a second row
   and made the footer as tall as a screen; the same links read across in a
   fraction of the height. A label on the left, its links running right.
   -------------------------------------------------------------------------- */
/* Bottom padding carries the floating WhatsApp button: it is fixed to the
   bottom-right of the viewport, so without this it lands on top of the
   copyright line and the cookie link. */
.site-foot{
  background:var(--ink);color:var(--paper);
  padding-top:var(--s-50);padding-bottom:calc(var(--s-25) + 84px);
}
.foot-in{
  max-width:var(--wrap);margin:0 auto;padding-inline:var(--gut);
  display:flex;flex-wrap:wrap;gap:var(--s-25) var(--s-59);align-items:flex-start;
}
.foot-mark{flex:0 1 320px}
.foot-mark img{height:32px;width:auto;margin-bottom:var(--s-13)}
.foot-mark p{font-size:15px;line-height:1.45;max-width:34ch}
.foot-facts{flex:1 1 420px;min-width:0}

/* one label, then its content reading across */
.foot-row{display:flex;flex-wrap:wrap;gap:var(--s-5) var(--s-17);
  font-size:15px;line-height:1.5;align-items:baseline}
.foot-row + .foot-row{margin-top:var(--s-8)}
.foot-lbl{flex:0 0 122px;font-size:12px;line-height:1.5;opacity:.55}
/* The value takes whatever is left beside its label and wraps inside that
   column, rather than dropping the whole row onto a second line. */
.foot-row > *:not(.foot-lbl){flex:1 1 0;min-width:0}
.foot-strip{display:flex;flex-wrap:wrap;gap:var(--s-5) var(--s-17)}
.foot-strip a,.foot-row a{white-space:nowrap}
.site-foot a:hover{text-decoration:underline;text-underline-offset:4px}

.foot-nav{
  max-width:var(--wrap);margin:var(--s-25) auto 0;padding-inline:var(--gut);
  padding-top:var(--s-17);
}

.foot-end{
  max-width:var(--wrap);margin:var(--s-25) auto 0;padding-inline:var(--gut);
  display:flex;flex-wrap:wrap;gap:var(--s-8) var(--s-25);justify-content:space-between;
  font-size:12px;line-height:1.5;opacity:.75;
  /* The WhatsApp and back-to-top buttons hover over the bottom corners, and
     at the very end of the page they were sitting on top of this line. The
     copyright and the privacy links are inset far enough to clear both. */
  padding-bottom:var(--s-11);
}
.foot-end > :first-child{padding-left:72px}
.foot-end > :last-child{padding-right:72px}
@media(max-width:760px){
  .foot-end > :first-child{padding-left:0}
  .foot-end > :last-child{padding-right:0}
  /* Stacked on a phone, so give the whole bar room beneath both buttons. */
  .foot-end{padding-bottom:84px}
}

/* On a phone a 138px label column eats a third of the width, so the label
   sits on its own line above the links it introduces. */
@media(max-width:760px){
  .site-foot{padding-top:var(--s-25);padding-bottom:calc(var(--s-25) + 72px)}
  .foot-in{gap:var(--s-17)}
  .foot-row{gap:var(--s-5) var(--s-17)}
  .foot-lbl{flex:0 0 100%}
  .foot-nav{margin-top:var(--s-25)}
  .foot-mark img{height:34px}
}

/* --------------------------------------------------------------- lightbox */
.lb{
  position:fixed;inset:0;z-index:280;background:var(--ink);
  display:none;align-items:center;justify-content:center;padding:var(--s-8);
  /* Two quick taps on Prev or Next are, to a phone, a double-tap — and a
     double-tap is the browser's own zoom gesture, so the whole page jumped
     instead of the photograph advancing. `manipulation` switches off
     double-tap zoom inside the lightbox while leaving pinch-to-zoom alone,
     so a photograph can still be examined. It also removes the 300ms wait
     the browser otherwise spends deciding whether a second tap is coming,
     which is why the buttons now answer immediately. */
  touch-action:manipulation;
}
.lb.is-on{display:flex}
/* The frame is stated, and the photograph is fitted into it. Sizing from the
   image's own intrinsic dimensions is what made portrait and square shots
   open as a small tile: with a srcset the browser derives the intrinsic size
   from the `sizes` value and the chosen candidate's density, so it is not the
   file's real pixel size and cannot be relied on. A fixed box with
   object-fit:contain always fills the screen and never crops. */
/* As large as the screen allows, never cropped. The box is the whole
   viewport less a hairline of padding, and object-fit:contain letterboxes
   the photograph inside it — so a landscape shot runs the full width and a
   portrait one the full height, and neither loses an edge. The controls
   float over the top; they carry their own background and stay readable. */
.lb img{
  width:100%;height:100%;
  max-width:100%;max-height:100%;
  object-fit:contain;image-rendering:auto;
}
.lb button{
  position:absolute;color:var(--paper);background:var(--ink);
  border:1px solid var(--paper);border-radius:0;
  padding:var(--s-8) var(--s-17);font-size:17px;line-height:1.5;
  touch-action:manipulation;-webkit-tap-highlight-color:transparent;
  -webkit-user-select:none;user-select:none;
}
/* Guarded, because a phone has no hover. A touchscreen fakes one on tap and
   then leaves it there until you touch something else, so Prev and Next were
   staying inverted — white — after every tap. Only a device with a real
   pointer gets the hover state now. */
@media (hover:hover) and (pointer:fine){
  .lb button:hover{background:var(--paper);color:var(--ink)}
}
.lb .lb-x{top:var(--s-25);right:var(--s-25)}
.lb .lb-p{left:var(--s-25);top:50%;transform:translateY(-50%)}
.lb .lb-n{right:var(--s-25);top:50%;transform:translateY(-50%)}
.lb .lb-c{
  position:absolute;bottom:var(--s-25);left:50%;transform:translateX(-50%);
  color:var(--paper);font-size:13px;line-height:1.5;
}

/* ==========================================================================
   MOTION — enhancement only. With JavaScript off, or prefers-reduced-motion
   set, the page renders complete and static.
   ========================================================================== */

/* ---- line-by-line headline wipe ---------------------------------------- */
.js h1[class*="t-display"], .js h1[class*="t-heading"],
.js h2[class*="t-display"], .js h2[class*="t-heading"],
.js h3[class*="t-heading"]{opacity:0}
.js h1.is-split,.js h2.is-split,.js h3.is-split{opacity:1}

/* The mask box is grown below the baseline and pulled back with a matching
   negative margin, so descenders (g j p q y) are never sheared off while the
   line still occupies exactly its authored height. */
.ln{display:block;overflow:hidden;padding-bottom:.20em;margin-bottom:-.20em}
.ln > i{
  display:block;font-style:normal;
  transform:translateY(132%);
  transition:transform .95s cubic-bezier(.16,.84,.44,1);
}
.is-split.in .ln > i{transform:translateY(0)}

/* A line that wraps cannot be masked without being clipped, so those
   headings fade in rather than sliding out from behind an edge. */
.is-wrapped .ln{overflow:visible}
.is-wrapped .ln > i{transform:none;opacity:0;transition:opacity .8s ease}
.is-wrapped.in .ln > i{opacity:1}

/* ---- cycling headline ---------------------------------------------------
   Three statements occupying the same two lines. The live one is in flow and
   sets the height; the others stack on top of it, waiting below their masks.
   Each phrase wipes up and out as the next wipes up and in.
   ------------------------------------------------------------------------- */
.cyc-h{
  position:relative;display:block;max-width:none;
  font-size:clamp(38px,10.2vw,146px);line-height:.92;letter-spacing:-.05em;
}
.cyc{position:relative;display:block}
.cyc-i{display:block}
.cyc-i + .cyc-i{position:absolute;top:0;left:0;width:100%}
.cyc-h .ln > i{white-space:nowrap}
/* The hero phrase changes on a loop rather than once on scroll, so it runs
   quicker than the site-wide .95s line reveal — long enough to read, short
   enough that the headline feels alive. Scoped here so no other heading
   speeds up with it. */
.cyc-h .ln > i{transition-duration:.7s}
.js .cyc-h .ln > i{transform:translateY(132%)}
.js .cyc-h.in > .ln > i{transform:translateY(0)}          /* the fixed first line */
.js .cyc-h.in .cyc-i.is-on .ln > i{transform:translateY(0)}
.js .cyc-h.in .cyc-i.is-out .ln > i{transform:translateY(-132%)}
.cyc-i.no-tr .ln > i{transition:none}
/* no script, or motion suppressed: the first statement simply stands */
html:not(.js) .cyc-i + .cyc-i{display:none}
@media(prefers-reduced-motion:reduce){
  .cyc-h .ln > i{transform:none!important}
  .cyc-i + .cyc-i{display:none}
}

/* ------------------------------------------------------------ whatsapp tab */
.wa{
  position:fixed;right:var(--gut);bottom:var(--s-25);z-index:200;
  display:inline-flex;align-items:center;gap:var(--s-11);
  /* A square, not a pill. The label used to expand on hover, which meant the
     control had one shape on a desktop and another on a phone. */
  width:56px;height:56px;padding:0;justify-content:center;
  /* WhatsApp's own green. The two-colour rule holds everywhere else on the
     site; this one control is a third-party affordance and is recognised
     faster in its own colour than in ink. */
  background:#25d366;color:#08120b;
  border:1px solid #25d366;
  box-shadow:0 6px 24px rgba(0,0,0,.22);
  transition:background .2s ease,color .2s ease,transform .3s cubic-bezier(.16,.84,.44,1),opacity .25s ease;
}
.wa svg{width:24px;height:24px;flex:0 0 auto;fill:currentColor}
/* The label stays in the markup for screen readers and search, but never
   takes space — the icon and the aria-label carry the meaning. */
.wa span{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}
.wa:focus-visible{background:#1ebe5b;color:#000;border-color:#1ebe5b}
@media (hover:hover) and (pointer:fine){
  .wa:hover{background:#1ebe5b;color:#000;border-color:#1ebe5b}
}
/* The consent card now sits in the opposite corner, so the WhatsApp button
   stays reachable while it is up. Only the open phone menu displaces it. */
.nav-open .wa{opacity:0;pointer-events:none;transform:translateY(20px)}
@media(max-width:760px){
  .wa{width:52px;height:52px;padding:0;right:var(--s-17);bottom:var(--s-17)}
}
@media print{.wa{display:none}}

.skip{position:absolute;left:-9999px}

/* ---- loading screen -----------------------------------------------------
   Shown once per visit, on the first page opened. Wordmark, a hairline that
   fills against real image progress, and a count to one hundred. It leaves
   upward, in the same movement as the page-transition wipe.
   ------------------------------------------------------------------------- */
.load{
  position:fixed;inset:0;z-index:400;
  background:var(--ink);color:var(--paper);
  display:grid;place-items:center;padding:var(--gut);
  transform:translateY(0);
  /* Appended to <html>, not to <body>, so it does not inherit the body font
     and has to state it. Without this line the wordmark line and the count
     render in the browser's default serif. */
  font-family:var(--f);font-weight:400;
  -webkit-font-smoothing:antialiased;
}
.load-in{width:min(620px,78vw)}
.load-mark{height:clamp(30px,3.6vw,52px);width:auto;margin-bottom:var(--s-25)}
.load-bar{position:relative;height:1px;background:rgba(255,255,252,.26);overflow:hidden}
.load-bar span{
  position:absolute;left:0;top:0;bottom:0;width:0;
  background:var(--paper);transition:width .28s linear;
}
.load-foot{
  display:flex;justify-content:space-between;align-items:baseline;gap:var(--s-25);
  margin-top:var(--s-13);font-size:13px;line-height:1.5;
}
.load.is-done{
  transform:translateY(-100%);
  transition:transform .82s cubic-bezier(.62,.05,.36,1);
}
html.loading,html.loading body{overflow:hidden}
html:not(.js) .load{display:none}

/* ---- opening sequence --------------------------------------------------- */
.js.intro .nav-bar{transform:translateY(-100%)}
.js .nav-bar{transition:transform .75s cubic-bezier(.16,.84,.44,1),padding .35s cubic-bezier(.16,.84,.44,1)}
.js.intro-done .nav-bar{transform:none}

.js .intro-hold{opacity:0;transform:translateY(16px)}
.js .intro-hold.in{
  opacity:1;transform:none;
  transition:opacity .85s ease,transform .85s cubic-bezier(.16,.84,.44,1);
}

/* ---- sticky header shrink ---------------------------------------------- */
.nav-bar.is-tight{padding-block:var(--s-8)}
.nav-bar.is-tight .nav-logo img{height:17px;transition:height .35s cubic-bezier(.16,.84,.44,1)}

/* ---- page transition wipe ---------------------------------------------- */
.wipe{
  position:fixed;inset:0;z-index:300;
  background:var(--ink);
  transform:translateY(100%);
  pointer-events:none;
  will-change:transform;
}
.wipe.is-cover{transform:translateY(0)}
.wipe.is-in{transform:translateY(0);transition:transform .55s cubic-bezier(.62,.05,.36,1)}
.wipe.is-out{transform:translateY(-100%);transition:transform .70s cubic-bezier(.22,.61,.36,1)}

/* ---- custom cursor ------------------------------------------------------
   A filled circle. Ink on the cream ground; the accent red over the ink
   sections and over photography, where black would disappear.
   ------------------------------------------------------------------------- */
@media(hover:hover) and (pointer:fine){
  .has-cursor,.has-cursor *{cursor:none}
}
.cursor,.cursor-t{
  position:fixed;top:0;left:0;
  border-radius:50%;
  pointer-events:none;opacity:0;
  transition:width .32s cubic-bezier(.16,.84,.44,1),
             height .32s cubic-bezier(.16,.84,.44,1),
             background .25s ease,
             opacity .25s ease;
}
.cursor{
  /* Above the consent card (320). At 290 the pointer disappeared behind it,
     which on a site with no system cursor means losing the pointer entirely. */
  z-index:330;
  width:14px;height:14px;
  background:rgba(var(--ink-rgb),var(--cursor-alpha));
  display:grid;place-items:center;
  font-size:12px;line-height:1;color:var(--paper);white-space:nowrap;
}
/* The label stays solid. A caption painted at the circle's own transparency
   is unreadable over a photograph, and the instruction was about the circle. */
.cursor span{opacity:0;transition:opacity .2s ease}
.cursor.is-on{opacity:1}
.cursor.on-dark{background:rgba(var(--cursor-accent-rgb),var(--cursor-alpha))}
.cursor.is-link{width:32px;height:32px}
.cursor.is-view{
  width:74px;height:74px;
  background:rgba(var(--cursor-accent-rgb),var(--cursor-alpha));
}
.cursor.is-view span{opacity:1}

/* Two smaller circles running behind the main one. They chase the pointer on
   a slower easing, so they string out into a short tail while the hand is
   moving and slide back under the leader the moment it stops. They step
   aside for the "Explore" disc, which is large enough to sit on its own. */
.cursor-t{z-index:329}
.cursor-t.t1{width:9px;height:9px;background:rgba(var(--ink-rgb),calc(var(--cursor-alpha) * .72))}
.cursor-t.t2{width:6px;height:6px;background:rgba(var(--ink-rgb),calc(var(--cursor-alpha) * .48))}
.cursor-t.is-on{opacity:1}
.cursor-t.on-dark.t1{background:rgba(var(--cursor-accent-rgb),calc(var(--cursor-alpha) * .72))}
.cursor-t.on-dark.t2{background:rgba(var(--cursor-accent-rgb),calc(var(--cursor-alpha) * .48))}
.cursor-t.is-hidden{opacity:0}

/* ---- reveal -------------------------------------------------------------
   Scoped to .js, which the inline head script sets. Unscoped, this hid every
   reveal block on every page for any visitor whose JavaScript did not run —
   and main.js can fail to run, because a dynamic import() inside a classic
   script is a parse error on an older browser. The site claimed to render
   complete without JavaScript and did not. 1 September 2026.
   Also lifted for print, where nothing below the fold has been intersected
   and most of a long page would otherwise print blank. */
.js .rv{opacity:0;transform:translateY(14px);transition:opacity .6s ease,transform .6s cubic-bezier(.16,.84,.44,1)}
.js .rv.in{opacity:1;transform:none}
/* Marked by the short script at the foot of every page: this element was
   already on screen when the page opened, so it is simply there, with no
   fade. Fading in something the visitor is already looking at is not an
   entrance — it is a delay, and it was costing about two and a half seconds
   of the score Google keeps for how quickly a page appears. */
.js .rv.rv-now{transition:none}
@media print{
  .js .rv, .js h1[class*="t-display"], .js h1[class*="t-heading"],
  .js h2[class*="t-display"], .js h2[class*="t-heading"],
  .js h3[class*="t-heading"]{opacity:1!important;transform:none!important}
}

@media(prefers-reduced-motion:reduce){
  .js h1[class*="t-display"], .js h1[class*="t-heading"],
  .js h2[class*="t-display"], .js h2[class*="t-heading"],
  .js h3[class*="t-heading"]{opacity:1}
  .ln > i{transform:none;transition:none}
  .bleed img{transform:none}
  .wipe,.cursor,.cursor-t{display:none}
  .has-cursor,.has-cursor *{cursor:auto}
  .rv{opacity:1;transform:none;transition:none}
  *{animation:none!important;transition-duration:.01ms!important}
}

/* ---- touch targets ------------------------------------------------------ */
@media(pointer:coarse){
  .btn{padding-block:var(--s-11);min-height:44px}
  .lb button{padding-block:var(--s-11);min-height:44px}
  .nav-links a{min-height:44px;display:flex;align-items:center}
}

/* ------------------------------------------------------------ instagram grid
   Six square tiles, the proportion Instagram itself uses. The photographs are
   served from this site, so the page needs no Instagram script and sets no
   third-party cookie.
   -------------------------------------------------------------------------- */
.ig-grid{
  display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--s-8);
}
.ig-grid a{display:block;position:relative;overflow:hidden;background:var(--ink)}
.ig-grid img{
  width:100%;aspect-ratio:1/1;object-fit:cover;
  transition:transform .55s cubic-bezier(.16,.84,.44,1),opacity .3s ease;
}
.ig-grid a:hover img,.ig-grid a:focus-visible img{transform:scale(1.05);opacity:.86}
.ig-grid .ig-go{
  position:absolute;inset:auto var(--s-8) var(--s-8) auto;
  background:var(--paper);color:var(--ink);font-size:13px;line-height:1;
  padding:var(--s-5) var(--s-8);opacity:0;transform:translateY(4px);
  transition:opacity .25s ease,transform .25s ease;
}
.ig-grid a:hover .ig-go,.ig-grid a:focus-visible .ig-go{opacity:1;transform:none}
@media(max-width:640px){.ig-grid{grid-template-columns:repeat(2,minmax(0,1fr))}}

/* ------------------------------------------------------------ consent card
   A small card in the bottom-left corner rather than a bar across the whole
   screen. It still has to say what is collected and still has to make "no"
   as easy as "yes" — the DPDP Act asks for that — but it can do both in a
   third of the room. The WhatsApp button lives in the opposite corner, so
   the two no longer fight for the bottom of the screen.
   -------------------------------------------------------------------------- */
.consent{
  position:fixed;left:var(--s-17);bottom:var(--s-17);z-index:320;
  width:min(340px, calc(100vw - var(--s-17) * 2 - 76px));
  background:var(--ink);color:var(--paper);
  border:1px solid var(--paper);
  padding:var(--s-17);
  opacity:0;transform:translateY(14px);
}
.consent.in{
  opacity:1;transform:none;
  transition:opacity .4s ease, transform .5s cubic-bezier(.16,.84,.44,1);
}
.consent-in{display:block}
.consent-t{font-size:15px;line-height:1.3}
.consent-b{font-size:12px;line-height:1.5;opacity:.8;margin-top:var(--s-8)}
.consent-a{display:flex;gap:var(--s-8);flex-wrap:wrap;margin-top:var(--s-13)}
.consent .btn{
  color:var(--paper);background:var(--ink);border-color:var(--paper);
  font-size:13px;padding:var(--s-8) var(--s-13);min-height:38px;
}
.consent .btn:hover{background:var(--paper);color:var(--ink)}
@media(max-width:520px){
  .consent{left:var(--s-13);bottom:var(--s-13);
    width:calc(100vw - var(--s-13) * 2 - 72px)}
  .consent-b{font-size:11px}
}
@media (pointer:coarse){
  .consent .btn{min-height:44px}
}

.skip:focus{left:var(--gut);top:var(--s-10);z-index:100;background:var(--paper);color:var(--ink);padding:var(--s-8) var(--s-17);border:1px solid var(--ink)}
:focus-visible{outline:2px solid currentColor;outline-offset:3px}

/* ==========================================================================
   Touch targets and phone comfort.

   Apple asks for 44pt and Google for 48dp; the audit found the menu button at
   26px, footer and contact links at 19-21px and breadcrumbs at 16px. These
   rules apply only where the pointer is coarse, so nothing on a desktop moves.

   Navigation, list and footer links get real height. Links inside running
   prose are deliberately left alone: WCAG exempts inline text links, and
   padding them would break the line rhythm of a paragraph.
   ========================================================================== */
@media (pointer:coarse){

  /* The menu button keeps its drawn size and gains an invisible 48px pad. */
  .nav-toggle{position:relative}
  .nav-toggle::after{
    content:"";position:absolute;top:50%;left:50%;
    transform:translate(-50%,-50%);width:48px;height:48px;
  }
  .nav-logo{position:relative}
  .nav-logo::after{
    content:"";position:absolute;top:50%;left:0;transform:translateY(-50%);
    width:100%;height:46px;
  }

  /* Footer: the strips read across, so every link needs its own height.
     The 11px of padding above and below each link is what separates the
     wrapped rows, so the flex row-gap goes to zero — otherwise the two
     stack and the footer grows by a third for no gain.

     The Instagram mark is excluded, and has to be. It is a fixed 22px box,
     so the padding could not grow it — it only pushed the mark 11px down
     inside a 44px line, leaving it hanging below the email address beside
     it. It needs no padding anyway: it carries its own invisible 44px tap
     area in .ig::after, which is what makes it thumb-sized without
     changing where the mark is drawn. */
  .foot-strip a:not(.ig),
  .foot-row a:not(.ig),
  .foot-mark a{display:inline-block;padding-block:11px;min-height:22px}
  .foot-strip,.foot-row{row-gap:0}
  .foot-nav .foot-row + .foot-row{margin-top:var(--s-8)}
  .btn{padding-block:0}
  .foot-end a{display:inline-block;padding-block:var(--s-13)}

  /* Numbered-list titles that are links (the "Beyond Hospitality" block on
     Services) and any link standing alone in a list description. */
  .n-title a,.n-desc a{display:inline-block;padding-block:11px}

  /* Breadcrumbs. */
  .crumbs a,.crumbs [aria-current]{display:inline-block;padding-block:13px}
  .crumbs span{padding-inline:var(--s-8)}

  /* The record list — fifty-one project names, each one a link. */
  .record-list a{display:block;padding-block:var(--s-8)}

  /* Buttons already clear 44px; this guarantees it if one ever shrinks. */
  .btn{min-height:44px;display:inline-flex;align-items:center}

  /* The telephone number and the email address on the contact page are the
     two things that page exists to get tapped, and both were 21px tall.
     Scoped to a link that is the whole paragraph, so the rule cannot reach
     the same class inside running prose, where it would break line rhythm —
     which is why .link-u is not padded generally.

     Padding is not used to grow these: .link-u draws its underline as a
     border-bottom, and padding would push that rule 11px clear of the text.
     An invisible box does the work instead, the same way the menu button
     does it, so the link looks untouched and the tap area is 44px. */
  .t-body > a.link-u[href^="tel:"]:only-child,
  .t-body > a.link-u[href^="mailto:"]:only-child{position:relative}
  .t-body > a.link-u[href^="tel:"]:only-child::after,
  .t-body > a.link-u[href^="mailto:"]:only-child::after{
    content:"";position:absolute;left:0;top:50%;
    transform:translateY(-50%);width:100%;height:44px;
  }
}

/* iPhone home indicator: keep the floating button and the consent bar clear
   of the gesture area rather than under it. */
@supports (padding:max(0px)){
  .wa{bottom:max(var(--s-25), env(safe-area-inset-bottom))}
  .consent{padding-bottom:max(var(--s-17), env(safe-area-inset-bottom))}
  @media(max-width:760px){
    .wa{bottom:max(var(--s-17), env(safe-area-inset-bottom))}
  }
}

/* Stop iOS painting a grey box over a tapped tile or button. */
a,button{-webkit-tap-highlight-color:rgba(0,0,0,.08)}

/* ==========================================================================
   Opening splash — the home page only.

   The headline at full height for three seconds, cycling its three endings,
   then lifting away in the same movement as the page-transition wipe. The
   whole sequence is CSS keyframes on a single three-second timeline, so it
   plays correctly whether or not main.js has finished parsing.

   Percentages against 3s:  8% = 0.24s   30% = 0.90s   38% = 1.14s
                           63% = 1.89s   71% = 2.13s
   ========================================================================== */
.splash{
  position:fixed;inset:0;z-index:400;
  background:var(--ink);color:var(--paper);
  display:grid;align-content:center;
  padding:var(--gut);
  transform:translateY(0);
  /* Same reason as .load — this sits on <html>, so it states its own font. */
  font-family:var(--f);font-weight:400;
  -webkit-font-smoothing:antialiased;
}
.splash-in{width:100%;max-width:var(--wrap);margin:0 auto}
.splash-h{
  font-weight:400;
  /* Sized from the longest phrase rather than guessed. "Hospitality Spaces."
     measures 7.553em wide in Outfit at this tracking, so the type can be at
     most (content width / 7.7) before it runs past the gutter — 7.7 leaves a
     two per cent margin for a fallback face with wider metrics. The vw term
     stops it growing absurdly on a wide monitor. */
  font-size:clamp(26px,
    min(12.4vw, min(100vw - 2 * var(--gut), var(--wrap)) / 7.7),
    208px);
  line-height:.9;
  letter-spacing:-.05em;
  margin:0;
}
/* The clip box and the descender room cannot be the same box.
   overflow:hidden clips at the PADDING edge, not the content edge — so the
   padding-bottom that was here to stop the "p" of "Spaces" being cut was
   also a .16em window the sliding words showed through. Those were the
   stray dashes across the headline on every line change.
   Descender room now comes from the line box itself, which the clip
   respects, and the negative margin closes the gap again without touching
   the clip. */
/* Two faults made the stray dashes across the splash headline.
   First, .ln above pairs overflow:hidden with padding-bottom:.20em — and
   overflow clips at the PADDING edge, so that band was a window the sliding
   words showed through. The splash inherited it while travelling only 112%,
   not far enough to clear it. Padding is zeroed here and the descender room
   comes from the line box instead, which the clip is genuinely cut to.
   Second, a travel of exactly one box height still leaks a hairline when the
   compositor rounds fractional pixels. The splash now travels 132%, the same
   figure the rest of the site uses, which puts the edge well clear. */
.splash-h .ln{display:block;overflow:hidden;padding-bottom:0;margin-bottom:-.18em}
.splash-h .ln > i{
  display:block;font-style:normal;white-space:nowrap;
  /* .ln has one block child, so THIS line box is what the clip is cut to.
     At 1.08em the descenders sit inside it. Net line advance stays .90em
     (1.08 less the .18em pulled back above), so the stacking is unchanged. */
  line-height:1.08;
}

/* "We Design" rises once and stays put. */
.splash-h .sp-fixed{animation:spRise .78s cubic-bezier(.16,.84,.44,1) .05s both}
@keyframes spRise{from{transform:translateY(132%)}to{transform:translateY(0)}}

/* The three endings share one stack, so they occupy one line rather than three. */
.sp-stack{position:relative;display:block}
.sp-stack .sp-i{position:absolute;top:0;left:0;width:100%}
.sp-stack .sp-i:first-child{position:relative}
.sp-i > i{transform:translateY(132%)}
.sp1 > i{animation:sp1 3s cubic-bezier(.16,.84,.44,1) both}
.sp2 > i{animation:sp2 3s cubic-bezier(.16,.84,.44,1) both}
.sp3 > i{animation:sp3 3s cubic-bezier(.16,.84,.44,1) both}
@keyframes sp1{
  0%{transform:translateY(132%)}
  8%,30%{transform:translateY(0)}
  38%,100%{transform:translateY(-132%)}
}
@keyframes sp2{
  0%,30%{transform:translateY(132%)}
  38%,63%{transform:translateY(0)}
  71%,100%{transform:translateY(-132%)}
}
@keyframes sp3{
  0%,63%{transform:translateY(132%)}
  71%,100%{transform:translateY(0)}
}

.splash-foot{
  position:absolute;left:var(--gut);right:var(--gut);bottom:var(--gut);
  font-size:13px;line-height:1.5;opacity:.6;
  animation:spFade .8s ease 1.4s both;
}
@keyframes spFade{from{opacity:0}to{opacity:.6}}

/* Leaves upward, exactly as the loader and the transition wipe do. */
.splash.is-done{
  transform:translateY(-100%);
  transition:transform .86s cubic-bezier(.62,.05,.36,1);
}
html:not(.js) .splash{display:none}
@media(prefers-reduced-motion:reduce){.splash{display:none}}

/* ==========================================================================
   REPEAT CLIENTS — the credibility block. A returning client is a fact, not
   a testimonial, so it is set as a record: count, brand, span, one line of
   what happened. Same two-colour rule as everything else.
   ========================================================================== */
.repeat{
  display:grid;
  grid-template-columns:repeat(3,minmax(0,1fr));
  column-gap:var(--s-50);
  row-gap:var(--s-50);
}
.repeat > li{
  display:flex;flex-direction:column;
  border-top:1px solid currentColor;
  padding-top:var(--s-13);
}
/* The count is the headline of each entry — ten properties says more than
   any adjective would. Tabular figures so the column reads straight. */
.rpt-n{
  display:block;
  font-size:34px;line-height:1.1;
  font-variant-numeric:tabular-nums;
  margin-bottom:var(--s-8);
}
.rpt-brand{display:block;font-size:17px;line-height:1.4;margin-bottom:var(--s-5)}
.rpt-span{
  display:block;font-size:13px;line-height:1.5;opacity:.6;
  font-variant-numeric:tabular-nums;
  margin-bottom:var(--s-11);
}
.rpt-note{display:block;font-size:13px;line-height:1.6;opacity:.8}
@media(max-width:1100px){ .repeat{grid-template-columns:repeat(2,minmax(0,1fr))} }
@media(max-width:760px){
  .repeat{grid-template-columns:repeat(2,minmax(0,1fr));column-gap:var(--s-17);row-gap:var(--s-25)}
  .rpt-n{font-size:25px}
  .rpt-brand{font-size:14px}
}
@media(max-width:380px){ .repeat{grid-template-columns:1fr} }

/* ==========================================================================
   FAQ — native <details>, so it works with JavaScript off and the answers
   are in the DOM for a crawler whether or not anyone opens them.
   ========================================================================== */
.faq{border-top:1px solid currentColor;max-width:var(--wrap)}
.faq-i{border-bottom:1px solid currentColor}
.faq-q{
  display:block;position:relative;
  padding:var(--s-17) var(--s-50) var(--s-17) 0;
  font-size:21px;line-height:1.35;
  cursor:pointer;list-style:none;
  min-height:44px;
}
.faq-q::-webkit-details-marker{display:none}
/* A plus that becomes a minus. No icon font, no image. */
.faq-q::after{
  content:"";position:absolute;right:var(--s-8);top:50%;
  width:15px;height:1px;background:currentColor;
  transition:transform .25s cubic-bezier(.16,.84,.44,1);
}
.faq-q::before{
  content:"";position:absolute;right:var(--s-8);top:50%;
  width:15px;height:1px;background:currentColor;
  transform:rotate(90deg);
  transition:opacity .25s ease,transform .25s cubic-bezier(.16,.84,.44,1);
}
.faq-i[open] .faq-q::before{opacity:0;transform:rotate(0deg)}
.faq-a{padding:0 0 var(--s-25) 0;max-width:var(--measure)}
.faq-a .t-body{margin:0}
@media(prefers-reduced-motion:reduce){
  .faq-q::after,.faq-q::before{transition:none}
}
@media(max-width:760px){
  .faq-q{font-size:17px;padding-right:var(--s-25)}
}

/* ==========================================================================
   PRESS & RECOGNITION — a record, like everything else on this site: status,
   source, date, and what it said. Each row links out to the publication so a
   reader can check the claim instead of trusting it.
   ========================================================================== */
.press{border-top:1px solid currentColor}
.prs{
  display:grid;
  grid-template-columns:110px minmax(0,1.1fr) 130px minmax(0,2fr);
  column-gap:var(--s-25);
  align-items:baseline;
  border-bottom:1px solid currentColor;
  padding-block:var(--s-17);
}
.prs-tag{
  font-size:13px;line-height:1.5;opacity:.55;
  text-transform:uppercase;letter-spacing:.04em;
}
.prs-name{font-size:21px;line-height:1.35}
.prs-name a{
  color:inherit;text-decoration:underline;
  text-decoration-thickness:1px;text-underline-offset:.22em;
  text-decoration-color:rgba(var(--ink-rgb),.35);
  transition:text-decoration-color .2s ease;
}
.prs-name a:hover,.prs-name a:focus-visible{text-decoration-color:currentColor}
.prs-date{font-size:13px;line-height:1.5;opacity:.6;font-variant-numeric:tabular-nums}
.prs-note{font-size:15px;line-height:1.55;opacity:.85}

@media(max-width:1100px){
  .prs{grid-template-columns:100px minmax(0,1fr) 120px;row-gap:var(--s-8)}
  .prs-note{grid-column:2 / -1}
}
@media(max-width:760px){
  .prs{grid-template-columns:1fr;row-gap:var(--s-5);padding-block:var(--s-17)}
  .prs-note{grid-column:1}
  .prs-name{font-size:17px}
  /* The whole row is the tap target on a phone, so give the link real height. */
  .prs-name a{display:inline-block;padding-block:var(--s-8);min-height:44px}
}

/* ==========================================================================
   HOMEPAGE HERO — the headline is sized from the longest phrase so it fills
   the measure rather than sitting in the middle of it, and the photograph
   that follows runs the full width of the viewport.
   ========================================================================== */
/* Same formula as .splash-h, so the headline the splash lifts away from and
   the headline underneath it are the same size — the transition reads as one
   object settling rather than two headlines of different scales. */
.cyc-h{
  font-size:clamp(30px,
    min(12.4vw, min(100vw - 2 * var(--gut), var(--wrap)) / 7.7),
    208px);
}

.bleed-hero{
  margin:0;
  width:100%;
  display:block;
  /* No ink ground here: the caption sits directly under the photograph on
     paper, and an ink panel behind it made the caption invisible. */
}
.bleed-hero img{
  display:block;width:100%;height:auto;
  /* A tall crop on a phone and a letterbox on a monitor: the porte-cochère
     reads either way because the columns run top to bottom of the frame. */
  aspect-ratio:16 / 9;
  object-fit:cover;
  /* The columns and the patterned floor are the subject and they sit low in
     the frame, so the crop is pulled down rather than centred. */
  object-position:center 74%;
}
@media(max-width:760px){
  .bleed-hero img{aspect-ratio:4 / 5}
}

/* ==========================================================================
   NUMBERED — five across. With the description line gone these are labels,
   not paragraphs, so they can sit much tighter than the three-column set.
   ========================================================================== */
.numbered--5{
  grid-template-columns:repeat(5,minmax(0,1fr));
  column-gap:var(--s-25);
  row-gap:var(--s-25);
}
@media(max-width:1100px){ .numbered--5{grid-template-columns:repeat(3,minmax(0,1fr))} }
@media(max-width:760px){
  .numbered--5{grid-template-columns:repeat(2,minmax(0,1fr));column-gap:var(--s-17)}
}
/* No description under the title, so the title carries the whole item and
   should not sit hard against the rule above it. */
.numbered--5 .n-title{padding-bottom:var(--s-8)}

/* ==========================================================================
   BACK TO TOP — the same square as the WhatsApp button, opposite corner, so
   the two read as a pair. It earns its place only once there is a page to
   come back up, and it stands down while the consent card occupies the same
   corner rather than fighting it.
   ========================================================================== */
.totop{
  position:fixed;left:var(--gut);bottom:var(--s-25);z-index:200;
  width:56px;height:56px;padding:0;
  display:inline-flex;align-items:center;justify-content:center;
  background:var(--ink);color:var(--paper);
  border:1px solid var(--paper);
  border-radius:0;
  cursor:pointer;
  opacity:0;pointer-events:none;transform:translateY(20px);
  transition:opacity .25s ease,transform .3s cubic-bezier(.16,.84,.44,1),
             background .2s ease,color .2s ease;
}
.totop.is-on{opacity:1;pointer-events:auto;transform:translateY(0)}
.totop svg{width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:1.5}
.totop:focus-visible{background:var(--paper);color:var(--ink);border-color:var(--ink)}
@media (hover:hover) and (pointer:fine){
  .totop:hover{background:var(--paper);color:var(--ink);border-color:var(--ink)}
}
.totop:focus-visible{outline:2px solid var(--paper);outline-offset:3px}
/* The consent card and the open phone menu both take this corner. */
.consent-open .totop,.nav-open .totop{opacity:0;pointer-events:none;transform:translateY(20px)}
@media(max-width:760px){
  .totop{width:52px;height:52px;left:var(--s-17);bottom:var(--s-17)}
}
@media print{.totop{display:none}}
@media(prefers-reduced-motion:reduce){.totop{transition:opacity .25s ease}}

/* ==========================================================================
   FOOTER INSTAGRAM — the mark rather than the handle. Monochrome, because
   the two-colour rule holds everywhere except the one third-party control
   people need to recognise instantly.
   ========================================================================== */
/* The icon sits in a row of text links, so its box has to match their line
   rather than tower over it — a 44px box in a 22px line dragged the whole
   row's alignment down. The box is now the icon; the tap area is an
   invisible 44px square centred on it, which costs no layout. */
.foot-strip{align-items:center}
.ig{
  display:inline-flex;align-items:center;justify-content:center;
  width:22px;height:22px;margin:0;position:relative;
  color:inherit;
}
/* Hit area only. The 44px padding rule was excluded from .ig earlier because
   it pushed the icon out of line with the row; this gives the same target
   without moving anything. */
.ig::after{content:'';position:absolute;top:50%;left:50%;
  width:44px;height:44px;transform:translate(-50%,-50%)}
.ig::after{
  content:"";position:absolute;left:50%;top:50%;
  transform:translate(-50%,-50%);
  width:44px;height:44px;
}
.ig svg{width:21px;height:21px;fill:none;stroke:currentColor;stroke-width:1.6}
.ig:hover,.ig:focus-visible{opacity:.65}

/* The rest of a role, one tap away. Collapsed it is a single quiet line, so
   the page reads as a short list; open, the full description is on the page,
   which is what the JobPosting markup describes. */
.role-more{margin:var(--s-8) 0 0}
.role-more > summary{
  list-style:none;cursor:pointer;
  font-size:13px;line-height:1.5;opacity:.6;
  padding-block:var(--s-8);min-height:44px;
  display:flex;align-items:center;gap:8px;
}
.role-more > summary::-webkit-details-marker{display:none}
.role-more > summary::after{content:"+";font-size:15px;line-height:1}
.role-more[open] > summary::after{content:"\2212"}
.role-more > summary:hover,.role-more > summary:focus-visible{opacity:1}
.role-more p{margin:0 0 var(--s-8);font-size:15px;line-height:1.6;max-width:var(--measure)}

/* The opening section carries only the eyebrow and the headline, so it needs
   room above and almost none below — the photograph is the next thing and it
   should arrive while the headline is still on screen. */
.sec-hero{padding:var(--s-101) 0 var(--s-50)}
@media(max-width:760px){ .sec-hero{padding:var(--s-59) 0 var(--s-25)} }

.bleed-hero figcaption{
  font-size:13px;line-height:1.5;opacity:.55;
  padding:var(--s-11) var(--gut) 0;
}


/* ==========================================================================
   CLOSING CALL TO ACTION — a heading and a button, nothing else. The
   two-column section this used to borrow expects a paragraph beside the
   heading; with only a button there, the button parked at the top of a
   column three heading-lines tall and the section ran on for a screen.
   ========================================================================== */
.sec-cta{padding:var(--s-59) 0}
.cta-row{
  display:flex;flex-wrap:wrap;
  align-items:flex-end;
  justify-content:space-between;
  gap:var(--s-25) var(--s-50);
}
.cta-row h2{margin:0;flex:1 1 20ch;max-width:22ch}
.cta-row p{margin:0;flex:0 0 auto}
.cta-row .t-display{font-size:clamp(34px,4.8vw,70px);line-height:1.02}
@media(max-width:760px){
  .sec-cta{padding:var(--s-50) 0}
  .cta-row{gap:var(--s-25)}
  .cta-row h2{flex:1 1 100%;max-width:none}
}


/* ==========================================================================
   LIVE MODELS — the two WebGL blocks. Everything visible around the canvas
   is drawn in the same two colours as the rest of the page: a hairline box,
   a caption line above it, one quiet instruction below. The scene inside
   clears to --paper, so the canvas edge does not read as a panel sitting on
   the page.

   The canvas is sized entirely by CSS. The renderer reads clientWidth /
   clientHeight through a ResizeObserver, so these rules — not the script —
   decide the shape at every width.
   ========================================================================== */
.mdl-set{
  display:grid;
  grid-template-columns:repeat(3,1fr);
  gap:var(--s-25);
}
.mdl-set--one{grid-template-columns:1fr}

.mdl-panel{
  margin:0;
  border-top:1px solid var(--ink);
  padding-top:var(--s-13);
}
.mdl-cap{
  display:flex;flex-direction:column;gap:2px;
  padding-bottom:var(--s-13);
}
.mdl-cap-name{font-size:17px;line-height:1.4}
.mdl-cap-sub{font-size:13px;line-height:1.5;opacity:.55}

/* The box the scene lives in. The border is the frame; the canvas fills it.
   touch-action keeps a vertical swipe scrolling the page — without it the
   orbit control swallows the gesture and the block traps a phone reader. */
.mdl-stage{
  position:relative;
  aspect-ratio:3/4;
  border:1px solid var(--ink);
  background:var(--paper);
  overflow:hidden;
}
/* The furniture row is ten metres wide and knee-high. A 16:9 box framed it
   like a landscape photograph and left a screenful of empty air above the
   pieces, so this band is proportioned to what is actually in it. */
.mdl-stage--wide{aspect-ratio:7/2}
.mdl-stage canvas{
  display:block;width:100%;height:100%;
  outline:none;
  touch-action:pan-y;
}

.mdl-note{
  margin-top:var(--s-17);
  font-size:13px;line-height:1.5;opacity:.55;
  max-width:var(--measure);
}

/* Tablet: three portrait panels get too narrow to read, so the row becomes
   a single column of wider stages rather than a squeeze. */
@media(max-width:900px){
  .mdl-set{grid-template-columns:1fr;gap:var(--s-25)}
  .mdl-stage{aspect-ratio:4/3}
  .mdl-stage--wide{aspect-ratio:3/2}
}
@media(max-width:760px){
  .mdl-stage{aspect-ratio:1/1}
  .mdl-stage--wide{aspect-ratio:4/3}
}

/* A visitor who asked for less motion still gets the model and can turn it
   by hand; the script reads the same query and leaves the turntable off. */
@media(prefers-reduced-motion:reduce){
  .mdl-note{opacity:.55}
}


/* ==========================================================================
   QUESTIONS AND ANSWERS — a disclosure list, not an accordion widget.
   Each question is a real heading a reader can scan; the answer is in the
   document whether or not it is open, which is what matters both for someone
   using a screen reader and for an engine reading the page.
   ========================================================================== */
.qa-set{border-top:1px solid var(--ink)}
.qa{border-bottom:1px solid rgba(var(--ink-rgb),.18)}
.qa > summary{
  list-style:none;cursor:pointer;
  display:flex;align-items:flex-start;gap:var(--s-17);
  padding:var(--s-17) 0;
  font-size:20px;line-height:1.4;
  min-height:44px;
}
.qa > summary::-webkit-details-marker{display:none}
.qa > summary::after{
  content:"+";margin-left:auto;flex:0 0 auto;
  font-size:22px;line-height:1.2;opacity:.5;
}
.qa[open] > summary::after{content:"\2212"}
.qa > summary:hover,.qa > summary:focus-visible{opacity:.65}
.qa p{
  margin:0;padding:0 0 var(--s-25);
  font-size:17px;line-height:1.6;max-width:var(--measure);
  opacity:.78;
}
@media(max-width:760px){
  .qa > summary{font-size:18px;padding:var(--s-13) 0}
  .qa p{font-size:16px}
}
