/* ============================================================================
   Public site stylesheet.

   Ports the accepted visual language from /v2/mockup.html. Colours, fonts and
   the page/gutter custom properties come from tokens.css (loaded first) — do
   not redefine them here. box-sizing and prefers-reduced-motion are also
   handled by tokens.css.

   Every project uses one asymmetric two-column grid — narrative on the left,
   evidence (screenshot/gallery, then diagram) on the right — with no
   alternation, so a project renders identically in the homepage list and in
   the admin Draft Preview, which shows a single project outside that list.
   ========================================================================== */

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

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: var(--sans);
  font-size: 18px;
  line-height: 1.7;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

a {
  color: inherit;
}

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

figure {
  margin: 0;
}

.page {
  max-width: var(--page);
  margin: 0 auto;
  padding: 0 var(--gutter);
}

/* ================= skip link ================= */
.skip-link {
  position: absolute;
  left: 12px;
  top: -60px;
  z-index: 100;
  background: var(--ink);
  color: var(--paper);
  padding: 10px 16px;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-decoration: none;
  border-radius: 2px;
  transition: top 0.15s;
}
.skip-link:focus {
  top: 12px;
}

/* ================= focus states ================= */
a:focus-visible,
button:focus-visible,
[tabindex]:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 3px;
  border-radius: 2px;
}

/* ================= header ================= */
.masthead {
  position: sticky;
  top: 0;
  z-index: 40;
  background: rgba(251, 250, 247, 0.94);
  -webkit-backdrop-filter: saturate(180%) blur(8px);
  backdrop-filter: saturate(180%) blur(8px);
  border-bottom: 1px solid var(--rule);
}
.masthead .page {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 32px;
  height: 74px;
}
.wordmark {
  font-family: var(--serif);
  font-size: 15px;
  text-transform: uppercase;
  letter-spacing: 0.235em;
  text-decoration: none;
  white-space: nowrap;
  padding-right: 0.235em;
}
.masthead nav {
  display: flex;
  gap: 30px;
}
.masthead nav a {
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--muted);
  text-decoration: none;
  padding: 4px 0 3px;
  border-bottom: 1px solid transparent;
  transition:
    color 0.15s,
    border-color 0.15s;
}
.masthead nav a:hover {
  color: var(--ink);
  border-bottom-color: var(--accent);
}

/* ================= hero ================= */
.hero {
  padding: 122px 0 108px;
}
.hero h1 {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(37px, 5.1vw, 58px);
  line-height: 1.1;
  letter-spacing: -0.021em;
  margin: 0;
  max-width: 15ch;
}
.hero .keyline {
  width: 54px;
  height: 2px;
  background: var(--accent);
  margin: 38px 0 34px;
}
.hero p {
  margin: 0;
  font-size: 20px;
  line-height: 1.62;
  color: var(--ink-70);
  max-width: 56ch;
}
.hero p + p {
  margin-top: 17px;
}

/* ================= projects ================= */
.projects {
  padding-bottom: 36px;
}
/* One layout for every project, never alternating: narrative left, evidence
   right. The media column is the wider of the two because screenshots and
   diagrams gain more from horizontal space than a 46ch measure does. The
   ratio is expressed in fr so the two columns keep their proportions while
   the gap stays a fixed optical distance. */
.project {
  display: grid;
  grid-template-columns: minmax(0, 41fr) minmax(0, 59fr);
  column-gap: 64px;
  row-gap: 34px;
  align-items: start;
  padding-bottom: 72px;
}
/* `.text` is a container so the stats block can respond to the width of the
   narrative column itself rather than the viewport (see .stats below). */
.project .text {
  grid-column: 1;
  grid-row: 1;
  container-type: inline-size;
}
/* screenshot and diagram share ONE grid cell, so they stay tight to each
   other however far the text column runs */
.project .visuals {
  grid-column: 2;
  grid-row: 1;
}
/* graceful: a project with no screenshot and no diagram collapses to one column */
.project.no-media {
  grid-template-columns: minmax(0, 1fr);
  row-gap: 38px;
}
.project.no-media .text {
  grid-column: 1;
}

/* A hairline between projects — the faintest rule in the system, with the
   72px above it (the previous project's padding) matched below, so the line
   sits centred in generous space rather than attached to either project. The
   adjacent-sibling selector is what keeps it off the first project, and off a
   lone project in the admin Draft Preview. */
.project + .project {
  border-top: 1px solid var(--rule-soft);
  padding-top: 72px;
}
/* The last project keeps the larger gap into The Index: that section opens
   with its own heading and the `ol.entries` top rule, a stronger boundary
   than these separators, and a second hairline immediately above it would
   only compete with it. */
