/* archivo-vietnamese-wght-normal */
@font-face {
  font-family: 'Archivo Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url(/_astro/archivo-vietnamese-wght-normal.XAtsl5Q_.woff2) format('woff2-variations');
  unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB;
}

/* archivo-latin-ext-wght-normal */
@font-face {
  font-family: 'Archivo Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url(/_astro/archivo-latin-ext-wght-normal.C4zznr8T.woff2) format('woff2-variations');
  unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF;
}

/* archivo-latin-wght-normal */
@font-face {
  font-family: 'Archivo Variable';
  font-style: normal;
  font-display: swap;
  font-weight: 100 900;
  src: url(/_astro/archivo-latin-wght-normal.E0tuGl4L.woff2) format('woff2-variations');
  unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}@font-face {
  font-family: 'Anton';
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url('/fonts/anton-latin-400.woff2') format('woff2');
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
    U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193,
    U+2212, U+2215, U+FEFF, U+FFFD;
}

/* ==========================================================================
   safal.dev: hand-painted Kathmandu trade boards screwed to a sunlit wall.
   Light only. The wall is plaster; the boards are enamel on steel; the
   lettering is sign-painter's chrome yellow and cream with a hard cast shadow.
   ========================================================================== */

:root {
  /* Ground: sunlit lime plaster */
  --wall: #e9e0cd;
  --wall-shade: #ddd2b9;
  --wall-deep: #cbbd9c;
  --plinth: #4c5f52;

  /* Ink */
  --ink: #17130f;
  --ink-soft: #4a4038;

  /* Enamel stock: a sign painter's three tins */
  --teal: #0d4f45;
  --oxblood: #a2301f;
  --cobalt: #17457f;

  /* Lettering */
  --yellow: #f2b524;
  --cream: #f8f3e4;
  --brass: #ad8a3c;
  --brass-dark: #7d6027;

  --font-paint: 'Anton', 'Arial Narrow', 'Helvetica Neue Condensed', 'Liberation Sans Narrow', system-ui, sans-serif;
  --font-text: 'Archivo Variable', system-ui, -apple-system, sans-serif;

  /* Spacing scale: one rhythm for the whole wall */
  --s1: 0.375rem;
  --s2: 0.75rem;
  --s3: 1.125rem;
  --s4: 1.75rem;
  --s5: 2.75rem;
  --s6: 4rem;
  --s7: 6rem;
  --s8: 9rem;

  --gutter: clamp(1.25rem, 5vw, 5rem);
  --measure: 66ch;

  color-scheme: light;
}

*,
*::before,
*::after {
  box-sizing: border-box;
}

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

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

body {
  margin: 0;
  font-family: var(--font-text);
  font-size: clamp(1rem, 0.94rem + 0.25vw, 1.0625rem);
  line-height: 1.6;
  color: var(--ink);
  background-color: var(--wall);
  /* Plaster: sun from above, trowel mottling, no noise filter. */
  background-image:
    radial-gradient(64rem 34rem at 14% -6%, rgba(255, 253, 244, 0.95), transparent 58%),
    radial-gradient(26rem 20rem at 62% 6%, rgba(255, 252, 240, 0.55), transparent 66%),
    radial-gradient(34rem 26rem at 92% 16%, rgba(163, 145, 106, 0.3), transparent 64%),
    radial-gradient(22rem 30rem at 4% 44%, rgba(163, 145, 106, 0.26), transparent 62%),
    radial-gradient(40rem 30rem at 78% 56%, rgba(163, 145, 106, 0.2), transparent 66%),
    radial-gradient(28rem 22rem at 30% 78%, rgba(255, 253, 244, 0.6), transparent 64%),
    radial-gradient(46rem 32rem at 88% 94%, rgba(163, 145, 106, 0.28), transparent 68%),
    radial-gradient(130% 90% at 50% 40%, transparent 52%, rgba(120, 104, 74, 0.16) 100%);
  background-attachment: fixed;
  overflow-x: hidden;
}

/* --- Browser surfaces: these belong to the design too -------------------- */

::selection {
  background: var(--yellow);
  color: var(--ink);
}

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

.board :focus-visible,
.plate:focus-visible {
  outline-color: var(--yellow);
  outline-offset: 4px;
}

html {
  scrollbar-color: var(--brass) var(--wall-shade);
  scrollbar-width: thin;
  caret-color: var(--oxblood);
  /* The gutter stays when the viewer locks scrolling, so opening it never
     shifts the wall sideways mid view-transition. */
  scrollbar-gutter: stable;
}

::-webkit-scrollbar { width: 12px; height: 12px; }
::-webkit-scrollbar-track { background: var(--wall-shade); }
::-webkit-scrollbar-thumb {
  background: var(--brass);
  border: 3px solid var(--wall-shade);
  border-radius: 6px;
}
::-webkit-scrollbar-thumb:hover { background: var(--brass-dark); }

/* --- Painted lettering --------------------------------------------------- */

.lettering {
  font-family: var(--font-paint);
  font-weight: 400;
  text-transform: uppercase;
  letter-spacing: 0.005em;
  line-height: 0.92;
  /* A sign painter's cast shadow: hard, down and right, one colour. */
  text-shadow: 0.055em 0.07em 0 var(--cast, rgba(23, 19, 15, 0.55));
}

h1, h2, h3, h4, h5, h6 {
  margin: 0;
  font-weight: inherit;
  text-wrap: balance;
}

dl, dd, ul { margin: 0; }

p { margin: 0; }

a { color: inherit; }

/* --- Section headings painted straight onto the plaster ------------------ */

