/* ==================================================
   KONCAVE — shared stylesheet
   Palette and type are locked tokens. Do not hardcode
   colors or fonts outside :root.
   ================================================== */

/* Self-hosted fonts — no external requests, renders
   identically everywhere, no Google Fonts GDPR issue.

   Bilingual type: the Latin faces (Darker Grotesque / DM Mono) carry an
   explicit Latin unicode-range so they never claim Greek codepoints; the
   Greek faces below — Syne (display) and JetBrains Mono (mono),
   both declared under the SAME family names — cover the Greek block. The
   browser therefore renders Latin in Darker Grotesque/DM Mono and Greek in
   the matched faces automatically, with no per-page font switching. The
   "koncave" wordmark and Latin brand names stay in Darker Grotesque. */
@font-face{
  font-family:'Darker Grotesque';
  src:url('../fonts/darker-grotesque-400.woff2') format('woff2');
  font-weight:400; font-style:normal; font-display:swap;
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2190-2199,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face{
  font-family:'Darker Grotesque';
  src:url('../fonts/darker-grotesque-500.woff2') format('woff2');
  font-weight:500; font-style:normal; font-display:swap;
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2190-2199,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face{
  font-family:'Darker Grotesque';
  src:url('../fonts/darker-grotesque-600.woff2') format('woff2');
  font-weight:600; font-style:normal; font-display:swap;
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2190-2199,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face{
  font-family:'DM Mono';
  src:url('../fonts/dm-mono-400.woff2') format('woff2');
  font-weight:400; font-style:normal; font-display:swap;
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2190-2199,U+2212,U+2215,U+FEFF,U+FFFD;
}
@font-face{
  font-family:'DM Mono';
  src:url('../fonts/dm-mono-500.woff2') format('woff2');
  font-weight:500; font-style:normal; font-display:swap;
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2190-2199,U+2212,U+2215,U+FEFF,U+FFFD;
}
/* Greek — Syne (display) + JetBrains Mono (mono), Greek block only.

   These carry their OWN family names (not the Latin families') and sit FIRST
   in the font stacks below. That's deliberate: when a Greek variable face
   shares a family name AND weight range with the static Latin faces, the
   browser's per-character matcher can pick a Latin weight, find no Greek
   glyph, and drop to the system font instead of the Greek face — so Greek
   body text silently fell back. A separate family, first in the stack, means
   every Greek codepoint resolves here directly; Latin codepoints (absent from
   these Greek-only subsets) fall through to Darker Grotesque / DM Mono.

   Syne is shipped as two static instances so Greek never synthesises weight:
   the 400 file covers body/regular (element weight up to 500), the 500 file
   covers headings (element weight 600+). Syne widens with weight, so 500 is
   the "bold but not wider" heading weight — a touch heavier than the body,
   only ~9% wider (600 was +18% — too wide). size-adjust lands the Greek ink
   on the Latin so the two size-match. */
@font-face{
  font-family:'Syne Greek';
  src:url('../fonts/syne-greek-400.woff2') format('woff2');
  font-weight:100 500; font-style:normal; font-display:swap;
  size-adjust:85%;
  unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF;
}
@font-face{
  font-family:'Syne Greek';
  src:url('../fonts/syne-greek-500.woff2') format('woff2');
  font-weight:501 900; font-style:normal; font-display:swap;
  size-adjust:85%;
  unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF;
}
@font-face{
  font-family:'JetBrains Greek';
  src:url('../fonts/dm-mono-greek.woff2') format('woff2');
  font-weight:100 800; font-style:normal; font-display:swap;
  size-adjust:85%;
  unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF;
}

:root{
  --black:#141414;
  --white:#ffffff;
  /* was #8c8c8c in the mockup — darkened one step to pass
     WCAG AA (4.6:1) for the meta text it carries. */
  --grey:#757575;
  --grey-light:#ededed;
  --green:#b8ff4f;

  --font-display:'Syne Greek', 'Darker Grotesque', sans-serif;
  --font-mono:'JetBrains Greek', 'DM Mono', monospace;

  /* The numbered index rail (01, 02 …) is retired: 0 collapses its column
     everywhere --edge is used (the gallery break-out) without touching the
     markup the build still emits. */
  --edge:0px;
  /* content horizontal padding — grows with the screen, same both sides */
  --pad-x:clamp(20px, 5vw, 80px);

  /* type scale — one system across all pages */
  --t-hero:clamp(33px, 5.6vw, 57px);
  --t-pillar:clamp(35px, 5.8vw, 64px);   /* pillars: the entry to the work */
  --t-title:clamp(27px, 4.5vw, 48px);
  --t-marquee:clamp(24px, 3.2vw, 34px);
  --t-sub:clamp(19px, 1.9vw, 22px);
  --t-body:18px;
  /* 13px is the floor for any text on the site */
  --t-mono:14px;
  --t-mono-sm:13px;
}

*{ margin:0; padding:0; box-sizing:border-box; }

html{ scroll-behavior:smooth; }

body{
  background:var(--white);
  color:var(--black);
  font-family:var(--font-display);
  font-size:var(--t-body);
  -webkit-font-smoothing:antialiased;
  animation:page-in 0.35s ease both;
}
@keyframes page-in{ from{ opacity:0; } to{ opacity:1; } }

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

/* A touchscreen browser paints its own highlight over whatever you tap — a
   translucent wash, black on iOS, blue on Android. Laid over the lime it
   composited to a muddy olive (or a mint), which is why tapped words came up
   the wrong shade of green. Drop it; the :active states below acknowledge the
   tap instead, in the actual brand colour. */
html{ -webkit-tap-highlight-color:transparent; }

:focus-visible{
  outline:2px solid var(--black);
  outline-offset:3px;
}

::selection{ background:var(--green); color:var(--black); }

/* keyboard users land straight on content */
.skip-link{
  position:absolute;
  left:-9999px;
  top:0;
  z-index:10;
  font-family:var(--font-mono);
  font-size:13px;
  background:var(--black);
  color:var(--white);
  padding:10px 16px;
}
.skip-link:focus{ left:0; }

/* Darker Grotesque carries oversized descender space —
   display sizes need tight line-height or they float. */
.display{ line-height:0.95; font-weight:600; letter-spacing:-0.01em; }

/* ---------- page grid ---------- */

.section{
  display:grid;
  grid-template-columns:minmax(0, 1fr);
  border-top:0.5px solid var(--grey-light);
  position:relative;
  /* clears the sticky header (~86px open, ~46px once shrunk) so an anchor
     jump lands on the section's own top rule instead of behind the header */
  scroll-margin-top:96px;
}
.section.no-rule{ border-top:none; }
/* `hidden` has to be re-asserted here: the UA rule behind it is [hidden]
   {display:none}, which .section{display:grid} outranks — so a section
   marked hidden was still rendering, as an empty band with a dangling
   eyebrow and rail number. */
.section[hidden]{ display:none; }

/* section numbers — dropped from the design; the build still writes the
   rail divs, so they are hidden here */
.rail{ display:none; }

.section-inner{
  padding:clamp(32px, 4vw, 56px) var(--pad-x) clamp(64px, 8vw, 120px);
  min-width:0;
}

/* ---------- header ---------- */
/* logomark sits in the rail column: position 00 of the index */

/* Sticky: rides at the top, opaque so content passes underneath.
   JS toggles .scrolled (with hysteresis) — logomark shrinks and the
   bar compacts with a hairline. Motion honors the reduced-motion rule. */
header{
  position:sticky;
  top:0;
  z-index:20;
  background:var(--white);
  display:grid;
  grid-template-columns:auto 1fr;
  align-items:start;
  padding:24px 0 20px;
  border-bottom:0.5px solid transparent;
  transition:padding 0.3s ease, border-color 0.3s ease;
}
header.scrolled{
  padding:12px 0 10px;
  border-bottom-color:var(--grey-light);
}
/* logo + wordmark lockup. Sticky with the header: the "koncave" wordmark
   stays a constant size, while the mark shrinks on scroll (and stays
   visible the whole way down). */
.logomark{
  display:inline-flex;
  align-items:center;
  gap:9px;
  justify-self:start;
  color:var(--black);
  margin-left:var(--pad-x);   /* lines up with the content column */
  white-space:nowrap;
  transition:color 0.2s ease;
}
/* Proportion: the mark's height is set to sit just above the wordmark's
   cap height so the two read as one balanced lockup (the tall mark used to
   tower at ~2.4x the letters). width 20px -> ~32px tall ≈ the 30px wordmark. */
.logomark svg{ width:26px; height:auto; display:block; transition:width 0.3s ease; }
header.scrolled .logomark svg{ width:17px; }
.logomark:hover, .logomark:active{ color:var(--green); }
/* the wordmark is glued to the top of the page — it fades away as soon as
   you scroll, leaving just the (shrinking, sticky) mark */
.wordmark{
  font-family:var(--font-display);
  font-weight:600;
  font-size:38px;
  letter-spacing:-0.01em;
  line-height:1;
  /* Darker Grotesque's metrics seat the ink ~0.12em below the line-box
     centre, so the word reads low against the mark — lift it back to the
     optical centre. */
  transform:translateY(-0.12em);
  transition:opacity 0.25s ease;
}
header.scrolled .wordmark{ opacity:0; }
@media (max-width:640px){
  /* Two rows below 640px. Side by side, the lockup and the four nav items
     want about 390px between them and there isn't that much screen — which
     is why the wordmark used to print straight over "work". Stacking also
     keeps the header honest in Greek, where the same links run half again
     as long ("επικοινωνία" against "contact"). */
  header{
    grid-template-columns:1fr;
    align-items:start;
    row-gap:10px;
    padding:18px 0 12px;
  }
  header.scrolled{ row-gap:6px; padding:10px 0 8px; }
  .logomark{ gap:8px; }
  .logomark svg{ width:20px; }
  header.scrolled .logomark svg{ width:14px; }
  .wordmark{ font-size:28px; }
  .header-nav{
    justify-content:flex-start;
    gap:18px;
    font-size:13px;
    padding:0 var(--pad-x);
  }
}

.header-nav{
  display:flex;
  justify-content:flex-end;
  gap:24px;
  padding:4px var(--pad-x) 0 0;
  font-family:var(--font-mono);
  font-size:13px;
  color:var(--grey);
}
/* aria-current is "page" on the pages that still stand alone and "location"
   on the scrolling page, where js/main.js moves it as you scroll — match on
   the attribute rather than its value so both light up the same way. */
.header-nav a:hover, .header-nav a[aria-current]{ color:var(--black); }
.header-nav a[aria-current]{ border-bottom:2px solid var(--green); }

/* ΕΛ / EN language switch — sits at the end of the header nav */
.lang-switch{ display:inline-flex; align-items:baseline; gap:7px; }
.lang-switch .ls{ color:var(--grey); }
.lang-switch a.ls:hover{ color:var(--black); }
.lang-switch .ls.on{ color:var(--black); border-bottom:2px solid var(--green); }
.lang-switch .ls-sep{ color:var(--grey); }

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

/* Flush left, but wide: at 26ch the statement runs two lines instead of the
   five it took at 16ch, which lifts the work section (and its image) into the
   first screen. The break point is hand-placed with a <br> in the copy —
   admin.py's sanitize_rich() keeps <strong>/<em>/<br>, so it survives editing
   in the admin. */
.statement{
  font-size:var(--t-hero);
  max-width:26ch;
  text-wrap:balance;
}
#hero .section-inner{ padding-bottom:clamp(40px, 5vw, 64px); }
#services .section-inner{ padding-bottom:clamp(48px, 6vw, 88px); }

.statement-sub{
  margin-top:20px;
  font-size:16px;
  color:var(--grey);
}

/* ---------- 02 services marquee ---------- */

.eyebrow{
  font-family:var(--font-mono);
  font-size:var(--t-mono-sm);
  color:var(--grey);
  margin-bottom:20px;
}

.marquee-line{
  font-size:var(--t-marquee);
  line-height:1.1;
  font-weight:500;
  display:flex;
  align-items:baseline;
  gap:0.35em;
  flex-wrap:wrap;
}
.marquee-word{
  border-bottom:0.09em solid var(--green);
  padding-bottom:0.04em;
  display:inline-block;
  min-width:1ch;
  transition:opacity 0.2s ease;
  cursor:pointer;
}
.marquee-word:hover, .marquee-word:active{ background:var(--green); border-bottom-color:var(--black); }

/* ---------- 03 pillars — full-bleed image, quiet type over it ---------- */

/* isolation + own background: the image layer sits at z-index -1 and must
   stay inside this group instead of dropping behind the page. */
.pillars-section{
  overflow:hidden;
  isolation:isolate;
  background:var(--white);
}

.pillar-bgs{
  position:absolute;
  inset:0;
  z-index:-1; /* under the text, above the section background */
}
.pillar-bg{
  position:absolute;
  inset:0;
  opacity:0;
  transition:opacity 0.4s ease;
  background:var(--grey); /* placeholder — swap for project image */
}
.pillar-bg img{ width:100%; height:100%; object-fit:cover; display:block; }
/* a soft shade from the left, where the type sits — enough to hold white
   type on a light photo, too little to read as a dark overlay */
.pillar-bg::after{
  content:"";
  position:absolute;
  inset:0;
  background:linear-gradient(90deg, rgba(20,20,20,0.42) 0%, rgba(20,20,20,0.18) 45%, rgba(20,20,20,0) 75%);
}
.pillars-section[data-active="boats"] .pillar-bg[data-for="boats"],
.pillars-section[data-active="objects"] .pillar-bg[data-for="objects"],
.pillars-section[data-active="materials"] .pillar-bg[data-for="materials"]{
  opacity:1;
}
/* The page opens with the boats image already flooding the section, in full
   colour, so it never paints as a blank wall (js/main.js sets it). Hovering a
   pillar swaps the image; leaving falls back to boats rather than to nothing.

   Legibility note for whoever changes these photos: the type is plain white
   with a faint shadow, over the left-hand shade above. A photo whose left
   third is very bright (white wall, sky) still needs checking. */

/* Black on the white page with JS off (no image). Once an image floods in
   ([data-active]), the type turns white. The subtle part: while a pillar is
   engaged — hovered, focused, or tapped on a touch screen (js/main.js sets
   .engaged) — the other two dim and the active one stays full strength.
   Driven by a class, not :hover, because touch screens have no hover. */
.pillar{
  display:block;
  padding:14px 0;
  font-size:var(--t-pillar);
  line-height:0.95;
  font-weight:600;
  letter-spacing:-0.01em;
  color:var(--black);
  width:fit-content;
  transition:opacity 0.3s ease;
}
.pillars-section[data-active] .pillar{
  color:var(--white);
  text-shadow:0 1px 2px rgba(0,0,0,0.18), 0 0 28px rgba(0,0,0,0.22);
}
.pillars-section.engaged .pillar{ opacity:0.55; }
.pillars-section.engaged[data-active="boats"] .pillar[data-pillar="boats"],
.pillars-section.engaged[data-active="objects"] .pillar[data-pillar="objects"],
.pillars-section.engaged[data-active="materials"] .pillar[data-pillar="materials"]{ opacity:1; }
/* ---------- 04 studio ---------- */

.studio-text{
  max-width:52ch;
  font-size:var(--t-sub);
  font-weight:500;
  line-height:1.4;
}
/* the studio lede and the operating-model sentence are two paragraphs */
.side-lede + .studio-text{ margin-top:1em; }
.studio-clients{
  margin-top:56px;
  font-family:var(--font-mono);
  font-size:var(--t-mono);
  color:var(--grey);
  line-height:2;
}
.studio-clients .label{
  font-size:var(--t-mono-sm);
  display:block;
  margin-bottom:6px;
}
.studio-clients span{ color:var(--black); }
.studio-clients a{ border-bottom:1px solid var(--grey); }
.studio-clients a:hover, .studio-clients a:active{ border-bottom-color:var(--green); }

/* The image now floods the whole section from first paint, rail column
   included, so the labels that were designed as grey-on-white have to hold up
   on a photograph. Scoped to [data-active] so that with JS off — no image,
   white background — they stay as they were. */
.pillars-section[data-active] .eyebrow{ color:rgba(255,255,255,0.8); }
.pillars-section[data-active] .side-peek{ color:var(--white); }
.pillars-section[data-active] .side-peek:hover,
.pillars-section[data-active] .side-peek:active{
  color:var(--green);
  border-bottom-color:var(--green);
}

/* quiet link under the work pillars through to the side projects */
.side-peek{
  display:inline-block;
  margin-top:28px;
  font-family:var(--font-mono);
  font-size:13px;
  color:var(--grey);
  position:relative; /* sit above the pillar-bg flood layer */
  z-index:1;
}
.side-peek:hover, .side-peek:active{ color:var(--black); border-bottom:2px solid var(--green); }

/* the way through to the studio page — a clear, findable link, not a
   whisper: display face, sized up, with a green baseline and a nudging arrow */
.studio-link{
  display:inline-flex;
  align-items:baseline;
  gap:10px;
  margin-top:44px;
  font-family:var(--font-display);
  font-size:clamp(20px, 2.2vw, 26px);
  font-weight:600;
  letter-spacing:-0.01em;
  line-height:1.05;
  color:var(--black);
  border-bottom:3px solid var(--green);
  padding-bottom:4px;
}
.studio-arrow{ transition:transform 0.3s ease; }
.studio-link:hover .studio-arrow{ transform:translateX(6px); }

/* ---------- studio / founder page ---------- */

.founder-name{
  font-size:var(--t-sub);
  font-weight:500;
  line-height:1.1;
  margin-bottom:16px;
}
.founder-bio{
  max-width:60ch;
  line-height:1.6;
}
.founder-bio p + p{ margin-top:1em; }

/* the founder section is deliberately spare — name, a one-line role, and a
   link to the full CV as a PDF. The work across the site is the real CV. */
.founder-role{
  margin-top:14px;
  font-family:var(--font-mono);
  font-size:var(--t-mono);
  color:var(--grey);
}
/* The CV reads on the page instead of downloading as a PDF: the points a
   client actually weighs, one per line, with the year in mono so the column
   scans. Rows come from studio.cv in the admin.

   It sits behind a <details> on the scrolling page — eleven dense rows is a
   lot to put between a visitor and the rest of the studio, and someone who
   wants the CV is looking for it. <details> and not JS: the CSP forbids
   inline script, and this is keyboard-accessible and printable for free. */
.cv-toggle{ margin-top:30px; }
.cv-toggle > summary{
  display:inline-block;      /* drops the default disclosure triangle */
  position:relative;
  cursor:pointer;
  font-family:var(--font-mono);
  font-size:var(--t-mono-sm);
  color:var(--grey);
  border-bottom:1px solid var(--grey-light);
  padding-bottom:2px;
  transition:color 0.2s ease, border-color 0.2s ease;
}
.cv-toggle > summary::marker,
.cv-toggle > summary::-webkit-details-marker{ display:none; content:""; }
/* the label is one mono line (~17px), which is a mean thing to ask a thumb to
   hit — this grows the hit area to ~45px without moving the underline off the
   text the way vertical padding would */
.cv-toggle > summary::before{
  content:"";
  position:absolute;
  left:0; right:0; top:-14px; bottom:-14px;
}
.cv-toggle > summary::after{ content:" +"; }
.cv-toggle[open] > summary::after{ content:" −"; }
.cv-toggle > summary:hover{ color:var(--black); border-bottom-color:var(--green); }
.cv-toggle > summary:focus-visible{ outline:2px solid var(--black); outline-offset:3px; }
.cv-toggle .cv-list{ margin-top:22px; }

.cv-list{
  list-style:none;
  margin-top:30px;
  max-width:76ch;
}
.cv-list li{
  position:relative;
  padding:0 0 13px 22px;
  line-height:1.4;
}
.cv-list li::before{
  content:"";
  position:absolute;
  left:0;
  top:0.52em;
  width:7px;
  height:7px;
  background:var(--green);
}
.cv-yr{
  display:inline-block;
  min-width:11ch;
  font-family:var(--font-mono);
  font-size:var(--t-mono-sm);
  color:var(--grey);
  margin-right:10px;
}
@media (max-width:640px){
  .cv-list li{ padding-left:18px; padding-bottom:16px; }
  .cv-yr{ display:block; min-width:0; margin:0 0 3px; }
}

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

.footer-grid{
  display:grid;
  grid-template-columns:1fr 1fr;
  gap:clamp(56px, 6vw, 96px);
}
@media (max-width:780px){ .footer-grid{ grid-template-columns:1fr; } }

.footer-meta{
  font-family:var(--font-mono);
  font-size:var(--t-mono);
  color:var(--grey);
  line-height:2;
}
.footer-meta a{ color:var(--black); }
.footer-meta a:hover, .footer-meta a:active{ background:var(--green); }

/* A live Google Maps embed (chosen over the plain link-out box it replaced).
   Trade-off, accepted knowingly: Google's frame loads its own scripts and
   sets cookies. loading="lazy" on the iframe keeps that off the first screen
   — nothing is requested until the footer is near. The box is fixed-ratio,
   so the late load can't shift the layout. Greyscale to sit in the
   black-and-white site; colour comes back on hover. */
.map-embed{
  width:100%;
  max-width:380px;
  aspect-ratio:16/11;
  border:0.5px solid var(--grey-light);
  margin-top:24px;
  background:var(--grey-light);
}
.map-frame{
  display:block;
  width:100%;
  height:100%;
  border:0;
  filter:grayscale(1);
  transition:filter 0.3s ease;
}
.map-embed:hover .map-frame{ filter:none; }
.map-link{
  display:inline-block;
  margin-top:10px;
  font-family:var(--font-mono);
  font-size:var(--t-mono-sm);
  color:var(--grey);
}
.map-link:hover, .map-link:active{ color:var(--black); border-bottom:2px solid var(--green); }
/* the map waits for cookie consent (js/main.js). Until then the frame has
   no src and this button fills the same box */
.map-frame:not([src]){ display:none; }
.map-consent{
  width:100%;
  height:100%;
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  gap:6px;
  background:var(--grey-light);
  border:0;
  cursor:pointer;
  font-family:var(--font-mono);
  font-size:var(--t-mono);
  color:var(--black);
  transition:background 0.2s ease;
}
.map-consent[hidden]{ display:none; }
.map-consent:hover, .map-consent:active{ background:var(--green); }
.map-consent-note{ font-size:var(--t-mono-sm); color:var(--grey); }
.map-consent:hover .map-consent-note{ color:var(--black); }

/* ---------- cookie consent ----------
   A quiet card in the corner, not a wall: the page stays usable behind it.
   Same parts as the rest of the site — mono type, black on white, the
   contact form's button with its green hover. */
.consent{
  position:fixed;
  left:var(--pad-x);
  bottom:24px;
  z-index:50;                 /* over the header (20) and TOP (30), under the lightbox (60) */
  width:min(560px, calc(100vw - 2 * var(--pad-x)));
  background:var(--white);
  border:1px solid var(--black);
  padding:20px 24px 22px;
  font-family:var(--font-mono);
  font-size:var(--t-mono-sm);
  line-height:1.55;
  color:var(--black);
  animation:consent-in 0.25s ease both;
}
@keyframes consent-in{ from{ opacity:0; transform:translateY(8px); } }
.consent-title{ font-size:var(--t-mono); margin:0 0 6px; }
.consent-text{ margin:0; color:var(--grey); }
.consent-actions{ display:flex; flex-wrap:wrap; gap:8px; margin-top:18px; }
.consent-actions button{
  font-family:var(--font-mono);
  font-size:13px;
  letter-spacing:0.5px;
  padding:11px 18px;
  background:var(--white);
  color:var(--black);
  border:1px solid var(--black);
  border-radius:0;
  cursor:pointer;
  transition:background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
}
.consent-actions .consent-all{ background:var(--black); color:var(--white); }
.consent-actions button:hover, .consent-actions button:active{
  background:var(--green); color:var(--black); border-color:var(--green);
}
.consent-actions button:focus-visible{ outline:2px solid var(--black); outline-offset:2px; }
@media (max-width:480px){
  .consent{ left:16px; bottom:16px; width:calc(100vw - 32px); padding:18px 18px 20px; }
  .consent-actions button{ flex:1 1 auto; }
}
@media (prefers-reduced-motion: reduce){ .consent{ animation:none; } }
/* desk for hire — a quiet line under the address and map, not a headline */
.desk-link{
  display:table;
  margin-top:28px;
  font-family:var(--font-mono);
  font-size:var(--t-mono-sm);
  color:var(--black);
  border-bottom:1px solid var(--grey-light);
}
.desk-link:hover, .desk-link:active{ border-bottom:2px solid var(--green); }

.contact-title{
  font-size:var(--t-title);
  text-wrap:balance;
  line-height:0.95;
  font-weight:600;
  margin-bottom:8px;
}

.contact-form{ display:flex; flex-direction:column; }
.contact-form label{
  font-family:var(--font-mono);
  font-size:var(--t-mono-sm);
  color:var(--grey);
  margin-top:24px;
}
.contact-form input,
.contact-form textarea{
  font-family:var(--font-display);
  font-size:20px;
  color:var(--black);
  background:transparent;
  border:none;
  border-bottom:0.5px solid var(--grey-light);
  padding:10px 0;
  outline:none;
  resize:none;
  border-radius:0;
}
.contact-form input:focus,
.contact-form textarea:focus{ border-bottom-color:var(--black); }
.contact-form button{
  margin-top:32px;
  align-self:flex-start;
  font-family:var(--font-mono);
  font-size:13px;
  letter-spacing:0.5px;
  background:var(--black);
  color:var(--white);
  border:none;
  padding:14px 28px;
  cursor:pointer;
  transition:background 0.2s ease, color 0.2s ease;
}
.contact-form button:hover, .contact-form button:active{ background:var(--green); color:var(--black); }
.contact-form button:focus-visible{ outline-color:var(--green); }
.contact-form button[disabled]{ opacity:0.5; cursor:wait; }

/* honeypot field — invisible to people, tempting to bots */
.hp-field{ position:absolute; left:-9999px; }

.form-status{
  margin-top:16px;
  font-family:var(--font-mono);
  font-size:var(--t-mono);
  min-height:1.5em;
}
.form-status.ok{ background:var(--green); color:var(--black); padding:2px 8px; width:fit-content; }
.form-status.err{ color:var(--black); }

.footer-bottom{
  margin-top:88px;
  display:flex;
  justify-content:space-between;
  align-items:flex-end;
  font-family:var(--font-mono);
  font-size:var(--t-mono-sm);
  color:var(--grey);
}
.footer-bottom .logomark{ margin-left:0; color:var(--grey); }
.footer-bottom .logomark svg{ width:14px; }
.footer-bottom .logomark:hover{ color:var(--black); }

/* ---------- page titles + the project index (home #projects) ---------- */

.page-title{
  font-size:var(--t-title);
  text-wrap:balance;
  line-height:0.95;
  font-weight:600;
}

.filters{
  display:flex;
  gap:20px;
  flex-wrap:wrap;
  margin-top:28px;
  font-family:var(--font-mono);
  font-size:13px;
  color:var(--grey);
}
.filters button{
  font:inherit;
  color:inherit;
  background:none;
  border:none;
  cursor:pointer;
  padding:2px 0;
}
.filters button[aria-pressed="true"]{
  color:var(--black);
  border-bottom:2px solid var(--green);
}
.filters button:hover{ color:var(--black); }

.active-service{
  font-family:var(--font-mono);
  font-size:13px;
  color:var(--black);
  background:var(--green);
  padding:2px 8px;
  cursor:pointer;
  border:none;
}

.filter-note{
  margin-top:20px;
  font-family:var(--font-mono);
  font-size:var(--t-mono);
  color:var(--grey);
}
.filter-note[hidden]{ display:none; }

/* the index is a grid of square thumbnails — uniform, not scattered, with
   generous air between cards (more between rows, where each name sits under
   its square). The gap is plain whitespace, not a coloured background
   peeking through: a trailing row with fewer projects than columns leaves
   empty grid cells, and a coloured container would show there as a slab. */
.project-list{
  list-style:none;
  margin-top:64px;
  display:grid;
  grid-template-columns:repeat(3, 1fr);
  column-gap:clamp(16px, 2.4vw, 40px);
  row-gap:clamp(40px, 4.5vw, 72px);
  background:var(--white);
}
.project-list > li{ margin:0; }
.project-list > li[hidden]{ display:none; }

a.project-row{ display:block; color:inherit; background:var(--white); }
.project-row .num{ display:none; }        /* the index isn't a sequence */

/* thumbnails crop to a square — object-fit:cover on the existing optimized
   image, no new image processing. Which source image fills this box is a
   build-time / admin choice (project_thumb() in build_images.py); the crop
   itself is purely this CSS. */
a.project-row .thumb{
  display:block;
  aspect-ratio:1;
  overflow:hidden;
  background:var(--grey-light);
}
a.project-row .thumb.thumb-empty{ aspect-ratio:1; }
a.project-row .thumb img{
  width:100%; height:100%; display:block;
  object-fit:cover;
  transition:transform 0.6s cubic-bezier(.2,.7,.2,1), opacity 0.3s ease;
}
a.project-row:hover .thumb img{ transform:scale(1.03); opacity:0.92; }

.project-name{
  display:block;
  margin-top:16px;
  font-size:20px;
  font-weight:500;
  line-height:1.05;
}
a.project-row:hover .project-name, a.project-row:active .project-name{ border-bottom:2px solid var(--green); width:fit-content; }
.project-row .meta-row{
  display:flex;
  gap:14px;
  margin-top:5px;
  font-family:var(--font-mono);
  font-size:var(--t-mono-sm);
  color:var(--grey);
}
.project-row .tag, .project-row .year{ font-family:var(--font-mono); }

@media (max-width:900px){
  .project-list{ grid-template-columns:repeat(2, 1fr); }
}
@media (max-width:640px){
  .project-list{ grid-template-columns:1fr; }
  .project-row .tag{ display:none; }
}

/* index coda — side projects are kept out of the grid above and live here,
   a quiet aside pulled to the far edge (breaking the left rag) so it reads
   as a back room you discover, not another project competing for attention. */
.index-aside{
  margin-top:104px;
  padding-top:36px;
  border-top:0.5px solid var(--grey-light);
  display:flex;
  justify-content:flex-end;
}
.aside-link{
  display:block;
  text-align:right;
  max-width:40ch;
}
.aside-eyebrow{
  display:block;
  font-family:var(--font-mono);
  font-size:var(--t-mono-sm);
  color:var(--grey);
  margin-bottom:12px;
}
.aside-title{
  display:block;
  font-family:var(--font-display);
  font-size:clamp(26px, 3.6vw, 40px);
  font-weight:600;
  letter-spacing:-0.015em;
  line-height:0.98;
}
.aside-arrow{
  display:inline-block;
  color:var(--grey);
  transition:transform 0.3s ease, color 0.2s ease;
}
.aside-link:hover .aside-title, .aside-link:active .aside-title{ text-decoration:underline; text-decoration-color:var(--green); text-underline-offset:6px; text-decoration-thickness:2px; }
.aside-link:hover .aside-arrow{ transform:translate(5px, -5px); color:var(--black); }
.aside-note{
  display:block;
  margin-top:12px;
  font-family:var(--font-mono);
  font-size:var(--t-mono-sm);
  color:var(--grey);
}
@media (max-width:640px){
  .index-aside{ justify-content:flex-start; margin-top:64px; }
  .aside-link{ text-align:left; }
}

/* ---------- project page ---------- */

.back-link{
  font-family:var(--font-mono);
  font-size:13px;
  color:var(--grey);
  display:inline-block;
  margin-bottom:28px;
}
.back-link:hover{ color:var(--black); }

.project-title{
  font-size:var(--t-title);
  line-height:0.95;
  font-weight:600;
  max-width:14ch;
  text-wrap:balance;
}

.project-meta{
  display:grid;
  grid-template-columns:repeat(auto-fit, minmax(140px, max-content));
  gap:12px 48px;
  margin-top:44px;
  font-family:var(--font-mono);
  font-size:var(--t-mono);
}
.project-meta dt{ color:var(--grey); font-size:var(--t-mono-sm); margin-bottom:4px; }
.project-meta dd{ color:var(--black); }

.project-body{
  margin-top:64px;
  max-width:62ch;
  line-height:1.6;
}
/* an unfilled content slot (e.g. studio.shape before it is written)
   collapses instead of leaving a blank gap */
.studio-text:empty{ display:none; }
.project-body p + p{ margin-top:1em; }

/* ---------- project gallery — full-bleed carousel ----------
   The gallery breaks out of the section grid to the full width of the
   viewport and scrolls sideways, one image at a time, with the previous and
   next slide showing at either edge. Every image in the project lives in the
   one track, so nothing is buried behind a click and 28 images cost the page
   no vertical room.

   Sizes are caps, not frames: max-height AND max-width are set on the image
   with width/height auto, so each one grows to whichever limit it hits first
   and keeps its own proportions — a panorama is short and wide, a portrait
   tall and narrow, neither cropped nor letterboxed. The slide is a fixed
   height, so the track never jumps between them, and the caption is pinned
   to the bottom row so captions line up across slides.

   The break-out uses the grid's own tokens rather than 100vw: the content box
   starts exactly --edge + --pad-x from the left of the viewport, so negative
   margins of that size reach both edges without the scrollbar pushing the
   page into horizontal overflow. */
.project-images{
  /* Image height cap; the slide height derives from it. The vw term matters
     on narrow screens, where width is the binding constraint for nearly every
     image — without it the band stays viewport-tall while the pictures inside
     it are short, and each slide floats in a field of white. */
  --gh:min(72vh, 720px);
  margin-top:56px;
  margin-left:calc(-1 * (var(--edge) + var(--pad-x)));
  margin-right:calc(-1 * var(--pad-x));
}

.gallery-track{
  display:flex;
  align-items:stretch;
  gap:40px;
  overflow-x:auto;
  overscroll-behavior-x:contain;
  min-width:0;
  cursor:grab;
  /* the first image lines up with the title and copy above it; the track runs
     off the right edge of the screen, which is the thing that says there is
     more this way */
  padding-inline:calc(var(--edge) + var(--pad-x));
  scrollbar-width:none;
  -ms-overflow-style:none;
}
.gallery-track::-webkit-scrollbar{ display:none; }

/* a titled gallery (a page with more than one, e.g. Studies): the title sits
   on the content edge, and each gallery after the first gets clear air above */
.gallery-label{
  padding-inline:calc(var(--edge) + var(--pad-x));
  margin:0 0 20px;
  font-family:var(--font-mono);
  font-size:var(--t-mono);
  color:var(--black);
}
.gallery + .gallery-label{ margin-top:clamp(48px, 5vw, 80px); }
.gallery-track:focus-visible{ outline:2px solid var(--black); outline-offset:-2px; }

.gslide{
  flex:0 0 auto;
  height:calc(var(--gh) + 56px);
  display:grid;
  grid-template-rows:1fr auto;
  justify-items:start;
}
/* Width is definite — derived from the slide's own ratio — so the box is
   correct before a lazy image arrives and nothing shifts as you scroll. The
   height follows from aspect-ratio and can never exceed --gh, so an image is
   capped on whichever axis it hits first and is neither cropped nor
   letterboxed. The viewport cap is what leaves the neighbouring slides
   showing at either edge. */
.gslide img,
.gslide video{
  display:block;
  align-self:center;
  --gw:min(74vw, 1180px);
  width:min(calc(var(--ar, 1.5) * var(--gh)), var(--gw));
  aspect-ratio:var(--ar, 1.5);
  height:auto;
}
/* a percentage width doesn't feed the slide's intrinsic size, so a long
   caption can't stretch a narrow slide */
.gslide figcaption{
  width:100%;
  margin-top:12px;
  min-height:2.2em;
  font-family:var(--font-mono);
  font-size:var(--t-mono-sm);
  color:var(--grey);
  line-height:1.35;
}


@media (max-width:780px){
  .project-images{ --gh:min(50vh, 76vw); }
  .gallery-track{ gap:20px; }   /* padding-inline stays on the content edge */
  .gslide img, .gslide video{ --gw:88vw; }
}
/* the 6-track grid below lays out the home page's "the space" section */
.project-images.img-grid{
  margin-top:72px;
  margin-left:0;
  margin-right:0;
  display:grid;
  grid-template-columns:repeat(6, 1fr);
  align-items:start;
  column-gap:40px;
  row-gap:88px;
}
.img-slot{ grid-column:span 6; }
.img-slot img{ width:100%; height:auto; display:block; }
/* the studio photo is portrait: at full column width it ran ~1200px tall on
   a desktop — cap it at most of a screen */
#space .img-slot img{ width:auto; max-width:100%; max-height:80vh; }
.img-slot figcaption{
  font-family:var(--font-mono);
  font-size:var(--t-mono-sm);
  color:var(--grey);
  margin-top:10px;
}
/* unfilled slots keep a labelled grey box at a tidy ratio */
.img-slot[data-slot]{
  aspect-ratio:3/2;
  background:var(--grey-light);
  display:flex;
  align-items:center;
  justify-content:center;
  font-family:var(--font-mono);
  font-size:var(--t-mono-sm);
  color:var(--grey);
}

.gcol-1{ grid-column:span 1; }
.gcol-2{ grid-column:span 2; }
.gcol-3{ grid-column:span 3; }
.gcol-4{ grid-column:span 4; }
.gcol-5{ grid-column:span 5; }
.gcol-6{ grid-column:span 6; }
.gstagger{ margin-top:56px; }

@media (max-width:780px){
  .project-images.img-grid{ grid-template-columns:1fr; column-gap:0; row-gap:44px; }
  .img-slot{ grid-column:span 1; margin-top:0; }
}


.project-nav{
  margin-top:88px;
  display:flex;
  justify-content:space-between;
  font-family:var(--font-mono);
  font-size:13px;
  color:var(--grey);
}
.project-nav a:hover{ color:var(--black); }

/* ---------- side projects ----------
   Deliberately quieter than the work archive: a per-item mono
   rail (year · tag) echoes the section rail, content sits right.
   No display-size type here — this reads as marginalia. */

.side-lede{
  max-width:54ch;
  font-size:var(--t-sub);
  font-weight:500;
  line-height:1.4;
  margin-top:20px;
  color:var(--grey);
}
.side-lede strong{ color:var(--black); font-weight:500; }

.side-list{ list-style:none; margin-top:56px; }

/* Each side project is a full-width block: a compact header (meta line +
   big title + short note), then the pinboard spanning the whole wall. */
.side-item{
  padding:72px 0 96px;
  border-top:0.5px solid var(--grey-light);
}
.side-item:last-child{ border-bottom:0.5px solid var(--grey-light); }

.side-meta{
  font-family:var(--font-mono);
  font-size:var(--t-mono-sm);
  color:var(--grey);
  display:flex;
  gap:14px;
  margin-bottom:12px;
}
.side-meta .yr{ color:var(--black); }

.side-title{
  font-size:clamp(26px, 3.6vw, 40px);
  font-weight:600;
  letter-spacing:-0.015em;
  line-height:0.98;
  margin-bottom:16px;
}
/* header text stays readable-width; the board below goes full width */
.side-desc{ max-width:46ch; line-height:1.6; color:var(--grey); }
.side-desc:empty{ display:none; }
.side-desc strong{ color:var(--black); font-weight:500; }

.side-link{
  display:inline-block;
  margin-top:16px;
  font-family:var(--font-mono);
  font-size:13px;
  color:var(--grey);
  border-bottom:2px solid var(--green);
}
.side-link:hover{ color:var(--black); }

/* pinboard — small images pinned to a white wall. Column masonry keeps
   them flowing without gaps; per-pin width/offset variance (assigned,
   seeded, by the build script) breaks the grid so it reads as a scattered
   collection of ideas with plenty of air around each one. */
.note-gallery{
  margin-top:44px;
  column-count:5;
  column-gap:44px;
}
.pin{
  break-inside:avoid;
  margin:0 0 44px;
  width:100%;
}
.pin img{ display:block; width:100%; height:auto; }

/* the pin itself is the button — pins are small by design, so clicking one
   opens it full size in the lightbox below */
.pin-zoom{
  display:block;
  width:100%;
  padding:0;
  border:0;
  background:none;
  cursor:zoom-in;
}
.pin-zoom img{ transition:opacity 0.2s ease; }
.pin-zoom:hover img{ opacity:0.82; }
.pin-zoom:focus-visible{ outline:2px solid var(--black); outline-offset:4px; }
.pin figcaption{
  font-family:var(--font-mono);
  font-size:var(--t-mono-sm);
  color:var(--grey);
  margin-top:8px;
}
/* size + placement variance */
.pin.w-85{ width:85%; }
.pin.w-70{ width:70%; }
.pin.w-55{ width:55%; }
.pin.al-r{ margin-left:auto; }          /* narrower pins drift right … */
.pin.al-c{ margin-left:auto; margin-right:auto; }  /* … or centre */
.pin.nudge{ margin-top:40px; }          /* … or drop down the wall */

@media (max-width:1100px){
  .note-gallery{ column-count:4; }
}
@media (max-width:820px){
  .note-gallery{ column-count:3; column-gap:32px; }
}
@media (max-width:560px){
  .side-item{ padding:36px 0 44px; }
  .note-gallery{ column-count:2; column-gap:20px; }
  .pin{ margin-bottom:22px; }
  .pin.w-85, .pin.w-70, .pin.w-55{ width:100%; }
  .pin.al-r, .pin.al-c{ margin-left:0; margin-right:0; }
  .pin.nudge{ margin-top:0; }
}

/* ---------- lightbox ----------
   Opened from a side-project pin. Injected by js/main.js on first use, so
   there is no dead markup on pages that never open one. Click anywhere, Esc,
   or the close button dismisses it; the arrow keys step through the rest of
   that project's wall without closing. */
.lightbox{
  position:fixed;
  inset:0;
  z-index:60;
  display:flex;
  flex-direction:column;
  align-items:center;
  justify-content:center;
  gap:18px;
  padding:64px 24px 40px;
  background:rgba(20, 20, 20, 0.95);
  cursor:zoom-out;
}
.lightbox[hidden]{ display:none; }
.lightbox img{
  max-width:min(94vw, 1600px);
  max-height:78vh;
  width:auto;
  height:auto;
  display:block;
  cursor:default;
}
.lightbox figcaption{
  font-family:var(--font-mono);
  font-size:var(--t-mono);
  color:rgba(255, 255, 255, 0.72);
  text-align:center;
  max-width:60ch;
  line-height:1.4;
}
.lightbox figcaption:empty{ display:none; }
.lb-close{
  position:absolute;
  top:18px;
  right:22px;
  font-family:var(--font-mono);
  font-size:13px;
  line-height:1;
  color:var(--white);
  background:none;
  border:none;
  padding:10px 14px;
  cursor:pointer;
  transition:background 0.2s ease, color 0.2s ease;
}
.lb-close:hover, .lb-close:active{ background:var(--green); color:var(--black); }
.lb-close:focus-visible{ outline:2px solid var(--green); outline-offset:2px; }
.lb-count{
  position:absolute;
  top:22px;
  left:22px;
  font-family:var(--font-mono);
  font-size:var(--t-mono-sm);
  color:rgba(255, 255, 255, 0.55);
  letter-spacing:0.06em;
}
@media (max-width:640px){
  .lightbox{ padding:56px 12px 28px; }
  .lightbox img{ max-width:96vw; max-height:70vh; }
}

/* ---------- 404 ---------- */

.notfound{
  min-height:50vh;
}
.notfound .page-title{ margin-bottom:16px; }
.notfound p{ color:var(--grey); }
.notfound a{ border-bottom:2px solid var(--green); }

/* ---------- back-to-top ----------
   Injected by js/main.js on every page. Sits quietly out of the way and
   fades in only once you've scrolled, returning you to the header nav. */
/* the button is the logomark, quarter-turn clockwise, with TOP over it —
   no box, no border, it just floats bottom-right and fades in on scroll */
.to-top{
  position:fixed;
  right:22px;
  bottom:22px;
  z-index:30;
  display:flex;
  flex-direction:column;
  align-items:center;
  padding:6px 8px;
  background:none;
  border:none;
  cursor:pointer;
  opacity:0;
  visibility:hidden;
  transform:translateY(10px);
  transition:opacity 0.25s ease, transform 0.25s ease, visibility 0.25s;
}
.to-top.show{ opacity:1; visibility:visible; transform:none; }
.to-top:focus-visible{ outline:2px solid var(--black); outline-offset:4px; }
.to-top-label{
  font-family:var(--font-mono);
  font-size:13px;
  font-weight:500;
  letter-spacing:0.06em;
  color:var(--black);
}
/* TOP sits above the mark. The mark is rotated a quarter-turn CW; because a
   rotated element keeps its upright layout box (tall + narrow), negative
   vertical margins trim that reserved space so it stacks tightly under TOP. */
.to-top-mark{
  width:22px;
  height:auto;
  margin:-3px 0;
  color:var(--black);
  transform:rotate(90deg);
  transition:color 0.2s ease;
}
.to-top:hover .to-top-mark, .to-top:active .to-top-mark{ color:var(--green); }
@media (max-width:640px){
  .to-top{ right:16px; bottom:16px; }
}

/* ---------- motion preferences ---------- */

@media (prefers-reduced-motion: reduce){
  html{ scroll-behavior:auto; }
  *{ transition:none !important; animation:none !important; }
}