.project:last-child {
  padding-bottom: 110px;
}

.eyebrow {
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--muted);
  display: flex;
  gap: 11px;
  align-items: center;
  flex-wrap: wrap;
  margin: 0 0 18px;
}
.eyebrow .nm {
  color: var(--ink);
}
.eyebrow .sep {
  width: 3px;
  height: 3px;
  border-radius: 50%;
  background: var(--faint);
  flex: none;
}
.eyebrow .live {
  color: var(--accent);
}
.project h2 {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(24px, 2.5vw, 30px);
  line-height: 1.28;
  letter-spacing: -0.012em;
  margin: 0 0 22px;
  max-width: 20ch;
}
.project .text p {
  margin: 0 0 17px;
  color: var(--ink-70);
  font-size: 17.5px;
  line-height: 1.68;
  max-width: 46ch;
}
.project .text p:last-child {
  margin-bottom: 0;
}
/* the eyebrow meta line is also a <p> — it must not inherit body-copy sizing */
.project .text p.eyebrow {
  font-size: 10.5px;
  line-height: 1.6;
  letter-spacing: 0.15em;
  color: var(--muted);
  max-width: none;
  margin: 0 0 18px;
}
.project.no-media .text h2 {
  max-width: 24ch;
}
.project.no-media .text p {
  max-width: 62ch;
}

/* stats strip — up to 3 value/label pairs (MAX_STATS_PER_PROJECT), sitting in
   the narrative column beneath the description and above the project link. A
   hairline rule separates it from the description above; empty is simply
   absent, no placeholder. `dl > div` groups each dt/dd pair for layout, which
   HTML permits inside a definition list.

   Two aligned columns by default, so three stats become a deliberate 2 + 1
   rather than a ragged wrap; three columns only once the narrative column is
   wide enough for a full-length label to sit on one line at its own size.
   Type never shrinks to force three across. The query measures `.text`, not
   the viewport, so a wide single-column project (.no-media) gets three across
   while the standard two-column project gets 2 + 1. */
.project .stats {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 26px 32px;
  margin: 30px 0 0;
  padding-top: 24px;
  border-top: 1px solid var(--rule-soft);
}
@container (min-width: 540px) {
  .project .stats {
    grid-template-columns: repeat(3, minmax(0, 1fr));
  }
}
/* a full-width narrative column would otherwise stretch three stats across the
   whole page — same measure limit the prose in that case already takes */
.project.no-media .stats {
  max-width: 62ch;
}
.project .stat {
  display: flex;
  flex-direction: column-reverse;
  gap: 8px;
}
.project .stat dt {
  font-family: var(--mono);
  font-size: 9.5px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: var(--faint);
}
.project .stat dd {
  font-family: var(--serif);
  font-size: 30px;
  line-height: 0.95;
  letter-spacing: -0.02em;
  color: var(--ink);
  margin: 0;
}

/* project link — a citation, not a call to action: mono/uppercase like the
   eyebrow, but noticeably larger and heavier so it reads as a destination
   worth following rather than a footnote. Selected as `p.url`, not `.url`,
   for the same reason as the eyebrow above: `.project .text p` is the more
   specific selector, so a bare `.url` never wins its own margin back. */
.project .text p.url {
  margin: 34px 0 0;
  max-width: none;
}
.project .url a {
  font-family: var(--mono);
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--accent);
  padding-bottom: 5px;
  border-bottom: 1.5px solid var(--accent-soft);
  transition:
    border-color 0.15s,
    color 0.15s;
}
.project .url a:hover {
  color: var(--ink);
  border-bottom-color: var(--ink);
}
.project .url a:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 5px;
  border-bottom-color: var(--accent);
}

/* visuals: screenshot figure (if any) then diagram figure (if any), tight to
   each other — spacing applies only between stacked items, never before the
   first one, regardless of which media types are present */
.visuals > * + * {
  margin-top: 30px;
}

/* frames — the contained media treatment shared by screenshots */
.frame {
  border: 1px solid var(--rule);
  background: var(--panel);
  overflow: hidden;
}
.frame img {
  display: block;
  width: 100%;
  height: auto;
}
.cap {
  font-family: var(--mono);
  font-size: 10px;
  line-height: 1.7;
  letter-spacing: 0.045em;
  color: var(--faint);
  margin: 12px 0 0;
  max-width: 64ch;
}