.wall-heading {
  font-family: var(--font-paint);
  text-transform: uppercase;
  color: var(--ink);
  font-size: clamp(2.1rem, 1.1rem + 4.4vw, 4.75rem);
  line-height: 0.9;
  letter-spacing: 0.004em;
  text-shadow: 0.05em 0.06em 0 rgba(120, 104, 74, 0.34);
  margin-bottom: var(--s2);
}

.wall-note {
  max-width: var(--measure);
  color: var(--ink-soft);
  font-size: 1.0625rem;
}

.section {
  padding-block: clamp(3.25rem, 7vw, var(--s7));
  padding-inline: var(--gutter);
  max-width: 80rem;
  margin-inline: auto;
}

/* A painted rule: the line a sign painter snaps before lettering. The brass
   stub rides on top of the plaster band, so it is the first layer. */
.snap-line {
  height: 3px;
  border: 0;
  margin: var(--s3) 0 var(--s5);
  background-image:
    linear-gradient(90deg, var(--brass), var(--brass)),
    linear-gradient(90deg, var(--wall-deep), var(--wall-deep));
  background-size: 8rem 100%, 62% 100%;
  background-repeat: no-repeat;
}

/* The line is snapped as it comes into view: chalk runs left to right, the
   brass stub trailing the band. The range runs to mid-viewport so the draw
   happens where the reader is looking, not at the bottom edge. Same guards
   as the rake; without them the line is simply already there. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .snap-line {
      animation: snap-draw linear both;
      animation-timeline: view();
      animation-range: cover 0% cover 45%;
    }
  }
}

@keyframes snap-draw {
  from { background-size: 0rem 100%, 0% 100%; }
  to { background-size: 8rem 100%, 62% 100%; }
}

/* ==========================================================================
   The board: enamel paint on a steel plate, screwed to the wall.
   Elevation is declared once, as shadow. No borders.
   ========================================================================== */

.board {
  --paint: var(--teal);
  --letter: var(--cream);
  --soft: #b7cfc8;
  --cast: rgba(4, 24, 21, 0.62);

  position: relative;
  isolation: isolate;
  padding: clamp(1.1rem, 2.2vw, 1.9rem) clamp(1.25rem, 2.4vw, 2.1rem);
  border-radius: 4px;
  color: var(--letter);
  background-color: var(--paint);
  /* Enamel is not flat: it holds the sky at the top and pools at the base. */
  background-image: linear-gradient(
    173deg,
    rgba(255, 255, 255, 0.16) 0%,
    rgba(255, 255, 255, 0.03) 26%,
    rgba(0, 0, 0, 0.06) 68%,
    rgba(0, 0, 0, 0.16) 100%
  );
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.22) inset,
    0 -2px 6px rgba(0, 0, 0, 0.22) inset,
    0 10px 22px -8px rgba(43, 33, 20, 0.5),
    0 3px 6px -2px rgba(43, 33, 20, 0.34);
  transform: rotate(var(--hang, 0deg));
  transition: transform 260ms cubic-bezier(0.22, 1, 0.36, 1),
    box-shadow 260ms cubic-bezier(0.22, 1, 0.36, 1);
}

.board--oxblood { --paint: var(--oxblood); --soft: #f2cec3; --cast: rgba(46, 12, 6, 0.6); }
.board--cobalt  { --paint: var(--cobalt);  --soft: #c3d3ea; --cast: rgba(6, 18, 40, 0.62); }

/* The rake of sun across enamel. One authored moment, driven by scroll. */
.board::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
  z-index: 2;
  background: linear-gradient(
    104deg,
    transparent 30%,
    rgba(255, 255, 255, 0.13) 44%,
    rgba(255, 255, 255, 0.28) 50%,
    rgba(255, 255, 255, 0.12) 56%,
    transparent 70%
  );
  background-size: 260% 100%;
  background-repeat: no-repeat;
  background-position: 150% 0;
  opacity: 0;
}

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .board::after {
      opacity: 1;
      animation: rake linear both;
      animation-timeline: view();
      animation-range: entry 12% exit 88%;
    }
  }
}

@keyframes rake {
  from { background-position: 150% 0; }
  to { background-position: -60% 0; }
}

/* Screws hold the plate to the wall. */
.screw {
  position: absolute;
  width: 9px;
  height: 9px;
  border-radius: 50%;
  z-index: 3;
  background:
    linear-gradient(78deg, transparent 42%, rgba(38, 28, 8, 0.85) 42% 58%, transparent 58%),
    radial-gradient(circle at 34% 30%, #e6cf94 0%, var(--brass) 46%, var(--brass-dark) 100%);
  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.4), 0 0 0 1px rgba(0, 0, 0, 0.18);
  transition: filter 260ms cubic-bezier(0.22, 1, 0.36, 1);
}

.screw--tl { top: 9px; left: 9px; }
.screw--tr { top: 9px; right: 9px; }
.screw--bl { bottom: 9px; left: 9px; }
.screw--br { bottom: 9px; right: 9px; }

/* --- The brass date plate riveted to a board ---------------------------- */

.datestamp {
  display: inline-block;
  padding: 0.2rem 0.6rem 0.24rem;
  border-radius: 2px;
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: #2a2008;
  background-image: linear-gradient(168deg, #e2c98d 0%, var(--brass) 52%, #8e6f2d 100%);
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.34), 0 1px 0 rgba(255, 255, 255, 0.4) inset;
}

/* --- The screwed-on plate: every link and action on the wall ------------- */

