/*
 * SK01 — public site.
 *
 * A direct translation of the inline styles in the design handoff prototype
 * (design_handoff_sk01_site/SK01.dc.html). The prototype is inline-styled only
 * because of its authoring runtime; every value here is taken from it verbatim.
 * Theme colours arrive as custom properties written into <head> from D1.
 *
 * All spacing is in `em` off the root clamp(), so the whole layout scales.
 */

:root {
  /* Overwritten per-request from the `theme` settings row. These are the
     documented defaults from README.md § Design Tokens. */
  --bg: #546164;
  --text: #ffffff;
  --muted: #8f8f8f;
  --panel: #1b1b1b;
  --open-width: 50em;

  --radius: 0.2em;
  --col: 16.4em;
  --gap: 0.4em;
  /*
   * The break between two projects in the feed. Every entry is a single --bg
   * cut-out, so the gaps inside one and the gap between two are the same
   * colour; only the band of contours showing through tells them apart. At the
   * prototype's 3em that band is too thin to read as a boundary, so one project
   * runs into the next. This is the same gap, widened until the contours
   * between entries are unmistakably a break. Capped in em so it does not run
   * away on a tall display.
   */
  --entry-break: min(28vh, 12em);
  --measure: 32em;

  --font: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, sans-serif;
  --mono: ui-monospace, monospace;

  /* Page title. Overwritten per-request from the `typography` settings row. */
  --title-font: 'Suisse Intl', sans-serif;
  --title-weight: 300;
  --title-style: normal;
  --title-size: 4.4em;
  --title-leading: 1.02;
  --title-tracking: -0.02em;
  --title-color: var(--text);
}

html,
body {
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--font);
  -webkit-font-smoothing: antialiased;
  overflow: hidden;
}

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

/* Scrollbars are hidden by design; keyboard and trackpad scrolling still work
   because the scroller keeps `overflow-y: auto`. */
::-webkit-scrollbar {
  display: none;
}

.scroller {
  scrollbar-width: none;
  -ms-overflow-style: none;
}

/* ------------------------------------------------------------- background */

#bg {
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  width: 100%;
  transition: width 0.6s ease;
  font-size: clamp(13px, 1vw, 20px);
  z-index: 0;
  background: var(--bg);
}

/* The layer narrows to the configured open width once a project is selected. */
body[data-selected]:not([data-selected='']) #bg {
  width: var(--open-width);
}

#bg canvas {
  display: block;
  width: 100%;
  height: 100%;
  touch-action: none;
}

/* ----------------------------------------------------------------- layout */

#layout {
  position: relative;
  z-index: 1;
  height: 100vh;
  display: grid;
  grid-template-columns: var(--col) var(--col) var(--col) minmax(0, 1fr);
  gap: var(--gap);
  padding: 0.25em 0 0 0;
  box-sizing: border-box;
  font-size: clamp(13px, 1vw, 20px);
  line-height: 1.28;
  color: var(--text);
}

.col {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  min-width: 0;
}

/*
 * Panels have no fill and no border. They read as panels because the canvas
 * behind them is filled with --bg at each element's exact rect; see
 * data-mask in src/client/site.ts.
 */
.panel {
  border-radius: var(--radius);
  padding: 0.35em 0.55em 0.4em;
  display: flex;
  flex-direction: column;
}

/* Panel and block titles: 0.9em is the prototype's 0.75em plus 20%. */
.eyebrow {
  font-size: 0.9em;
  line-height: 1.3;
  font-weight: 700;
}

/* ---------------------------------------------------------- column 1: nav */

.wordmark {
  padding: 0.4em 0.55em 0.9em;
}

/* The wordmark links home. It replaces a <div>, so it keeps its own line, and
   it dims on hover as the only affordance the prototype's palette allows. */
.wordmark a {
  display: block;
}

.wordmark a:hover {
  color: var(--muted);
}

.navlink {
  cursor: pointer;
  display: flex;
  gap: 0.35em;
  color: var(--muted);
}