/* ================= carousel ================= */
/* Multiple screenshots only. A single screenshot is a plain .frame figure
   above, with no controls. The track is a native horizontally-scrolling,
   scroll-snapping list — it stays fully usable with JS disabled (touch,
   trackpad, or the track's own keyboard scrolling). carousel.js only adds the
   buttons' behaviour, arrow-key stepping and the live counter/caption sync. */
.carousel-frame {
  position: relative;
  aspect-ratio: 16 / 10;
  background: var(--paper-2);
}
.carousel-track {
  display: flex;
  height: 100%;
  margin: 0;
  padding: 0;
  list-style: none;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}
.carousel-track::-webkit-scrollbar {
  display: none;
}
.carousel-track:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: -2px;
}
.carousel-slide {
  flex: 0 0 100%;
  min-width: 0;
  height: 100%;
  scroll-snap-align: start;
  display: flex;
  align-items: center;
  justify-content: center;
}
.carousel-slide img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}
.carousel-controls {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 10px;
  padding: 10px;
  pointer-events: none;
}
.carousel-btn {
  pointer-events: auto;
  width: 30px;
  height: 30px;
  flex: none;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  border: 1px solid var(--rule);
  background: rgba(251, 250, 247, 0.92);
  color: var(--ink-70);
  cursor: pointer;
  padding: 0;
  transition:
    background 0.15s,
    color 0.15s,
    border-color 0.15s;
}
.carousel-btn svg {
  width: 14px;
  height: 14px;
}
.carousel-btn:hover {
  background: var(--panel);
  color: var(--ink);
  border-color: var(--ink-70);
}
.carousel-counter {
  pointer-events: auto;
  margin: 0;
  font-family: var(--mono);
  font-size: 9.5px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--ink-70);
  background: rgba(251, 250, 247, 0.92);
  border: 1px solid var(--rule);
  border-radius: 20px;
  padding: 4px 11px;
  white-space: nowrap;
}

/* ================= gallery ================= */
/* GALLERY-mode projects only (Project.mediaMode); a single screenshot uses
   the plain .frame figure above, and CAROUSEL-mode projects use the track
   above instead. One large image, one row of same-width thumbnail buttons
   beneath it. Thumbnails are real <button>s (see gallery.tsx); gallery.js
   only wires the click-to-swap behaviour. */
.gallery-frame {
  aspect-ratio: 16 / 10;
  background: var(--paper-2);
}
.gallery-image {
  transition: opacity 0.15s ease;
}
.gallery-image.is-swapping {
  opacity: 0;
}
.gallery-thumbs {
  display: grid;
  gap: 10px;
  margin-top: 10px;
}
/* Equal-width columns for whatever count the project actually has (2-4,
   MAX_MEDIA_PER_PROJECT) — never inferred, always exactly `media.length`. */
.gallery-thumbs.count-2 {
  grid-template-columns: repeat(2, 1fr);
}
.gallery-thumbs.count-3 {
  grid-template-columns: repeat(3, 1fr);
}
.gallery-thumbs.count-4 {
  grid-template-columns: repeat(4, 1fr);
}
.gallery-thumb {
  padding: 0;
  border: 1px solid var(--rule);
  background: var(--panel);
  cursor: pointer;
  aspect-ratio: 16 / 10;
  overflow: hidden;
  opacity: 0.6;
  transition:
    opacity 0.15s,
    border-color 0.15s;
}
.gallery-thumb img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.gallery-thumb:hover {
  opacity: 0.85;
  border-color: var(--ink-70);
}
.gallery-thumb:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  opacity: 1;
}
/* the active thumbnail — a subtle but unambiguous state, not colour-only:
   full opacity plus a darker border */
.gallery-thumb[aria-current='true'] {
  opacity: 1;
  border-color: var(--ink);
}