.plate {
  position: relative;
  overflow: hidden;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  padding: 0.6rem 0.95rem;
  border-radius: 3px;
  font-family: var(--font-text);
  font-size: 0.9375rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  text-decoration: none;
  color: var(--ink);
  background-color: var(--cream);
  background-image: linear-gradient(172deg, rgba(255, 255, 255, 0.7), rgba(0, 0, 0, 0.06));
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.7) inset,
    0 5px 12px -5px rgba(43, 33, 20, 0.5),
    0 2px 3px -1px rgba(43, 33, 20, 0.3);
  transition: transform 180ms cubic-bezier(0.22, 1, 0.36, 1),
    box-shadow 180ms cubic-bezier(0.22, 1, 0.36, 1);
}

.plate--primary {
  background-color: var(--yellow);
  background-image: linear-gradient(172deg, rgba(255, 255, 255, 0.55), rgba(120, 78, 6, 0.16));
  font-size: 1rem;
}

.plate--dark {
  color: var(--cream);
  background-color: var(--ink);
  background-image: linear-gradient(172deg, rgba(255, 255, 255, 0.16), rgba(0, 0, 0, 0.3));
}

.plate:hover,
.plate:focus-visible {
  transform: translateY(-2px);
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.7) inset,
    0 9px 18px -6px rgba(43, 33, 20, 0.52),
    0 3px 5px -1px rgba(43, 33, 20, 0.32);
}

.plate:active { transform: translateY(0); }

/* The sheen: one streak of sun crosses the metal when a hand reaches for it. */
.plate::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(
    104deg,
    transparent 38%,
    rgba(255, 255, 255, 0.34) 50%,
    transparent 62%
  );
  background-size: 250% 100%;
  background-repeat: no-repeat;
  background-position: 165% 0;
  opacity: 0;
}

@media (prefers-reduced-motion: no-preference) {
  .plate:not([disabled]):hover::after,
  .plate:not([disabled]):focus-visible::after {
    animation: plate-sheen 560ms ease-out;
  }
}

@keyframes plate-sheen {
  from { opacity: 1; background-position: 165% 0; }
  to { opacity: 1; background-position: -65% 0; }
}

@media (pointer: coarse) {
  .plate { min-height: 44px; }
  .plate--on-board { min-height: 44px; padding-inline: 0.85rem; }
}

.plate svg {
  flex: none;
  transition: translate 180ms cubic-bezier(0.22, 1, 0.36, 1);
}

/* Icons acknowledge the reach: arrows lean the way they point, the download
   glyph dips toward the paper it drops onto. */
@media (hover: hover) {
  .plate:not([disabled]):hover svg[data-icon='arrow'] { translate: 3px 0; }
  .plate:not([disabled]):hover svg[data-icon='download'] { translate: 0 2px; }
  /* A prev plate's arrow is the same glyph mirrored, so it leans left. */
  .shots__nav--prev:not([disabled]):hover svg[data-icon='arrow'],
  .viewer__nav--prev:not([disabled]):hover svg[data-icon='arrow'] { translate: -3px 0; }
}

/* Small plates riding on a coloured board */
.plate--on-board {
  padding: 0.4rem 0.7rem;
  font-size: 0.8125rem;
  letter-spacing: 0.03em;
}

/* --- Hover: the board lifts a hair on its screws ------------------------ */

@media (hover: hover) {
  .board--interactive:hover {
    transform: rotate(var(--hang, 0deg)) translateY(-3px);
    box-shadow:
      0 1px 0 rgba(255, 255, 255, 0.26) inset,
      0 -2px 6px rgba(0, 0, 0, 0.22) inset,
      0 18px 30px -10px rgba(43, 33, 20, 0.52),
      0 5px 9px -3px rgba(43, 33, 20, 0.36);
  }

  /* Lifted toward the sun, the screw heads catch it. */
  .board--interactive:hover > .screw { filter: brightness(1.22); }
}

.board--interactive:focus-within > .screw { filter: brightness(1.22); }

.board--interactive:focus-within {
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.26) inset,
    0 -2px 6px rgba(0, 0, 0, 0.22) inset,
    0 18px 30px -10px rgba(43, 33, 20, 0.52),
    0 5px 9px -3px rgba(43, 33, 20, 0.36);
}

/* --- Reveal: every board gets hung as the reader reaches it -------------- */
/* The entrance animates the `translate` / `rotate` longhands, never
   `transform`: the fill would otherwise sit on `transform` forever and
   silently swallow the hover lift. */

@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .hangs {
      animation: hang-up linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 75%;
    }
  }
}

@keyframes hang-up {
  from {
    opacity: 0.001;
    translate: 0 26px;
    rotate: -2.2deg;
  }
  to {
    opacity: 1;
    translate: 0 0;
    rotate: 0deg;
  }
}

.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ==========================================================================
   The page. These rules are global on purpose: several of them target the
   root element of the Board component, which Astro's scoped styles cannot
   reach from the page that composes it.
   ========================================================================== */

.skip {
  position: absolute;
  left: -999px;
  top: 0;
  z-index: 20;
  padding: 0.7rem 1rem;
  background: var(--yellow);
  color: var(--ink);
  font-weight: 700;
  text-decoration: none;
}
.skip:focus { left: 0.5rem; top: 0.5rem; }

/* ---------------- The fascia ---------------- */
.fascia { position: relative; padding-top: clamp(1.25rem, 3vw, 2.5rem); }

.fascia__inner {
  max-width: 80rem;
  margin-inline: auto;
  padding-inline: var(--gutter);
  display: grid;
  grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
  gap: clamp(1.5rem, 4vw, 3.5rem);
  align-items: end;
}

.fascia__left { padding-bottom: clamp(1.75rem, 4vw, 3.25rem); }