.navlink:hover,
.navlink[aria-current],
.navlink.is-active {
  color: var(--text);
}

/* The bullet occupies its slot only when the item is active, so labels do not
   shift horizontally between states. */
.bullet {
  width: 0.5em;
  display: none;
}

.navlink[aria-current] .bullet,
.navlink.is-active .bullet {
  display: inline;
}

.footer-link {
  padding: 0.6em 0.55em;
}

/* -------------------------------------------------------- column 2: index */

.contact-row {
  padding: 0.6em 0.55em;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4em 1.1em;
}

.prose {
  max-width: var(--measure);
  display: flex;
  flex-direction: column;
  gap: 0.9em;
  text-wrap: pretty;
}

.prose p {
  margin: 0;
}

/* The global reset strips link styling; rich-text links must still read as links. */
.prose a {
  text-decoration: underline;
  text-underline-offset: 0.15em;
  text-decoration-thickness: 1px;
}

.prose a:hover {
  text-decoration: none;
}

.info-panel {
  padding: 0.35em 0.55em 0.5em;
  gap: 0.9em;
}

/* ------------------------------------------ columns 2-3: the page title */

/*
 * The title spans both middle columns and starts below their panels, so it
 * cannot be placed in either column's flow. Instead the two columns dissolve
 * into a grid of their own: `display: contents` hands their panels to this
 * grid, which lays them out in row 1, the title in row 2 and the two footers
 * in row 3. Row 2 takes the leftover height, so the footers stay on the
 * baseline of the page and the title clears whichever panel stack is taller —
 * a long project index, or the Services list on About.
 */
#col-pair {
  grid-column: 2 / 4;
  display: grid;
  grid-template-columns: var(--col) var(--col);
  grid-template-rows: auto minmax(0, 1fr) auto;
  gap: 0 var(--gap);
  min-width: 0;
}

#col-pair > .col {
  display: contents;
}

#col-index > .col-top {
  grid-column: 1;
  grid-row: 1;
  min-width: 0;
}

#col-meta > .col-top {
  grid-column: 2;
  grid-row: 1;
  min-width: 0;
}

#col-index > .contact-row {
  grid-column: 1;
  grid-row: 3;
  align-self: end;
}

#col-meta > #esc {
  grid-column: 2;
  grid-row: 3;
  align-self: end;
}

/*
 * The heading box keeps the layout's own font size, so its padding lines up
 * with the panels above it; the line inside carries the display size.
 */
.page-title {
  grid-column: 1 / 3;
  grid-row: 2;
  align-self: start;
  margin: 0;
  padding: 1.1em 0.55em 0;
  font-size: inherit;
  font-weight: inherit;
  min-width: 0;
}

.title-line {
  display: block;
  font-family: var(--title-font);
  font-weight: var(--title-weight);
  font-style: var(--title-style);
  font-size: var(--title-size);
  line-height: var(--title-leading);
  letter-spacing: var(--title-tracking);
  color: var(--title-color);
  /* A display size needs to be allowed to break inside a long project name. */
  overflow-wrap: break-word;
  hyphens: auto;
}

/* One line shows at a time: the landing line, or the selected project's. */
.title-line[data-slug] {
  display: none;
}

body[data-view='work'] .title-line[data-slug].is-current {
  display: block;
}

body[data-view='work']:not([data-selected='']) .title-line.is-landing {
  display: none;
}

/* ----------------------------------------------------- column 3: metadata */

.meta-panel {
  display: grid;
  grid-template-columns: 3.4em 1fr;
  column-gap: 0.5em;
  row-gap: 0.1em;
  align-items: baseline;
  padding: 0.4em 0.55em 0.45em;
  border-radius: var(--radius);
}

.meta-label {
  font-size: 0.75em;
}

.meta-link {
  color: var(--muted);
}

.meta-link:hover {
  color: var(--text);
}

.services {
  display: flex;
  flex-direction: column;
}