/* ================= index ================= */
.index {
  padding-bottom: 20px;
}
.index-head {
  max-width: 640px;
  margin-bottom: 52px;
}
.index-head h2 {
  font-family: var(--serif);
  font-weight: 400;
  font-size: clamp(27px, 3.1vw, 35px);
  line-height: 1.2;
  letter-spacing: -0.016em;
  margin: 0 0 15px;
}
.index-head p {
  margin: 0;
  color: var(--muted);
  font-size: 17.5px;
  /* No measure limit of its own: this is a single-sentence subtitle, not body
     prose, and it should sit on one line at desktop widths. The 46ch it used to
     carry left only ~34px of slack, which the wider fallback faces in --sans
     (Segoe UI, Arial) would have overflowed into a second line. The container's
     640px still bounds it, and on narrow screens that container shrinks, so the
     sentence wraps naturally rather than overflowing. */
}
ol.entries {
  list-style: none;
  margin: 0;
  padding: 0;
  border-top: 1px solid var(--rule);
}
ol.entries li {
  border-bottom: 1px solid var(--rule-soft);
}
.entry {
  display: grid;
  grid-template-columns: 96px minmax(0, 1fr) 104px;
  gap: 28px;
  align-items: baseline;
  padding: 17px 6px 18px 2px;
  text-decoration: none;
  color: inherit;
}
a.entry {
  transition: background 0.15s;
}
a.entry:hover {
  background: var(--accent-wash);
}
a.entry:hover .h {
  border-bottom-color: var(--ink);
}
.entry .y {
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.07em;
  color: var(--faint);
  white-space: nowrap;
}
.entry .h {
  font-family: var(--serif);
  font-size: 19px;
  line-height: 1.35;
  border-bottom: 1px solid transparent;
  padding-bottom: 1px;
}
.entry .d {
  display: block;
  font-size: 16px;
  line-height: 1.55;
  color: var(--muted);
  margin-top: 5px;
  max-width: 58ch;
}
.entry .s {
  font-family: var(--mono);
  font-size: 9.5px;
  letter-spacing: 0.15em;
  text-transform: uppercase;
  color: var(--faint);
  text-align: right;
  white-space: nowrap;
}
.entry .s.live {
  color: var(--accent);
}
.entry-top {
  display: none;
}

/* ================= footer ================= */
footer {
  margin-top: 80px;
  border-top: 1px solid var(--rule);
}
footer .page {
  display: flex;
  justify-content: space-between;
  gap: 24px;
  flex-wrap: wrap;
  padding-top: 30px;
  padding-bottom: 60px;
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--faint);
}
footer .set {
  display: flex;
  gap: 24px;
  flex-wrap: wrap;
}
footer a {
  color: var(--muted);
  text-decoration: none;
  border-bottom: 1px solid var(--rule-soft);
  padding-bottom: 2px;
  transition:
    color 0.15s,
    border-color 0.15s;
}
footer a:hover {
  color: var(--ink);
  border-bottom-color: var(--rule);
}

/* ================= responsive ================= */

/* Tablet: both columns stay, rebalanced. The gap tightens and the media column
   takes a little more, so neither the measure nor the screenshot ends up
   cramped at the width where the two-column layout is at its tightest. */
@media (max-width: 1100px) and (min-width: 1001px) {
  .project {
    grid-template-columns: minmax(0, 40fr) minmax(0, 60fr);
    column-gap: 44px;
  }
}

/* Below this the two columns stop being comfortable, so the project stacks —
   always narrative first (text, stats, link), then evidence (image/gallery,
   diagram, caption), which is already the DOM order. */
@media (max-width: 1000px) {
  .project {
    grid-template-columns: minmax(0, 1fr);
    row-gap: 34px;
  }
  .project .text,
  .project .visuals {
    grid-column: 1;
  }
  .project .text {
    grid-row: 1;
  }
  .project .visuals {
    grid-row: 2;
    max-width: 620px;
  }
  .project h2,
  .project .text p {
    max-width: 58ch;
  }
  .hero {
    padding: 92px 0 84px;
  }
}

@media (max-width: 680px) {
  body {
    font-size: 17px;
  }
  .masthead .page {
    height: 62px;
  }
  .wordmark {
    font-size: 13px;
    letter-spacing: 0.19em;
  }
  .masthead nav {
    gap: 20px;
  }
  .masthead nav a {
    font-size: 9.5px;
    letter-spacing: 0.13em;
  }
  .hero {
    padding: 62px 0 60px;
  }
  .hero p {
    font-size: 18.5px;
  }
  .hero .keyline {
    margin: 30px 0 28px;
  }
  .project {
    padding-bottom: 52px;
    row-gap: 28px;
  }
  .project + .project {
    padding-top: 52px;
  }
  .project:last-child {
    padding-bottom: 76px;
  }
  .project .text p {
    font-size: 17px;
  }
  .project .stats {
    gap: 20px 28px;
    margin-top: 26px;
    padding-top: 20px;
  }
  .project .stat dd {
    font-size: 26px;
  }
  .gallery-thumbs {
    gap: 7px;
  }

  .index-head {
    margin-bottom: 38px;
  }
  .entry {
    grid-template-columns: 1fr;
    gap: 0;
    padding: 16px 2px 17px;
  }
  .entry-top {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    gap: 16px;
    margin-bottom: 7px;
  }
  .entry > .y,
  .entry > .s {
    display: none;
  }
  .entry .d {
    max-width: none;
  }

  footer {
    margin-top: 56px;
  }
  footer .page {
    padding-bottom: 48px;
  }
}