/* The services board a shopfront hangs above its name board. */
.tradestrip {
  max-width: 44rem;
  margin-top: 2.85rem;
  padding: 0.6rem 1.6rem 0.65rem;
}
.tradestrip__list {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.1rem 0.15rem;
}
.tradestrip__item {
  font-family: var(--font-paint);
  text-transform: uppercase;
  color: var(--letter);
  font-size: clamp(0.8125rem, 0.72rem + 0.34vw, 1.0625rem);
  letter-spacing: 0.075em;
  text-shadow: 0.04em 0.05em 0 var(--cast);
}

.headboard { max-width: 44rem; }
.headboard__name {
  color: var(--yellow);
  font-size: clamp(3rem, 1rem + 8.8vw, 5.75rem);
}
/* A sign painter's thick-and-thin pair, not a progress bar. */
.headboard__rules {
  margin: clamp(0.6rem, 1.4vw, 1rem) 0 clamp(0.55rem, 1.2vw, 0.85rem);
  display: grid;
  gap: 4px;
}
.headboard__rules span { display: block; border-radius: 2px; }
.headboard__rules span:first-child { height: 4px; background: var(--cream); }
.headboard__rules span:last-child { height: 2px; background: var(--yellow); opacity: 0.85; }

/* On arrival the painter strikes the rule pair under the name, thick then
   thin, the same left-to-right stroke as the snap-lines. Load-timed, not
   scroll-timed: the head board is already up when the page opens. */
@media (prefers-reduced-motion: no-preference) {
  .headboard__rules span {
    transform-origin: 0 50%;
    animation: rule-draw 620ms cubic-bezier(0.22, 1, 0.36, 1) both;
  }
  .headboard__rules span:first-child { animation-delay: 140ms; }
  .headboard__rules span:last-child { animation-delay: 320ms; }
}

@keyframes rule-draw {
  from { scale: 0 1; }
  to { scale: 1 1; }
}

.headboard__trade {
  color: var(--cream);
  font-size: clamp(1.05rem, 0.62rem + 1.6vw, 1.75rem);
  letter-spacing: 0.055em;
}
.headboard__place {
  margin-top: 0.5rem;
  color: var(--soft);
  font-size: 0.875rem;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
}

/* The OPEN plate hangs off the board's bottom edge on its hook. */
.hanger {
  position: absolute;
  right: clamp(1rem, 3vw, 2.2rem);
  bottom: -1.85rem;
  z-index: 4;
  padding-top: 0.9rem;
}
.hanger__hook {
  position: absolute;
  top: -0.4rem;
  left: 50%;
  width: 2px;
  height: 1.6rem;
  transform: translateX(-50%) rotate(6deg);
  background: linear-gradient(180deg, var(--brass-dark), var(--brass));
  border-radius: 1px;
}
.openplate { padding: 0.55rem 1.6rem 0.6rem; }
.openplate__text {
  color: var(--cream);
  font-size: clamp(0.875rem, 0.8rem + 0.3vw, 1.0625rem);
  letter-spacing: 0.09em;
}

/* The breeze. The one object on the wall that hangs free swings itself into
   rest when the page opens, the way a plate settles when a hand has just hung
   it, then keeps stirring on its hook: a degree either side of its rest,
   pivoting where the hook meets the board. The street's air, not the plate
   acting up. Paused by script when it scrolls out of view. */
@media (prefers-reduced-motion: no-preference) {
  .openplate {
    transform-origin: 50% -1.3rem;
    animation:
      settle 3s cubic-bezier(0.33, 0, 0.67, 1) both,
      sway 4.75s cubic-bezier(0.37, 0, 0.63, 1) 3s infinite;
  }
  .openplate.is-still { animation-play-state: paused; }
}

/* A decaying pendulum that hands off, at its final angle, to the sway. */
@keyframes settle {
  0% { transform: rotate(calc(var(--hang, 0deg) + 4.5deg)); }
  28% { transform: rotate(calc(var(--hang, 0deg) - 2.9deg)); }
  54% { transform: rotate(calc(var(--hang, 0deg) + 1.9deg)); }
  78% { transform: rotate(calc(var(--hang, 0deg) - 1.5deg)); }
  100% { transform: rotate(calc(var(--hang, 0deg) - 1.15deg)); }
}

@keyframes sway {
  0%, 100% { transform: rotate(calc(var(--hang, 0deg) - 1.15deg)); }
  50% { transform: rotate(calc(var(--hang, 0deg) + 1.15deg)); }
}

.fascia__intro {
  margin-top: clamp(2.5rem, 4vw, 3rem);
  max-width: 48ch;
  font-size: clamp(1.0625rem, 1rem + 0.42vw, 1.3125rem);
  line-height: 1.52;
}
.fascia__actions { margin-top: clamp(1.3rem, 2.4vw, 1.9rem); display: flex; flex-wrap: wrap; gap: 0.7rem; }

.fascia__portrait { justify-self: center; align-self: end; line-height: 0; }
.fascia__portrait img {
  display: block;
  width: 100%;
  max-width: 22rem;
  height: auto;
  /* Sun is upper-left, so he casts down and right, onto the plaster. */
  filter: drop-shadow(16px 12px 18px rgba(70, 56, 32, 0.34));
}

/* He walks into frame: fades up and slides in off the right, settling onto the
   plinth just after the headboard's rules are struck. Time-based rather than
   scroll-driven, because the fascia is the first viewport — a `view()` timeline
   would already be run out at load and he would simply be standing there.
   Animates the `translate` longhand so nothing lodges in `transform`, and
   starts at a hair of opacity so he still counts as painted for LCP. */