#esc {
  padding: 0.6em 0.55em;
  display: flex;
  align-items: center;
  gap: 0.3em;
}

.esc-badge {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 0.95em;
  height: 0.95em;
  border: 0.06em solid var(--text);
  border-radius: 50%;
  font-size: 0.8em;
  line-height: 1;
}

/* ------------------------------------------------------------ column 4: feed */

#feed {
  min-width: 0;
  height: 100vh;
  overflow-y: auto;
  /* Cancels the grid's top padding so the first entry aligns with columns 1-3. */
  margin-top: -0.25em;
}

.feed-inner {
  display: flex;
  flex-direction: column;
  gap: var(--entry-break);
  /* The 30vh bottom pad lets the last entry reach the scroll-sync line. */
  padding: 0.25em 0.4em 30vh 0;
}

.entry {
  display: flex;
  flex-direction: column;
  gap: var(--gap);
}

.text-block {
  padding: 0.4em 0.55em 0.6em;
  gap: 0.9em;
}

/*
 * Images fill the column width and keep their own aspect ratio (never cropped).
 * The width/height attributes reserve space before load. Only the empty
 * placeholder <div> (no image attached yet) uses the fixed prototype ratio.
 */
.media {
  display: block;
  width: 100%;
  border-radius: var(--radius);
  background-color: var(--panel);
}

img.media {
  height: auto;
}

div.media {
  aspect-ratio: 1594 / 2480;
}

.media-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: var(--gap);
  /* Without this, grid stretch would force both images to the taller height. */
  align-items: start;
}

.embed-panel {
  border-radius: var(--radius);
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

.embed-panel .eyebrow {
  padding: 0.4em 0.55em 0;
}

.embed-panel iframe {
  display: block;
  width: 100%;
  height: 9.5em;
  border: 0;
  margin-top: 0.4em;
}

.placeholder {
  aspect-ratio: 16 / 9;
  border-radius: var(--radius);
  background: var(--panel);
  display: flex;
  align-items: center;
  justify-content: center;
  font: 0.7em var(--mono);
  color: var(--muted);
}

.contact-panel {
  padding: 0.35em 0.55em 0.5em;
  gap: 0.9em;
  max-width: var(--col);
  margin-top: 0.25em;
}

.contact-links {
  display: flex;
  flex-direction: column;
  color: var(--muted);
}

.contact-links a:hover {
  color: var(--text);
}

/* ---------------------------------------------------------- view switching */

/*
 * Server-rendered state lives in body[data-view] and body[data-selected]; the
 * client only flips those attributes. Everything below is the prototype's
 * `sc-if` conditions expressed in CSS.
 *
 * The `html.js` guard matters: with JavaScript disabled the feed and metadata
 * stay visible, so every project remains readable and crawlable on `/`. With JS
 * enabled the landing state matches the prototype exactly — columns 3 and 4
 * empty until a project is chosen.
 */
body[data-view='work'] .about-only {
  display: none;
}

body[data-view='about'] .work-only {
  display: none;
}

html.js body[data-view='work'][data-selected=''] .needs-selection {
  display: none;
}

/* A project is selected: show only that entry's metadata. */
.meta-panel[data-slug] {
  display: none;
}

body[data-view='work'] .meta-panel[data-slug].is-current {
  display: grid;
}

/* -------------------------------------------------------- mobile menu bits */

/*
 * The burger header and its sheet are the only elements this design adds for
 * small screens, and they hold no content of their own: site.js moves the
 * existing wordmark, nav, project index, contact row and footer link into them
 * below 1024px, and moves them back above it. Everything therefore keeps the
 * classes — and so the type, colour and spacing — it already has.
 *
 * They stay out of the document until that relocation has happened, so a page
 * without JavaScript is exactly the page it was before.
 */
#mnav {
  display: none;
}

