/* ============================================================
   L'APERTURA — Lorenzo Montes
   The name is the window. The work is the only colour.
   ============================================================ */

:root {
  --bone: #EFEEE9;
  --bone-2: #E6E4DD;
  --ink: #0D0D0F;
  --ink-2: #17181C;
  --crew: #28BCA9;
  --farro: #F8D05C;

  --rule: rgba(13, 13, 15, 0.13);
  --rule-strong: rgba(13, 13, 15, 0.28);
  --mute: rgba(13, 13, 15, 0.56);
  --faint: rgba(13, 13, 15, 0.34);

  --pad: clamp(20px, 5vw, 76px);
  --bar: 40px;

  --ease: cubic-bezier(0.16, 1, 0.3, 1);
  --ease-io: cubic-bezier(0.65, 0, 0.35, 1);

  --display: "Archivo", "Helvetica Neue", Arial, sans-serif;
  --mono: "Spline Sans Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
}

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

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

body {
  margin: 0;
  background: var(--bone);
  color: var(--ink);
  font-family: var(--display);
  font-size: 17px;
  line-height: 1.5;
  font-variant-numeric: tabular-nums;
  -webkit-font-smoothing: antialiased;
  padding-bottom: var(--bar);
  overflow-x: hidden;
}

img { max-width: 100%; display: block; }
a { color: inherit; }

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

/* ---------- shared type ---------- */

.mono {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 400;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}

.idx {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.1em;
  color: var(--faint);
}

h1, h2, h3 { margin: 0; font-weight: 800; letter-spacing: -0.03em; line-height: 0.94; }

.section-title {
  font-size: clamp(2.1rem, 5.2vw, 4.2rem);
  max-width: 20ch;
}

.lede {
  font-size: clamp(1.15rem, 2vw, 1.6rem);
  font-weight: 300;
  line-height: 1.42;
  letter-spacing: -0.01em;
  color: var(--mute);
  max-width: 46ch;
}

/* ---------- nav ---------- */

.nav {
  position: fixed;
  inset: 0 0 auto 0;
  z-index: 60;
  display: flex;
  align-items: center;
  gap: 28px;
  padding: 14px var(--pad);
  background: color-mix(in srgb, var(--bone) 72%, transparent);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-bottom: 1px solid var(--rule);
}

.nav__mark {
  font-family: var(--mono);
  font-size: 11px;
  font-weight: 600;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  text-decoration: none;
  margin-right: auto;
}

.nav__links { display: flex; gap: 22px; }

.nav__link {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--mute);
  transition: color 0.2s var(--ease);
}
.nav__link i { font-style: normal; color: var(--faint); margin-right: 6px; }
.nav__link:hover { color: var(--ink); }

.pill {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 10px 18px;
  border-radius: 999px;
  background: var(--ink);
  color: var(--bone);
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  text-decoration: none;
  border: 1px solid var(--ink);
  transition: transform 0.3s var(--ease), background 0.3s var(--ease), color 0.3s var(--ease);
}
.pill:hover { transform: translateY(-2px); }
button.pill { cursor: pointer; -webkit-appearance: none; appearance: none; }

/* Copy feedback sits on the control itself rather than in a toast, and the
   label never moves: NN/G's point is that a changing label costs the reader
   a re-read, while a state change on the button is understood at a glance. */
.pill--copy { gap: 12px; }
.pill--copy .ico { width: 13px; height: 13px; flex: none; transition: opacity 0.18s var(--ease); }
.pill--copy .ico--check { display: none; color: var(--crew); }
.pill--copy.is-copied { border-color: var(--crew); }
.pill--copy.is-copied .ico--copy { display: none; }
.pill--copy.is-copied .ico--check { display: block; }
.copy-live { align-self: center; color: var(--faint); }
.pill--ghost { background: transparent; color: var(--ink); border-color: var(--rule-strong); }
.pill--ghost:hover { background: var(--ink); color: var(--bone); border-color: var(--ink); }
.pill svg { width: 11px; height: 11px; }

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