@media (prefers-reduced-motion: no-preference) {
  .fascia__portrait img {
    animation: step-in 720ms cubic-bezier(0.22, 1, 0.36, 1) 120ms both;
  }
}

@keyframes step-in {
  from {
    opacity: 0.001;
    translate: 20px 26px;
  }
  to {
    opacity: 1;
    translate: 0 0;
  }
}

.plinth {
  height: clamp(26px, 3.6vw, 44px);
  background-color: var(--plinth);
  background-image: linear-gradient(
    180deg,
    rgba(255, 255, 255, 0.32) 0 2px,
    rgba(255, 255, 255, 0.08) 2px 5px,
    rgba(0, 0, 0, 0.16) 100%
  );
  box-shadow: 0 -1px 6px rgba(43, 33, 20, 0.24);
}

/* ---------------- Trades ---------------- */
.trades { display: grid; gap: clamp(0.85rem, 1.7vw, 1.4rem); }
.trades__row {
  display: grid;
  grid-template-columns: minmax(9rem, 14rem) minmax(0, 1fr);
  gap: 0.35rem clamp(1rem, 3vw, 2.5rem);
  align-items: baseline;
  padding-bottom: clamp(0.85rem, 1.7vw, 1.35rem);
  border-bottom: 1px solid var(--wall-deep);
}
.trades__row:last-child { border-bottom: 0; padding-bottom: 0; }
.trades__name {
  color: var(--ink);
  font-size: clamp(1.3rem, 0.9rem + 1.5vw, 2rem);
  text-shadow: 0.045em 0.055em 0 rgba(120, 104, 74, 0.32);
}
.trades__tools {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  font-size: clamp(1rem, 0.95rem + 0.3vw, 1.1875rem);
  font-weight: 500;
  color: var(--ink-soft);
}
.dot { color: var(--brass); padding: 0 0.4rem; font-weight: 700; }

/* ---------------- Experience ---------------- */
.stack { display: grid; gap: clamp(1rem, 2vw, 1.5rem); }
.stack--tight { gap: clamp(0.8rem, 1.5vw, 1.05rem); }