#burger {
  appearance: none;
  -webkit-appearance: none;
  background: transparent;
  border: 0;
  color: inherit;
  font: inherit;
  cursor: pointer;
  flex: none;
  /* A small glyph with a large touch target around it. */
  padding: 0.7em 0.2em 0.7em 0.8em;
  margin: -0.3em -0.2em -0.3em 0;
  display: flex;
  align-items: center;
}

.burger-bars {
  display: block;
  position: relative;
  width: 1.35em;
  height: 0.9em;
}

.burger-bars span {
  position: absolute;
  left: 0;
  right: 0;
  height: 0.09em;
  background: currentColor;
  transition:
    transform 0.34s cubic-bezier(0.22, 0.61, 0.36, 1),
    opacity 0.18s ease;
}

.burger-bars span:nth-child(1) {
  top: 0;
}

.burger-bars span:nth-child(2) {
  top: 50%;
  margin-top: -0.045em;
}

.burger-bars span:nth-child(3) {
  bottom: 0;
}

/* Closed → open: the outer bars meet in the middle, the centre one leaves. */
body[data-menu='open'] .burger-bars span:nth-child(1) {
  transform: translateY(0.405em) rotate(45deg);
}

body[data-menu='open'] .burger-bars span:nth-child(2) {
  opacity: 0;
  transform: scaleX(0.3);
}

body[data-menu='open'] .burger-bars span:nth-child(3) {
  transform: translateY(-0.405em) rotate(-45deg);
}

/* ------------------------------------------------------------- responsive */