.hero {
  min-height: 100svh;
  padding: 94px var(--pad) 34px;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.hero__over { color: var(--faint); margin-bottom: clamp(16px, 3.6vh, 34px); }
.hero__over b { font-weight: 400; color: var(--mute); }

/* Three lines now — Lorenzo / Alejandro / Montes — so the type is sized to
   the block, not to a single word. Alejandro is the widest at 9 characters
   and sets the ceiling. */
.name {
  display: block;
  font-size: clamp(2.5rem, 11vw, 8.6rem);
  line-height: 0.87;
  letter-spacing: -0.045em;
  font-weight: 800;
  text-transform: uppercase;
}

/* THE APERTURE — the letterforms are a window onto the vocabulary the work
   is made of. A screenshot inside a 150px band is texture, not an interface;
   a word is legible at any size, which is why this is a lexicon and not a UI.
   `cover` guarantees the band is always full — no empty ink, ever.
   Percentages travel top→bottom: the offset is (band − image) × P, so a
   negative value pushes the image out and leaves the band blank. */
.name--open {
  background-color: var(--ink);
  background-image: url("../img/aperture-lexicon.svg?v=10");
  background-repeat: no-repeat;
  background-size: cover;
  background-position: 50% var(--ap, 0%);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

.aperture-rule {
  border: 0;
  border-top: 1px dashed var(--rule);
  margin: 0;
}

.hero__say {
  margin: clamp(20px, 3.4vh, 38px) 0 0;
  font-size: clamp(1.25rem, 2.2vw, 1.95rem);
  font-weight: 300;
  line-height: 1.3;
  letter-spacing: -0.02em;
  max-width: 26ch;
}
.hero__sub { margin: 12px 0 0; color: var(--faint); }

.hero__acts { display: flex; flex-wrap: wrap; gap: 12px; margin-top: clamp(20px, 3vh, 34px); }

/* ---------- generic section ---------- */

.sec { padding: clamp(72px, 12vh, 148px) var(--pad); border-top: 1px solid var(--rule); }
.sec__head { display: flex; align-items: baseline; gap: 18px; margin-bottom: clamp(34px, 6vh, 64px); }
.sec__head .idx { flex: none; }

/* ---------- work ---------- */

.work { display: grid; gap: 0; }

.piece {
  display: grid;
  grid-template-columns: minmax(0, 1fr) clamp(168px, 17vw, 224px);
  gap: clamp(20px, 4vw, 56px);
  align-items: start;
  padding: clamp(28px, 4vh, 46px) 0;
  border-top: 1px solid var(--rule);
}
.piece:last-child { border-bottom: 1px solid var(--rule); }

.piece__name {
  font-size: clamp(2rem, 5.6vw, 4.4rem);
  line-height: 0.94;
  letter-spacing: -0.035em;
  display: flex;
  align-items: baseline;
  gap: 16px;
  flex-wrap: wrap;
}
.piece__what { margin: 14px 0 0; font-size: clamp(1.05rem, 1.5vw, 1.35rem); font-weight: 300; color: var(--mute); max-width: 54ch; }
.piece__acts { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 22px; }

.tag {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  padding: 5px 10px;
  border-radius: 3px;
  white-space: nowrap;
}
.tag--live { background: var(--crew); color: #05312B; }
.tag--soon { border: 1px solid var(--rule-strong); color: var(--mute); }
.tag--closed { border: 1px dashed var(--rule-strong); color: var(--faint); }

.piece__shot {
  border-radius: 14px;
  overflow: hidden;
  border: 1px solid var(--rule);
  background: var(--bone-2);
}
/* Crop to the working part of the screen: an uncropped 620×1385 phone shot
   makes a 650px row against 250px of text and opens a dead void underneath. */
.piece__shot img { width: 100%; height: auto; aspect-ratio: 620 / 880; object-fit: cover; object-position: 50% 0; }
.piece__meta { margin: 18px 0 0; color: var(--faint); }
/* The product mark, rendered as an object rather than a flat favicon.
   A screenshot in this slot was arbitrary — you cannot read a phone screen
   at 200px, so it was decoration pretending to be evidence. */
.piece__icon { margin: 0; }
.piece__icon img { width: 100%; height: auto; }
.piece__icon--quiet { opacity: 0.72; }

/* ---------- inverted block ---------- */

.dark {
  background: var(--ink);
  color: var(--bone);
  --rule: rgba(239, 238, 233, 0.14);
  --rule-strong: rgba(239, 238, 233, 0.3);
  --mute: rgba(239, 238, 233, 0.62);
  --faint: rgba(239, 238, 233, 0.38);
}
.dark .pill { background: var(--bone); color: var(--ink); border-color: var(--bone); }
.dark .pill--ghost { background: transparent; color: var(--bone); border-color: var(--rule-strong); }
.dark .pill--ghost:hover { background: var(--bone); color: var(--ink); }

.gripes { display: grid; gap: 0; }
.gripe { padding: clamp(30px, 5vh, 54px) 0; border-top: 1px solid var(--rule); display: grid; grid-template-columns: 60px 1fr; gap: clamp(16px, 3vw, 40px); }
.gripe:last-child { border-bottom: 1px solid var(--rule); }
.gripe__t { font-size: clamp(1.35rem, 2.6vw, 2.1rem); font-weight: 700; letter-spacing: -0.025em; line-height: 1.1; margin: 0 0 16px; }
.gripe__b { margin: 0; font-size: clamp(1.02rem, 1.4vw, 1.18rem); font-weight: 300; line-height: 1.55; color: var(--mute); max-width: 60ch; }
.gripe__b em { font-style: normal; color: var(--bone); }

/* The AI entry opens instead of sprawling: the one-line answer is the honest
   default, and the long answer is there for whoever actually wants it. */
.ai summary { list-style: none; cursor: pointer; display: block; }
.ai summary::-webkit-details-marker { display: none; }
.ai summary:focus-visible { outline: 2px solid var(--crew); outline-offset: 6px; }
.ai__more {
  display: inline-flex; align-items: center; gap: 7px;
  margin-left: 8px; padding: 3px 11px 4px;
  border: 1px solid var(--rule-strong); border-radius: 999px;
  font-family: var(--mono); font-size: 10px; letter-spacing: 0.12em;
  text-transform: uppercase; color: var(--mute); white-space: nowrap;
  transition: background 0.24s var(--ease), color 0.24s var(--ease);
}
.ai__more::after { content: "+"; font-size: 13px; line-height: 1; }
.ai summary:hover .ai__more { background: var(--bone); color: var(--ink); }
.ai[open] .ai__more::after { content: "–"; }
.ai[open] .ai__more { background: var(--bone); color: var(--ink); }
.ai__body { margin-top: 26px; padding-left: clamp(0px, 2vw, 26px); border-left: 1px solid var(--rule); }
.ai__body p { margin: 0 0 1em; font-size: clamp(1rem, 1.35vw, 1.12rem); font-weight: 300; line-height: 1.6; color: var(--mute); max-width: 58ch; }
.ai__body p:last-child { margin-bottom: 0; }
.ai__body strong { font-weight: 600; color: var(--bone); }

/* ---------- method / prose ---------- */

.cols { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: clamp(28px, 4vw, 60px); }
.note h3 { font-size: 1.18rem; font-weight: 700; letter-spacing: -0.015em; margin-bottom: 10px; }
.note p { margin: 0; color: var(--mute); font-weight: 300; line-height: 1.58; }

.prose p { font-size: clamp(1.05rem, 1.5vw, 1.28rem); font-weight: 300; line-height: 1.6; color: var(--mute); max-width: 58ch; }
.prose p + p { margin-top: 1.1em; }
.prose strong { font-weight: 600; color: var(--ink); }
.dark .prose strong { color: var(--bone); }

/* ---------- history ---------- */

.hist { border-top: 1px solid var(--rule); }
.hist__row { display: grid; grid-template-columns: 96px 1fr 1.3fr; gap: clamp(12px, 3vw, 40px); padding: 20px 0; border-bottom: 1px solid var(--rule); align-items: baseline; }
.hist__yr { font-family: var(--mono); font-size: 11px; letter-spacing: 0.08em; color: var(--faint); }
.hist__co { font-weight: 700; letter-spacing: -0.015em; font-size: 1.1rem; }
.hist__co span { display: block; font-family: var(--mono); font-size: 10px; letter-spacing: 0.13em; text-transform: uppercase; color: var(--faint); font-weight: 400; margin-top: 5px; }
.hist__d { color: var(--mute); font-weight: 300; font-size: 1rem; line-height: 1.5; }

/* ---------- portrait ---------- */

.about { display: grid; grid-template-columns: minmax(0, 1.9fr) clamp(220px, 24vw, 320px); gap: clamp(28px, 5vw, 76px); align-items: start; }
.portrait { border-radius: 14px; overflow: hidden; border: 1px solid var(--rule); filter: grayscale(1) contrast(1.06); }
/* The source photograph is already a tight head crop, so there is no way to
   zoom out except to give it room: the mat is what makes the face read
   smaller inside the frame. */
/* The source photograph is a tight head crop, so "further away" cannot come
   from the image — only from giving it room inside the frame. The mat does
   the pulling back. */
.portrait {
  margin: 0;
  padding: clamp(24px, 4vw, 56px);
  background: var(--bone-2);
}
.portrait img { display: block; width: 100%; height: auto; aspect-ratio: 1 / 1; object-fit: cover; object-position: 50% 42%; border-radius: 3px; }

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

.end { padding: clamp(84px, 16vh, 190px) var(--pad) clamp(60px, 10vh, 120px); }
.end__big { font-size: clamp(2.4rem, 8vw, 7rem); line-height: 0.94; letter-spacing: -0.04em; font-weight: 800; }
.end__acts { display: flex; flex-wrap: wrap; gap: 12px; margin-top: clamp(28px, 5vh, 52px); }
.colophon {
  margin-top: clamp(48px, 9vh, 110px);
  padding-top: 22px;
  border-top: 1px solid var(--rule);
  display: grid;
  grid-template-columns: 1fr auto auto;
  align-items: baseline;
  gap: 10px clamp(24px, 6vw, 80px);
  color: var(--faint);
}
.colophon__name { font-weight: 700; letter-spacing: -0.01em; color: var(--mute); font-size: 1rem; }
@media (max-width: 680px) {
  .colophon { grid-template-columns: 1fr; gap: 8px; }
}

/* ---------- status bar ---------- */

.status {
  position: fixed;
  inset: auto 0 0 0;
  z-index: 70;
  height: var(--bar);
  display: flex;
  align-items: center;
  gap: 18px;
  padding: 0 var(--pad);
  background: color-mix(in srgb, var(--bone) 80%, transparent);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-top: 1px solid rgba(13, 13, 15, 0.13);
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(13, 13, 15, 0.4);
}
.status b { font-weight: 400; color: rgba(13, 13, 15, 0.78); }
.status__mid { margin-left: auto; margin-right: auto; }
.status__end { margin-left: auto; }
.status__dot { width: 6px; height: 6px; border-radius: 50%; background: var(--crew); display: inline-block; vertical-align: 1px; margin-right: 7px; }

/* ---------- reveal ---------- */

.rv { opacity: 0; transform: translateY(24px); transition: opacity 0.85s var(--ease), transform 0.85s var(--ease); }
.rv.in { opacity: 1; transform: none; }

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

@media (max-width: 900px) {
  .piece { grid-template-columns: 1fr; }
  .piece__shot { max-width: 260px; }
  .about { grid-template-columns: 1fr; }
  .portrait { max-width: 300px; }
  .hist__row { grid-template-columns: 78px 1fr; }
  .hist__d { grid-column: 2; }
}

@media (max-width: 680px) {
  body { font-size: 16px; }

  /* On a one-page site the section links matter more than a wordmark
     that only repeats the H1 two lines below it. */
  .nav { gap: 0; padding: 11px var(--pad); justify-content: space-between; }
  .nav__mark { display: none; }
  .nav__links { display: flex; gap: 14px; margin-right: auto; }
  .nav__link { font-size: 9.5px; letter-spacing: 0.08em; }
  .nav__link i { display: none; }
  .nav .pill { display: none; }

  .name { font-size: clamp(2.2rem, 15vw, 5rem); letter-spacing: -0.04em; }

  /* The lexicon needs no breakpoint of its own: `cover` fills the band at
     any width, and a word stays a word at 60px of letter height. */

  .hero { padding-top: 88px; }
  .gripe { grid-template-columns: 1fr; gap: 10px; }
  .piece__shot { max-width: 220px; }
  .status { gap: 10px; font-size: 9px; letter-spacing: 0.1em; }
  .status__mid { display: none; }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .rv { opacity: 1; transform: none; transition: none; }
  .pill { transition: none; }
  * { animation-duration: 0.001ms !important; }
}

/* ============================================================
   MOTION — added last, on purpose. The page had to be right
   standing still before anything was allowed to move.
   Everything below degrades to "already visible" without JS
   and switches off entirely under prefers-reduced-motion.
   ============================================================ */

.end__say {
  margin: clamp(18px, 3vh, 30px) 0 0;
  font-size: clamp(1.05rem, 1.6vw, 1.35rem);
  font-weight: 300;
  line-height: 1.5;
  color: var(--mute);
  max-width: 42ch;
}

/* ---------- back to top: the scroll readout becomes the control ---------- */

.totop {
  position: relative;
  width: 26px; height: 26px;
  margin-right: 4px;
  flex: none;
  display: grid; place-items: center;
  background: none; border: 0; padding: 0;
  color: rgba(13, 13, 15, 0.78);
  cursor: pointer;
  opacity: 0;
  transform: scale(0.7);
  pointer-events: none;
  transition: opacity 0.45s var(--ease), transform 0.45s var(--ease);
}
.totop.is-on { opacity: 1; transform: none; pointer-events: auto; }
.totop__ring { position: absolute; inset: 0; width: 100%; height: 100%; transform: rotate(-90deg); }
.totop__track { stroke: rgba(13, 13, 15, 0.16); }
.totop__fill {
  stroke: var(--crew);
  stroke-dasharray: 100;
  stroke-dashoffset: calc(100 - var(--prog, 0) * 100);
  stroke-linecap: round;
  transition: stroke-dashoffset 0.12s linear;
}
.totop__arrow { width: 11px; height: 11px; transition: transform 0.3s var(--ease); }
.totop:hover .totop__arrow { transform: translateY(-2px); }
.totop:focus-visible { outline: 2px solid var(--crew); outline-offset: 3px; border-radius: 50%; }

/* ---------- line mask: type arrives from under its own baseline ---------- */

.lm { display: block; overflow: hidden; }
.lm > * {
  display: block;
  transform: translateY(108%);
  transition: transform 1.05s var(--ease);
  transition-delay: var(--d, 0ms);
}
.is-in .lm > *, .is-ready .lm > * { transform: none; }

/* ---------- word fill: the paragraph inks itself as you read it ---------- */

.wf w {
  display: inline;
  color: color-mix(in srgb, currentColor 22%, transparent);
  transition: color 0.42s var(--ease);
}
.wf w.on { color: inherit; }

/* ---------- reveal, upgraded ---------- */

.rv { opacity: 0; transform: translateY(26px); transition: opacity 0.9s var(--ease), transform 0.9s var(--ease); }
.rv.in { opacity: 1; transform: none; }

/* media arrives behind a wipe rather than a fade */
.piece__icon img, .portrait img { clip-path: inset(0 0 100% 0); transition: clip-path 1.1s var(--ease), transform 1.1s var(--ease); transform: scale(1.04); }
.in .piece__icon img, .piece__icon.in img, .portrait.in img { clip-path: inset(0 0 0 0); transform: none; }

/* ---------- nav retreats going down, returns coming up ---------- */

.nav { transition: transform 0.5s var(--ease), background 0.3s var(--ease); }
.nav.is-away { transform: translateY(-102%); }

/* ---------- magnetic controls, pointer devices only ---------- */

@media (hover: hover) and (pointer: fine) {
  .pill, .totop { will-change: transform; }
  .pill.is-mag { transition: transform 0.18s cubic-bezier(0.2, 0.7, 0.3, 1), background 0.3s var(--ease), color 0.3s var(--ease); }
}

/* ---------- portrait: tall on desktop so the whole head fits ---------- */

.portrait { padding: 0; background: none; }
.portrait img { aspect-ratio: 4 / 5; object-fit: cover; object-position: 50% 50%; border-radius: 0; }

@media (max-width: 900px) {
  /* On a phone the icons are decoration, not evidence — they get out of the way. */
  .piece__icon { max-width: 104px; }
  .piece__icon--quiet { max-width: 88px; }
  .portrait {
    max-width: 172px;
    padding: 12px;
    background: var(--bone-2);
  }
  .portrait img { aspect-ratio: 1 / 1; border-radius: 2px; }
  .totop { width: 22px; height: 22px; }
  .totop__arrow { width: 9px; height: 9px; }
}

@media (prefers-reduced-motion: reduce) {
  .lm > * { transform: none !important; transition: none !important; }
  .wf w { color: inherit !important; transition: none !important; }
  .piece__icon img, .portrait img { clip-path: none !important; transform: none !important; transition: none !important; }
  .nav.is-away { transform: none !important; }
  .totop { transition: none !important; }
  .totop__fill { transition: none !important; }
}

/* ============================================================
   CORRECTIONS
   ============================================================ */

/* A mask that clips descenders is worse than no mask at all: the y and the g
   in "what are you building?" were being cut by overflow:hidden. The padding
   gives the tail room and the negative margin gives the space back. */
.lm > * { padding-bottom: 0.16em; margin-bottom: -0.16em; }

/* Desktop portrait: tall, and large enough to hold the frame on its own.
   4/5 against a square source crops only the sides, never the top, so the
   whole head of hair survives. */
.about { grid-template-columns: minmax(0, 1.55fr) clamp(240px, 31vw, 440px); }
.portrait img { aspect-ratio: 4 / 5; object-fit: cover; object-position: 50% 50%; }

/* Media used to arrive with a snap. Longer, softer, and it starts earlier. */
.piece__icon img, .portrait img {
  opacity: 0;
  clip-path: inset(0 0 100% 0);
  transform: scale(1.03);
  transition: clip-path 1.35s var(--ease), transform 1.6s var(--ease), opacity 0.7s ease-out;
}
.piece__icon.in img, .portrait.in img { opacity: 1; clip-path: inset(0 0 0 0); transform: none; }

@media (max-width: 680px) {
  /* Alejandro was grazing the right edge at 15vw. */
  .name { font-size: clamp(2.1rem, 13.6vw, 5rem); }
}

@media (max-width: 900px) {
  .about { grid-template-columns: 1fr; }
  .portrait { max-width: 172px; }
  .portrait img { aspect-ratio: 1 / 1; }
}

@media (prefers-reduced-motion: reduce) {
  .piece__icon img, .portrait img { opacity: 1 !important; }
}

/* ============================================================
   CORRECTIONS — round 2
   ============================================================ */

/* Descenders again. 0.16em was not enough for Archivo 800 at display size:
   the tail of a g at 7rem is deeper than the em box suggests. */
.lm > * { padding-bottom: 0.24em; margin-bottom: -0.24em; }
.end__big { line-height: 1.0; }

/* The portrait is a vertical strip with the face held close, not a square.
   The frame is narrow and tall, `cover` trims the sides, and 0.7 is the
   widest crop that still reads as a strip while keeping the hair inside it. */
.about { grid-template-columns: minmax(0, 1.75fr) clamp(200px, 24vw, 360px); }
.portrait { padding: 0; background: none; max-width: none; }
.portrait img { aspect-ratio: 0.7; object-fit: cover; object-position: 50% 34%; border-radius: 0; }

@media (max-width: 900px) {
  .about { grid-template-columns: 1fr; }
  .portrait { max-width: 168px; padding: 12px; background: var(--bone-2); }
  .portrait img { aspect-ratio: 0.82; border-radius: 2px; }
}

/* ============================================================
   CORRECTIONS — round 3
   ============================================================ */

/* The real bug, found by measuring instead of looking: the negative margin
   I added to "preserve layout" removed from the masking box exactly the room
   the padding had just given the glyph. .lm bottom sat at 467px while the
   text needed 494px, so overflow:hidden kept cutting the y and the g.
   The padding stays. The compensation goes. */
.lm > * { padding-bottom: 0.22em; margin-bottom: 0; }
.end__big { line-height: 1.02; }
.sec__head { margin-bottom: clamp(26px, 5vh, 54px); }

/* The portrait is printed, not cropped: one-bit dither over the full head,
   dissolving into the paper at the foot of the strip. Nothing is cut because
   nothing needed to be — the canvas was extended instead of the face trimmed. */
.about { grid-template-columns: minmax(0, 1.75fr) clamp(210px, 25vw, 370px); }
.portrait { padding: 0; background: none; max-width: none; }
.portrait img {
  aspect-ratio: 700 / 1058;
  object-fit: contain;
  object-position: 50% 0;
  border-radius: 0;
  filter: none;
  width: 100%;
}

@media (max-width: 900px) {
  .about { grid-template-columns: 1fr; }
  .portrait { max-width: 210px; padding: 0; background: none; }
}

/* ---------- portrait, final: the real photograph, shown well ----------
   No dither, no mat, no invention. A tall frame with a modest horizontal
   crop: the source is square, so a taller box never touches the top of the
   head — all the hair stays, and only the edges of the frame give way. */
.about { grid-template-columns: minmax(0, 1.7fr) clamp(220px, 27vw, 400px); }
.portrait {
  padding: 0;
  background: none;
  max-width: none;
  border: 1px solid var(--rule);
  border-radius: 3px;
  overflow: hidden;
  filter: grayscale(1) contrast(1.04);
}
.portrait img {
  width: 100%;
  height: auto;
  aspect-ratio: 3 / 4;
  object-fit: cover;
  object-position: 50% 34%;
  border-radius: 0;
}

@media (max-width: 900px) {
  .portrait { max-width: 240px; }
  .portrait img { aspect-ratio: 4 / 5; }
}

/* ---------- portrait, final form ----------
   An arch: semicircular head, flat foot. It borrows the 999px radius the
   whole page already uses for its controls, so it reads as part of the
   system rather than a shape picked for effect. The image is composed, not
   cropped — the extra room at the top is what the arch curve eats, which is
   why the hair survives. Sticky, so it holds while the story is read. */
.about {
  grid-template-columns: minmax(0, 1.55fr) clamp(230px, 30vw, 430px);
  align-items: start;
}
.portrait {
  position: sticky;
  top: 92px;
  padding: 0;
  background: none;
  max-width: none;
  border: 1px solid var(--rule);
  border-radius: 999px 999px 8px 8px;
  overflow: hidden;
  filter: grayscale(1) contrast(1.03);
}
.portrait img {
  width: 100%;
  height: auto;
  aspect-ratio: 860 / 1600;
  object-fit: cover;
  object-position: 50% 50%;
  border-radius: 0;
}

@media (max-width: 900px) {
  .about { grid-template-columns: 1fr; }
  .portrait { position: static; max-width: 210px; }
}

/* ---------- portrait: sized to the viewport, then panned ----------
   Sticky only works if the thing being stuck actually fits. It didn't: an
   800px arch inside ~790px of usable height got guillotined at the chin by
   the status bar. So the frame is capped to what is visible, and instead of
   the image being cut, the framing travels: as the story is read the crop
   slides from the face down to the collar. */
.portrait {
  height: min(720px, calc(100svh - 168px));
}
.portrait img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% var(--py, 18%);
}

@media (max-width: 900px) {
  .portrait { height: auto; }
  .portrait img { height: auto; aspect-ratio: 860 / 1600; object-position: 50% 22%; }
}

/* ---------- portrait on a phone: a different idea entirely ----------
   A 720px arch is a desktop composition; on a 390px screen it becomes a
   column of face with nowhere to go. So the phone gets its own photograph
   and its own shape: a full-bleed band that runs edge to edge and acts as a
   rule between the story and the history, which is something a narrow
   screen does well and a wide one does not. */
@media (max-width: 900px) {
  .portrait {
    position: static;
    height: auto;
    max-width: none;
    width: calc(100% + var(--pad) * 2);
    margin: clamp(20px, 4vh, 34px) calc(var(--pad) * -1) 0;
    border-left: 0;
    border-right: 0;
    border-radius: 0;
  }
  .portrait picture { display: block; }
  .portrait img {
    width: 100%;
    height: auto;
    aspect-ratio: 4 / 3;
    object-fit: cover;
    object-position: 50% 20%;
  }
}

/* ============================================================
   EXPERIMENT — the portrait as a louvre
   The image is cut into vertical slats. Away from centre they shear apart
   and the face is unreadable; as the section reaches the middle of the
   screen they converge and it resolves. It rhymes with the aperture in the
   hero, which is also vertical bands, and it makes the one photograph on
   the site behave like the rest of the page: something that only assembles
   if you actually stop and look.
   Falls back to the plain arch with JS off.
   ============================================================ */

@media (min-width: 901px) {
  .portrait.has-slats > picture { opacity: 0; }
  .slats {
    position: absolute;
    inset: 0;
    display: flex;
    overflow: hidden;
  }
  .slat {
    flex: 1 1 0;
    background-image: url("../img/lorenzo-tall.webp");
    background-repeat: no-repeat;
    background-size: calc(var(--n, 12) * 100%) auto;
    background-position-x: var(--bx);
    background-position-y: var(--py, 22%);
    transform: translate3d(0, var(--ty, 0px), 0);
    will-change: transform;
  }
  /* a hairline between the blades, so the cut reads as intentional */
  .slat + .slat { box-shadow: -1px 0 0 rgba(239, 238, 233, 0.35); }
}

@media (max-width: 900px) {
  /* A band edge to edge was too much screen. The phone keeps the arch — the
     page's own shape — just small, and set beside the text rather than
     across it. */
  .portrait {
    position: static;
    height: auto;
    width: auto;
    max-width: 218px;
    margin: clamp(22px, 4vh, 34px) 0 0;
    border: 1px solid var(--rule);
    border-radius: 999px 999px 6px 6px;
    overflow: hidden;
  }
  .portrait img {
    width: 100%;
    height: auto;
    aspect-ratio: 3 / 4;
    object-fit: cover;
    object-position: 50% 18%;
  }
  .slats { display: none; }
}

/* ---------- louvre, corrected ----------
   It has to arrive somewhere. The first version converged at the centre of
   the screen and came apart again on the way out, so it never read as
   "finished" — only as a thing that wobbled. Now the travel is one-way:
   scattered on entry, assembled by the time you are reading, and it stays
   assembled. The blades keep no seam once closed, because a joint you can
   still see is not a joint. And no arch here: a louvre is rectangular. */
@media (min-width: 901px) {
  .portrait.has-slats { border-radius: 4px; }
  .slat + .slat { box-shadow: none; }
}

@media (max-width: 900px) {
  /* A plain rectangle, small, beside the text. */
  .portrait {
    max-width: 216px;
    border-radius: 4px;
  }
  .portrait img { aspect-ratio: 4 / 5; object-position: 50% 16%; }
}

/* ---------- the joint has to disappear ----------
   Twelve flex children land on fractional pixels, so even perfectly aligned
   they leave hairline seams. Rather than fight subpixel rounding, the blades
   hand over: at full convergence they fade out and the real photograph fades
   in underneath. What you end up looking at is the image itself, uncut. */
@media (min-width: 901px) {
  .portrait.has-slats > picture {
    opacity: var(--joined, 0);
    transition: opacity 0.28s linear;
  }
  .portrait.has-slats .slats {
    opacity: calc(1 - var(--joined, 0));
    transition: opacity 0.28s linear;
  }
}

/* ============================================================
   MOBILE SPACING — measured, not guessed
   On a 390×844 screen the hero was 844px tall holding 412px of content:
   249px of dead air above the overline and 183px below the buttons, because
   a full-height box centring a short block has to put the difference
   somewhere. Full-bleed height is a desktop luxury; here it just makes the
   reader scroll past nothing. The hero now takes the height it needs.
   ============================================================ */

@media (max-width: 680px) {
  .hero {
    min-height: auto;
    padding: 84px var(--pad) 44px;
  }
  .hero__over { margin-bottom: 18px; }
  .hero__say { margin-top: 24px; }
  .hero__acts { margin-top: 24px; }

  .sec { padding-top: 62px; padding-bottom: 62px; }
  .sec__head { margin-bottom: 26px; gap: 12px; }
  .end { padding-top: 74px; padding-bottom: 62px; }

  /* A 22px pill hung off the baseline of a 32px name reads as dropped.
     Centring it on the line box is what makes the pair look set rather
     than stacked. */
  .piece__name { align-items: center; gap: 12px; }
  .piece { padding: 26px 0; }
  .piece__what { margin-top: 12px; }
  .piece__acts { margin-top: 18px; }
  .gripe { padding: 26px 0; }
}

/* ============================================================
   ROUND 4 — measured corrections + the icon gets a life
   ============================================================ */

/* The descender padding on a masked heading is right for headings and wrong
   for this one: `.piece__name` is a flex row, so the extra room at the foot
   shifts the box centre 6px below the text centre and the tag centres on the
   box. Product names carry no descenders, so the padding buys nothing here
   and costs the alignment. */
.piece__name { padding-bottom: 0; }

@media (max-width: 680px) {
  /* Measured 63px above the section title against 52px below it. */
  .sec { padding-top: 52px; }
}

/* ---------- the app icon behaves like an object ----------
   It is rendered as a physical tile, so it should move like one: it drifts
   as the row crosses the screen, and under a real pointer it tilts toward
   the cursor as though catching the light. The transform lives on the
   figure and never on the image element, because the image already owns a
   clip-path reveal and two transforms on one element fight. */
.piece__icon {
  position: relative;
  transform: translateY(var(--fy, 0px));
  will-change: transform;
  transition: transform 0.6s var(--ease);
}

/* The specular band. Masked by the icon's own alpha, so the light lands on
   the tile and nowhere else — no rectangle, no halo on the transparent
   ground. `screen` keeps it a highlight rather than a wash. */
.piece__icon::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: linear-gradient(112deg,
    transparent 40%,
    rgba(255, 255, 255, 0.62) 50%,
    transparent 60%);
  background-size: 240% 100%;
  background-position: var(--sweep, 140%) 0;
  background-repeat: no-repeat;
  mix-blend-mode: screen;
  -webkit-mask-image: var(--icon);
  mask-image: var(--icon);
  -webkit-mask-size: contain;
  mask-size: contain;
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-position: center;
  mask-position: center;
}

@media (prefers-reduced-motion: reduce) {
  .piece__icon { transform: none !important; transition: none !important; }
  .piece__icon::after { display: none !important; }
}

/* The tag was still sitting 6px low after the padding fix, because
   `line-height: .94` makes the line box shorter than the glyphs it holds —
   so centring on the box is not centring on the letters. Matching the line
   box to the type is what actually lines the pill up with the name. */
.piece__name { line-height: 1.02; }

/* ---------- product name + status pill, both breakpoints ----------
   Centred, not baseline-aligned: a small pill hung off the baseline of a
   very large word reads as dropped at every size, and the name scales from
   32px to 70px so there is no single offset that would fix it. */
.piece__name { align-items: center; padding-bottom: 0; }
.piece__name .tag { position: relative; top: 0.04em; }