.rolecard__head {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: space-between;
  gap: 0.5rem 1.25rem;
}
.rolecard__role {
  color: var(--letter);
  font-size: clamp(1.2rem, 0.85rem + 1.2vw, 1.7rem);
  letter-spacing: 0.02em;
}
.rolecard__qual {
  font-family: var(--font-text);
  text-transform: none;
  font-weight: 500;
  font-size: 0.78em;
  letter-spacing: 0;
  color: var(--soft);
}
.rolecard__co {
  margin-top: 0.28rem;
  color: var(--soft);
  font-weight: 600;
  font-size: 1.0625rem;
}
.rolecard__notes {
  margin-top: 0.9rem;
  padding-left: 1.1rem;
  display: grid;
  gap: 0.4rem clamp(1.5rem, 4vw, 3rem);
  color: var(--letter);
}
.rolecard__notes li { max-width: 54ch; }
.rolecard__notes li::marker { color: var(--yellow); }
@media (min-width: 66rem) {
  .rolecard__notes { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

/* ---------------- The board wall ---------------- */
.group {
  margin-bottom: clamp(0.85rem, 1.7vw, 1.35rem);
  color: var(--ink-soft);
  font-size: clamp(1rem, 0.92rem + 0.45vw, 1.3rem);
  letter-spacing: 0.14em;
  text-shadow: none;
}
.group--spaced { margin-top: clamp(2.25rem, 4.5vw, 3.75rem); }

/* Every product board takes a row of its own, mobile and web alike: both
   carry real screens, and a screen is worth the width. */
.wall {
  display: grid;
  grid-template-columns: minmax(0, 1fr);
  gap: clamp(1rem, 2.2vw, 1.75rem);
  align-items: stretch;
}

.plaque { display: flex; flex-direction: column; gap: 0.45rem; }
.plaque__body { display: flex; flex-direction: column; gap: 0.45rem; flex: 1; min-width: 0; }
.plaque__head {
  display: flex;
  align-items: center;
  gap: clamp(0.5rem, 1.1vw, 0.8rem);
}
.plaque__name {
  color: var(--letter);
  font-size: clamp(1.15rem, 0.95rem + 0.8vw, 1.5rem);
  letter-spacing: 0.015em;
}
/* The product's own mark, mounted like a small print rather than dropped on
   the enamel: the same cream ground, the same fitting shadow. `contain` holds
   a square app icon and a wider wordmark equally well. */
.plaque__icon {
  --icon: clamp(2.4rem, 1.8rem + 1.7vw, 3.4rem);
  flex: none;
  width: var(--icon);
  height: var(--icon);
  padding: 3px;
  border-radius: 4px;
  object-fit: contain;
  background-color: var(--cream);
  background-image: linear-gradient(172deg, rgba(255, 255, 255, 0.7), rgba(0, 0, 0, 0.06));
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.34), 0 1px 0 rgba(255, 255, 255, 0.4) inset;
}
.plaque--wide .plaque__icon { --icon: clamp(2.7rem, 1.9rem + 2.2vw, 4rem); }
.plaque__note { color: var(--soft); font-size: 0.9688rem; line-height: 1.5; flex: 1; max-width: 54ch; }
.plaque__links { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 0.45rem; margin-top: 0.3rem; }

/* Both kinds of board hang their print beside the copy rather than above it:
   a print run the full width of a row would tower over the wall, whether it
   is 9:19.5 or 16:9. They differ only in how much of the row the print takes,
   which is the shape of the screen it holds. A site board starts stacked
   though, because a browser print squeezed into half a phone-width column
   would be a postage stamp; it turns sideways once there is room. */
.plaque--site { gap: clamp(0.7rem, 1.4vw, 1rem); }
.plaque--phone {
  --shot-col: clamp(6.5rem, 46%, 15rem);
  display: grid;
  grid-template-columns: minmax(0, var(--shot-col)) minmax(0, 1fr);
  gap: clamp(0.8rem, 1.8vw, 1.3rem);
  align-content: start;
}
/* The print stands in the first column, the copy beside it, and the controls
   run under both, where they can't crowd the copy on a narrow board. */
.plaque--phone .shots { display: contents; }
.plaque--phone .shots__frame { grid-column: 1; grid-row: 1; }
.plaque--phone .plaque__body { grid-column: 2; grid-row: 1; }
.plaque--phone .shots__bar { grid-column: 1 / -1; grid-row: 2; margin-top: 0; }
/* A board given no print at all keeps its copy in one column, and until the
   wall is wide it takes only the width that copy needs rather than stretching
   a bare row. Less to show, less board. */
.plaque--phone:not(:has(.shots)),
.plaque--site:not(:has(.shots)) { grid-template-columns: minmax(0, 1fr); max-width: 44rem; }
.plaque--phone:not(:has(.shots)) .plaque__body,
.plaque--site:not(:has(.shots)) .plaque__body { grid-column: 1; }

/* On a wide wall a printless board stops being an offcut and squares up with
   the boards around it. The lone mobile one takes the full row, so the Mobile
   run ends flush instead of trailing off. The web ones are a pair, so they
   hang two to a row and finish the wall level rather than leaving two short
   boards stacked down one side. Below this the wall is one column and they
   stay stacked. */
@media (min-width: 56rem) {
  .wall { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .wall > * { grid-column: span 2; }
  .wall > .plaque--phone:not(:has(.shots)) { max-width: none; }
  .wall > .plaque--site:not(:has(.shots)) { grid-column: span 1; max-width: none; }
}

/* Once a phone board is wide, its copy sits centred against the standing
   print rather than stranding a field of empty enamel below itself, and
   reads a size up. */
@media (min-width: 36rem) {
  .plaque--phone:has(.shots) .plaque__body { justify-content: center; }
  .plaque--phone:has(.shots) .plaque__note { flex: 0 1 auto; font-size: 1.0625rem; }
  .plaque--phone .plaque__name { font-size: clamp(1.45rem, 1rem + 1.4vw, 2.1rem); }
}

/* A phone board with a full row to itself hangs two prints at once: one
   standing print can't fill a row this wide without towering over it. */
@media (min-width: 48rem) {
  .plaque--phone { --shot-per: 2; --shot-col: min(62%, 32rem); }
  /* Two prints on one mount read as two prints, not a spread. */
  .plaque--phone .shots__track { --shot-gap: 12px; }
  .plaque--phone.plaque--wide { --shot-col: min(66%, 36rem); }
  .plaque--phone.plaque--wide .plaque__name { font-size: clamp(1.7rem, 1rem + 1.8vw, 2.6rem); }
}

/* Once a site board has the width for it, the browser print turns sideways
   and the copy comes up beside it, the same arrangement the phone boards
   keep. A landscape print earns more of the row than a standing one does:
   past half the board a 16:9 print is still only a ninth of it tall. */
@media (min-width: 44rem) {
  .plaque--site:has(.shots) {
    --shot-col: min(56%, 42rem);
    display: grid;
    grid-template-columns: minmax(0, var(--shot-col)) minmax(0, 1fr);
    gap: clamp(0.9rem, 2vw, 1.5rem);
    align-content: start;
  }
  /* The frame, the copy and the controls take their own cells, so the plates
     run under both columns instead of crowding the copy. */
  .plaque--site:has(.shots) .shots { display: contents; }
  .plaque--site:has(.shots) .shots__frame { grid-column: 1; grid-row: 1; }
  .plaque--site:has(.shots) .plaque__body { grid-column: 2; grid-row: 1; justify-content: center; }
  .plaque--site:has(.shots) .shots__bar { grid-column: 1 / -1; grid-row: 2; margin-top: 0; }
  .plaque--site:has(.shots) .plaque__note { flex: 0 1 auto; font-size: 1.0625rem; }
  .plaque--site .plaque__name { font-size: clamp(1.45rem, 1rem + 1.4vw, 2.1rem); }
}

@media (min-width: 56rem) {
  /* The emphasis the CV itself gives a board: a larger print and a larger
     name, the same step up a wide phone board takes. */
  .plaque--site.plaque--wide:has(.shots) { --shot-col: min(60%, 46rem); }
  .plaque--site.plaque--wide .plaque__name { font-size: clamp(1.7rem, 1rem + 1.8vw, 2.6rem); }
  .plaque--site.plaque--wide .plaque__note { font-size: 1.0625rem; max-width: 52ch; }
}

/* ---------------- Screenshots: prints mounted on the board -------------- */
/* The print is a cream-margined photograph screwed to the enamel: the fitting
   shadow off the date plate, no border, no new elevation. Its frame carries
   the product's own aspect ratio in --shot-ratio, set from the content file. */
.shots { margin: 0; min-width: 0; }

.shots__frame {
  padding: 4px;
  border-radius: 3px;
  background-color: var(--cream);
  background-image: linear-gradient(172deg, rgba(255, 255, 255, 0.7), rgba(0, 0, 0, 0.06));
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.34), 0 1px 0 rgba(255, 255, 255, 0.4) inset;
}

.shots__track {
  --shot-gap: 4px;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: calc((100% - (var(--shot-per, 1) - 1) * var(--shot-gap)) / var(--shot-per, 1));
  gap: var(--shot-gap);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x mandatory;
  border-radius: 2px;
  /* The brass scrollbar belongs to the wall, not inside a print. */
  scrollbar-width: none;
}
.shots__track::-webkit-scrollbar { display: none; }

/* A strip whose prints are not all one shape hangs them at a common height
   rather than a common width, the way a contact strip does. The mount keeps
   the frame's shape; each print takes the width its own shape asks for, so a
   standing app screen and a browser screen sit side by side and neither is
   cropped to look like the other. */
.shots--mixed .shots__track {
  display: flex;
  aspect-ratio: var(--shot-ratio);
}
.shots--mixed .shots__slide { flex: none; height: 100%; }
.shots--mixed .shots__slide img {
  width: auto;
  height: 100%;
  /* Each print's own shape, carried on the print, not the mount's. Declaring
     it rather than leaning on the file's intrinsic size means the strip is
     the right width before a single capture has loaded, which is what the
     plates measure themselves against. */
  aspect-ratio: var(--print-ratio);
}

.shots__slide { scroll-snap-align: start; scroll-snap-stop: always; }
.shots__slide img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: var(--shot-ratio);
  object-fit: cover;
  object-position: top center;
  border-radius: 2px;
  background-color: var(--wall-shade);
}