@media (max-width: 1024px) {
  /*
   * The height must be released here, or the body becomes an internal scroller
   * and window.scrollY stays 0 — which breaks scroll sync on mobile.
   */
  html,
  body {
    overflow: auto;
    height: auto;
    min-height: 100%;
  }

  #layout {
    grid-template-columns: minmax(0, 1fr);
    height: auto;
    min-height: 100vh;
    gap: 0.9em;
    padding: 0.5em 0.6em 2em;
    font-size: clamp(14px, 3.6vw, 16px);
  }

  #feed {
    height: auto;
    overflow: visible;
    margin-top: 0;
  }

  #esc {
    display: none;
  }

  /*
   * Stacked there is only one column, so the pair grid dissolves and its two
   * columns become ordinary blocks again. The title leads the page, ahead of
   * the metadata card, and is capped: 4.4em of a phone's root size is a word
   * per line.
   */
  #col-pair {
    display: contents;
  }

  #col-pair > .col {
    display: flex;
  }

  #col-index > .col-top,
  #col-meta > .col-top,
  #col-index > .contact-row,
  #col-meta > #esc,
  .page-title {
    grid-column: auto;
    grid-row: auto;
  }

  .page-title {
    order: -1;
    padding: 0 0 0.1em;
  }

  .title-line {
    font-size: min(var(--title-size), 2.7em);
  }

  #bg {
    width: 100%;
  }

  body[data-selected]:not([data-selected='']) #bg {
    width: 100%;
  }

  /* The feed column is the whole page here, so the contact panel is too. */
  .contact-panel {
    max-width: none;
  }

  /*
   * That 0.4em is the gutter between the feed column and the metadata column,
   * and there is no metadata column here. Left in, it holds every entry short
   * of the right edge, and the cut-out behind it leaves a strip of contours
   * standing beside the text.
   */
  .feed-inner {
    padding-right: 0;
    /*
     * Stacked, the entry reveal below already marks each boundary as it slides
     * in, and the column is the page: the prototype's break is enough here.
     */
    --entry-break: 3em;
  }

  /*
   * Landing state: with the index now behind the burger, an empty feed would
   * leave nothing to read. Stacked, the work feed is the page — the same thing
   * the no-JS document already does.
   */
  html.js body[data-view='work'][data-selected=''] .needs-selection {
    display: flex;
  }

  /* --------------------------------------------------- relocated navigation */

  body[data-mobile-nav] #mnav {
    display: block;
  }

  /* Emptied by the relocation; the grid gap would otherwise keep their slots. */
  body[data-mobile-nav] #col-identity,
  body[data-mobile-nav][data-view='work'] #col-index {
    display: none;
  }

  body[data-mobile-nav] #layout {
    padding-top: calc(var(--mnav-top, 3em) + 0.5em);
  }

  /*
   * Unlike every other panel, the bar is painted rather than cut out of the
   * canvas: it is fixed over the feed, and a cut-out would only remove the
   * contours behind it — the article would still scroll through it. The fill
   * is --bg, so it reads as the same flat rectangle as the cut-outs do.
   */
  body[data-mobile-nav] #mnav-bar {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 6;
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 0.6em;
    padding: 0.5em 0.6em 0.45em;
    box-sizing: border-box;
    background: var(--bg);
    transition: transform 0.35s cubic-bezier(0.22, 0.61, 0.36, 1);
  }

  /* Retreats while reading down, returns on the first upward flick. */
  body[data-bar='hidden'] #mnav-bar {
    transform: translateY(-100%);
  }

  body[data-mobile-nav] #mnav-bar .wordmark {
    padding: 0;
    min-width: 0;
  }

  /*
   * The sheet is a panel like any other: no fill, legible because the canvas
   * behind it is punched out at its exact rect (site.js masks it). The page
   * fades out underneath, so the contours it sits on stay live, framed by the
   * gutter around it.
   */
  body[data-mobile-nav] #mnav-sheet {
    position: fixed;
    top: var(--mnav-top, 3em);
    left: 0.6em;
    right: 0.6em;
    z-index: 5;
    height: 0;
    overflow: hidden;
    box-sizing: border-box;
    border-radius: var(--radius);
    padding: 0.35em 0.55em 0.5em;
    display: flex;
    flex-direction: column;
    gap: 0.9em;
    font-size: clamp(14px, 3.6vw, 16px);
    line-height: 1.28;
    overscroll-behavior: contain;
    transition: height 0.42s cubic-bezier(0.22, 0.61, 0.36, 1);
  }

  body[data-menu='open'] #mnav-sheet {
    overflow-y: auto;
  }

  /* The relocated panels supply their own rhythm; the sheet supplies the box. */
  body[data-mobile-nav] #mnav-sheet > .panel,
  body[data-mobile-nav] #mnav-sheet > .contact-row,
  body[data-mobile-nav] #mnav-sheet > .footer-link {
    padding: 0;
  }

  body[data-mobile-nav] #mnav-sheet > .footer-link {
    margin-top: auto;
    padding-top: 0.9em;
  }

  /* Touch targets, and the two top-level destinations set above the index. */
  body[data-mobile-nav] #mnav-sheet .navlink {
    padding: 0.34em 0;
  }

  body[data-mobile-nav] #mnav-sheet #site-nav .navlink {
    font-size: 1.3em;
    padding: 0.22em 0;
  }

  body[data-mobile-nav] #mnav-sheet .contact-row {
    gap: 0.4em 1.1em;
  }

  /* Each group arrives just behind the panel edge. */
  body[data-mobile-nav] #mnav-sheet > * {
    opacity: 0;
    transform: translateY(-0.5em);
    transition:
      opacity 0.26s ease,
      transform 0.34s cubic-bezier(0.22, 0.61, 0.36, 1);
  }

  body[data-menu='open'] #mnav-sheet > * {
    opacity: 1;
    transform: none;
  }

  body[data-menu='open'] #mnav-sheet > :nth-child(1) {
    transition-delay: 0.08s;
  }

  body[data-menu='open'] #mnav-sheet > :nth-child(2) {
    transition-delay: 0.13s;
  }

  body[data-menu='open'] #mnav-sheet > :nth-child(3) {
    transition-delay: 0.18s;
  }

  body[data-menu='open'] #mnav-sheet > :nth-child(4) {
    transition-delay: 0.23s;
  }

  /* The page gives way to the menu rather than sitting behind it. */
  body[data-mobile-nav] #layout > .col,
  body[data-mobile-nav] #feed {
    transition: opacity 0.32s ease;
  }

  body[data-menu='open'] #layout > .col,
  body[data-menu='open'] #feed {
    opacity: 0;
    pointer-events: none;
  }

  /*
   * Stacked, the metadata is a single card that cross-fades as the selection
   * follows the scroll. Laying every panel in one grid cell keeps the card a
   * fixed height, so a project changing under the reader never shifts the
   * article they are in the middle of.
   */
  body[data-mobile-nav][data-view='work'][data-selected=''] #col-meta {
    display: none;
  }

  body[data-mobile-nav][data-view='work'] #col-meta .col-top {
    display: grid;
  }

  body[data-mobile-nav][data-view='work'] .meta-panel[data-slug] {
    display: grid;
    grid-area: 1 / 1;
    opacity: 0;
    pointer-events: none;
    transition: opacity 0.3s ease;
  }

  body[data-mobile-nav][data-view='work'] .meta-panel[data-slug].is-current {
    opacity: 1;
    pointer-events: auto;
  }

  /* ------------------------------------------------------- entry reveals */

  /*
   * Only once the script is driving the page: the class is added by an
   * IntersectionObserver, and a document that never gets one must not be left
   * holding invisible articles.
   */
  body[data-mobile-nav] .entry {
    opacity: 0;
    transform: translateY(0.9em);
    transition:
      opacity 0.65s ease,
      transform 0.65s cubic-bezier(0.22, 0.61, 0.36, 1);
  }

  body[data-mobile-nav] .entry.is-visible {
    opacity: 1;
    transform: none;
  }
}

/* Honour a reduced-motion preference: the canvas engine stops animating and the
   width transition is removed, but the layout is unchanged. */
@media (prefers-reduced-motion: reduce) {
  #bg {
    transition: none;
  }

  * {
    scroll-behavior: auto !important;
  }

  /*
   * The mobile menu still changes state, it just stops travelling: the sheet
   * and the entries cut straight to their finished position.
   */
  #mnav-bar,
  #mnav-sheet,
  .meta-panel[data-slug],
  #mnav-sheet > *,
  .burger-bars span,
  .entry,
  #layout > .col,
  #feed {
    transition-duration: 0.01ms !important;
    transition-delay: 0s !important;
  }
}

/* ------------------------------------------------------------ contact form */

/*
 * Plain, unstyled-feeling inputs that borrow the page's own type and colour.
 * The form is inside the masked Contact panel, so it reads on the cut-out.
 */
.contact-form {
  display: flex;
  flex-direction: column;
  gap: 0.4em;
  margin-top: 0.3em;
}

.contact-form label {
  display: flex;
  flex-direction: column;
  gap: 0.15em;
}

.contact-form label > span {
  font-size: 0.75em;
  line-height: 1.3;
  color: var(--muted);
}

.contact-form input,
.contact-form textarea {
  font: inherit;
  color: var(--text);
  background: transparent;
  border: 0.06em solid var(--muted);
  border-radius: var(--radius);
  padding: 0.2em 0.35em;
  width: 100%;
  box-sizing: border-box;
}

.contact-form textarea {
  resize: vertical;
}

.contact-form input:focus,
.contact-form textarea:focus {
  outline: none;
  border-color: var(--text);
}

.contact-form button {
  font: inherit;
  color: var(--text);
  background: transparent;
  border: 0.06em solid var(--text);
  border-radius: var(--radius);
  padding: 0.2em 0.6em;
  cursor: pointer;
  align-self: flex-start;
  margin-top: 0.2em;
}

.contact-form button:hover {
  background: var(--text);
  color: var(--bg);
}

/* Honeypot: off-screen rather than display:none, so bots still fill it in. */
.contact-form .hp {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  opacity: 0;
}

.form-ok,
.form-err {
  margin: 0;
  font-size: 0.75em;
  line-height: 1.3;
}

.form-err {
  color: #f0b36b;
}