.shots__bar { display: flex; align-items: center; gap: 0.4rem; margin-top: 0.55rem; }
.shots__bar[hidden] { display: none; }
.shots__page { display: flex; align-items: center; gap: 0.4rem; }
.shots__page[hidden] { display: none; }

/* Once the script is up, a print is a way into the viewer. */
[data-shots-live] .shots__slide img { cursor: zoom-in; }

.shots__nav { padding: 0.4rem 0.5rem; }
.shots__nav--prev svg { transform: scaleX(-1); }
.shots__nav[disabled] {
  opacity: 0.42;
  transform: none;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 1px 0 rgba(255, 255, 255, 0.4) inset;
  cursor: default;
}

.shots__count { font-size: 0.75rem; letter-spacing: 0.09em; }

/* ---------------- The viewer: a print held up to the light -------------- */
/* The wall goes dark and one print is held against it at the size the capture
   actually is. This is the only place a screenshot is not cropped: `contain`
   here, `cover` on the boards. No new elevation, no new shapes. */
.viewer {
  margin: auto;
  width: 100%;
  height: 100%;
  max-width: 100vw;
  max-height: 100vh;
  padding: clamp(0.9rem, 2.5vw, 2.25rem);
  border: 0;
  border-radius: 0;
  color: var(--cream);
  background: rgba(23, 19, 15, 0.88);
  overflow: hidden;
}
.viewer:not([open]) { display: none; }
.viewer[open] {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: clamp(0.7rem, 1.8vw, 1.1rem);
}
.viewer::backdrop { background: rgba(23, 19, 15, 0.88); }
html.viewer-open { overflow: hidden; }

.viewer__of {
  flex: none;
  color: var(--yellow);
  font-size: clamp(1.05rem, 0.9rem + 0.7vw, 1.55rem);
  letter-spacing: 0.02em;
  text-align: center;
  --cast: rgba(0, 0, 0, 0.55);
}

.viewer__stage {
  flex: 1 1 auto;
  min-height: 0;
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
  /* Sideways gestures page the viewer; pinch to zoom a screenshot still works. */
  touch-action: pinch-zoom;
  -webkit-user-select: none;
  user-select: none;
}

/* The print carries its own cream mount, the same 4px margin the boards use.
   It sits directly in the stage: a wrapper would have no height of its own for
   the print's `max-height` to measure against, and the print would overflow. */
.viewer__print {
  display: block;
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  padding: 4px;
  object-fit: contain;
  border-radius: 3px;
  background-color: var(--cream);
  background-image: linear-gradient(172deg, rgba(255, 255, 255, 0.7), rgba(0, 0, 0, 0.06));
  box-shadow:
    0 1px 0 rgba(255, 255, 255, 0.22) inset,
    0 10px 22px -8px rgba(0, 0, 0, 0.6),
    0 3px 6px -2px rgba(0, 0, 0, 0.45);
}

.viewer__bar { flex: none; display: flex; align-items: center; gap: clamp(0.5rem, 1.6vw, 1rem); }
.viewer__page { display: flex; align-items: center; gap: 0.5rem; }
.viewer__page[hidden] { display: none; }
.viewer__nav { padding: 0.6rem 0.7rem; }
.viewer__nav--prev svg { transform: scaleX(-1); }
.viewer__nav[disabled] {
  opacity: 0.42;
  transform: none;
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 1px 0 rgba(255, 255, 255, 0.4) inset;
  cursor: default;
}
.viewer__count { font-size: 0.8125rem; }

@media (prefers-reduced-motion: no-preference) {
  /* Light coming up on a fixed object. */
  .viewer[open] { animation: viewer-lit 160ms ease-out both; }
  .viewer[open]::backdrop { animation: viewer-lit 160ms ease-out both; }
}
@keyframes viewer-lit {
  from { opacity: 0; }
  to { opacity: 1; }
}

/* The lift: where view transitions exist, the clicked print rises off its
   mount into the stage and settles back onto the board on close, and a page
   turn morphs one print into the next. The script only engages this when
   the browser supports it and motion is welcome; the fade above remains the
   whole story everywhere else. */
.viewer__print { view-transition-name: print; }

::view-transition-group(print) {
  animation-duration: 340ms;
  animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1);
}
::view-transition-old(print),
::view-transition-new(print) {
  animation-duration: 200ms;
}

/* While paging inside the viewer only the print morphs; the rest of the
   screen holds still instead of crossfading along. */
html.vt-paging::view-transition-group(root),
html.vt-paging::view-transition-old(root),
html.vt-paging::view-transition-new(root) {
  animation: none;
}

/* ---------------- Education ---------------- */
.edu__award {
  color: var(--letter);
  font-size: clamp(1rem, 0.88rem + 0.55vw, 1.3rem);
  letter-spacing: 0.02em;
}
.edu__status { color: var(--soft); font-weight: 500; }

/* ---------------- The doorway ---------------- */
.doorway { padding-top: clamp(1.5rem, 3vw, 2.75rem); padding-bottom: clamp(2.5rem, 5vw, 4.5rem); }
.doorway__board { max-width: 62rem; padding: clamp(1.5rem, 3.2vw, 2.75rem); }
.doorway__heading { color: var(--yellow); font-size: clamp(1.9rem, 1.1rem + 3.4vw, 3.5rem); }
.doorway__avail {
  margin-top: 0.7rem;
  color: var(--cream);
  font-size: clamp(1.05rem, 1rem + 0.5vw, 1.3125rem);
  font-weight: 500;
  max-width: 40ch;
}
.doorway__plates { margin-top: clamp(1.3rem, 2.4vw, 1.9rem); display: flex; flex-wrap: wrap; gap: 0.6rem; }
.doorway__place {
  margin-top: clamp(1.1rem, 2vw, 1.6rem);
  display: flex;
  align-items: center;
  gap: 0.45rem;
  color: var(--soft);
  font-size: 0.875rem;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

.colophon {
  margin-top: clamp(1.4rem, 2.6vw, 2.25rem);
  color: var(--ink-soft);
  font-size: 0.8125rem;
  font-weight: 600;
  letter-spacing: 0.22em;
  text-transform: uppercase;
}

/* ---------------- Narrow ---------------- */
@media (max-width: 54rem) {
  /* On a phone you read the boards first and meet him at the bottom, standing
     on the plinth. Putting the portrait first left half a viewport of bare
     plaster above the name and cropped him off mid-thigh with no ground. */
  .fascia { padding-top: clamp(0.75rem, 2.5vw, 1.5rem); }
  .fascia__inner {
    grid-template-columns: minmax(0, 1fr) auto;
    gap: 0;
    column-gap: clamp(0.8rem, 3vw, 1.5rem);
  }
  .fascia__left { display: contents; }
  .headboard, .tradestrip { grid-column: 1 / -1; }
  .fascia__intro { grid-column: 1; grid-row: 3; margin-top: clamp(1.6rem, 3.4vw, 2.4rem); }
  .fascia__actions {
    grid-column: 1;
    grid-row: 4;
    align-self: end;
    flex-direction: column;
    align-items: stretch;
    max-width: 24rem;
    margin-bottom: clamp(1.5rem, 4vw, 2.25rem);
  }
  /* The plates share the wall with the portrait here, so they wear a
     slightly narrower stamp to keep off his sleeve. */
  .fascia__actions .plate { padding-inline: 0.7rem; }
  .fascia__actions .plate--primary { font-size: 0.9375rem; }
  /* He stands beside the intro, not below it, so the wall keeps no hole. */
  .fascia__portrait {
    grid-column: 2;
    grid-row: 3 / 5;
    align-self: end;
    justify-self: end;
    margin-right: calc(var(--gutter) * -0.55);
    margin-bottom: 0;
  }
  .fascia__portrait img { max-width: min(42vw, 14rem); }
  .headboard, .tradestrip { max-width: none; }
  .tradestrip { margin-top: 0.7rem; }
  /* Separators would otherwise dangle at a line end when the list wraps. */
  .tradestrip .dot { display: none; }
  .tradestrip__list { gap: 0.2rem 0.9rem; }
  /* On narrow screens the hook plate hangs below the board instead of over it.
     It stays positioned, or the hook lets go of the plate and rides up to the
     board's own top-left corner. */
  .hanger {
    position: relative;
    right: auto;
    bottom: auto;
    padding-top: 0.85rem;
    margin: 0.9rem 0 0 0;
    /* Shrink to the plate, or the hook centres itself on the whole board
       and hangs off to one side of what it carries. */
    width: fit-content;
  }
  .hanger__hook { top: 0; }
  /* The hook sits mid-plate here as it does at full width, so the swing
     pivots on the middle of the top edge. */
  .openplate { transform-origin: 50% -0.85rem; }
  .trades__row { grid-template-columns: minmax(0, 1fr); }
}

/* The smallest phones: the email plate is as narrow as it honestly gets,
   so the portrait cedes the difference. */
@media (max-width: 24rem) {
  .fascia__portrait img { max-width: 38vw; }
}

/* Below ~340px the side-by-side split leaves him a sliver with bare wall
   overhead, so the fascia stacks instead and he takes the plinth centred,
   full presence. */
@media (max-width: 21.25rem) {
  .fascia__intro, .fascia__actions { grid-column: 1 / -1; }
  .fascia__actions { max-width: none; margin-bottom: 1.4rem; }
  .fascia__portrait {
    grid-column: 1 / -1;
    grid-row: 5;
    justify-self: center;
    margin-right: 0;
  }
  .fascia__portrait img { max-width: 60vw; }
  /* Long URL plates (github.com/..., linkedin.com/...) have no natural
     break points; let them break rather than push the wall sideways. */
  .plate { overflow-wrap: anywhere; }
}
