/*
  landing.css
  Stylesheet for index.html. One dark ground from top to bottom:
  the arc hero, the About section, the projects shelf, the footer. No wave
  dividers -- sections are separated by spacing and a hairline.

  How the arc is built -- all of it in this file, none of it in JS:

    .arcStage      full-width box from the top of the hero down to the
                   "horizon" (--pivot-y), overflow hidden. Anything below
                   the horizon is simply clipped, which is how a whole
                   ring becomes an arc.
    .arcWheel      a 0x0 point sitting on the horizon at centre: the ring's
                   pivot. It carries the ring's rotation:
                     --rot = --drag - --turn * --step
                   where --step = 360deg / --n, --turn is the integer the
                   script increments, and --drag is only ever non-zero
                   mid-swipe.
    .arcTile       rotate(i * --step) translateY(-r): lift the tile up by
                   the radius, then swing it round to its slot, tilting
                   with the ring the way the old wheel's items did.
                   Its angle from the top is --a = i*--step + --rot, so
                   cos(--a) is 1 at the apex, 0 at the horizon and
                   negative below it. That one number drives the scale
                   (biggest at the top) and the fade near the horizon.

  Three tunings of the same geometry, chosen by width:
    >= 1024px   arc:  pivot at 92% of the hero, r ~36vw, 7 tiles across,
                      the copy sits inside the arc.
    641-1023px  fan:  pivot at 50%, bigger r relative to width, 5-7 tiles
                      overlapping like a hand of cards, copy below.
    <= 640px    fan:  pivot at 52%, 5 tiles with the middle one dominant.

  How the projects shelf is built (see the Projects section further down):
  each project is a box on a shelf, spine towards you. The box is a real
  3D cuboid of two faces -- the spine on the shelf plane, the front face
  folded back behind it -- rotating about the spine's centre line. Select
  one and it turns 90deg to face you, its glass cover swings open on a left
  hinge, and the photos inside eject to the right. Because the turned-out
  front sits half a spine's depth in front of the shelf plane, its spilled
  photos always paint over the neighbouring spines.
*/

:root {
  /* The site's brand gradients, carried over unchanged from the original
     style.css. */
  --base-grad: linear-gradient(
    25deg,
    #ee7752,
    #ff5e00,
    #e73c7e,
    #d90ead,
    #23a6d5,
    #70e0ec,
    #23d5ab,
    #00ff44
  );
  /* Project gradients, built to the same pattern as --base-grad: every stop
     fully opaque (the old ones had alpha, which halved their saturation
     over the dark ground), 7-8 saturated stops so the animated 400%
     background always shows a rich passage, no dark stops so text set in
     them stays legible, and a hue arc that suits the city.
     Istanbul: Bosphorus turquoise -> Grand Bazaar gold -> saffron -> tulip.
     Edinburgh: heather violet -> Firth blue -> mint -> gorse -> sandstone.
     The same values live in content/<city>.json for the gallery pages. */
  --istanbul-grad: linear-gradient(
    25deg,
    #00a8b5,
    #19d3d0,
    #ffd54a,
    #ff9d1f,
    #ff6a3d,
    #f0327a,
    #b5179e
  );
  --edinburgh-grad: linear-gradient(
    25deg,
    #7b3fe4,
    #2f6bff,
    #00b8ff,
    #22e3b6,
    #9dff3c,
    #ffe23a,
    #ff9a1f
  );

  /* Palette: a near-black with the site's violet in it, so the brand
     gradient glows against it rather than sitting on plain black. */
  --hero-bg: #0d0b16;
  --hero-fg: #f6f3fb;
  --hero-fg-2: rgba(246, 243, 251, 0.72);
  --hero-fg-3: rgba(246, 243, 251, 0.5);
  --hero-line: rgba(246, 243, 251, 0.16);

  /* One turn of the wheel. */
  --turn-dur: 0.9s;
  --turn-ease: cubic-bezier(0.22, 0.85, 0.25, 1);

  /* Hero geometry (desktop values). Declared here rather than on .arcHero
     because the About section's bloom on desktop is positioned off the
     hero's horizon and radius. --hero-h mirrors the hero's height rule so
     the radius cap can be written in terms of the hero's real height -- a
     percentage wouldn't resolve inside translateY(). The tablet/phone
     media blocks override both on .arcHero. */
  --hero-h: max(100vh, 640px);
  --hero-h: max(100svh, 640px);
  --r: min(36vw, 620px, calc(var(--hero-h) * 0.92 - 230px));
}

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

html {
  scroll-behavior: smooth;
  overflow-x: hidden;
}

body {
  position: relative;
  margin: 0;
  padding: 0;
  background: var(--hero-bg);
  color: var(--hero-fg);
  overflow-x: hidden;
}

body.lb-open {
  overflow: hidden;
}

@keyframes Gradient {
  0% {
    background-position: 0% 50%;
  }
  50% {
    background-position: 100% 50%;
  }
  100% {
    background-position: 0% 50%;
  }
}

/* ---------- Scroll reveal (replaces AOS) ---------- */

[data-reveal] {
  opacity: 0;
  transform: translateY(28px);
  transition: opacity 0.9s ease, transform 0.9s ease;
  transition-delay: var(--reveal-delay, 0s);
}

[data-reveal="right"] {
  transform: translateX(-48px);
}

[data-reveal].is-visible {
  opacity: 1;
  transform: none;
}

@media (prefers-reduced-motion: reduce) {
  [data-reveal] {
    transition: none;
    transform: none;
    opacity: 1;
  }
}

/* ======================================================================
   Hero
   ====================================================================== */

.arcHero {
  /* Geometry (desktop values; --hero-h and --r come from :root, the two
     media blocks further down retune everything here and nothing else). */
  --bar-h: 76px;
  --pivot-y: 92%;
  /* Tile width is the spacing knob: the arc allots each tile r * 24deg of
     arc (~0.42r), so whatever a tile doesn't use is gap. 10.4vw of a
     1440px screen is 150px on a 217px allotment -- a 67px gap. */
  --tile-w: clamp(96px, 10.4vw, 180px);
  /* Scale = --s-min + (1 - --s-min) * cos(angle)^--s-pow: the apex tile is
     always 1; a higher power makes the falloff sharper. */
  --s-min: 0.82;
  --s-pow: 2;
  /* Where the copy block is anchored. On desktop its centre sits inside
     the arc, halfway between the apex tile's bottom edge (pivot - r +
     tile height / 2, with tile height = 4/3 tile width) and the horizon;
     on the fan layouts its top sits just below the horizon. */
  --copy-y: calc(var(--pivot-y) - 0.5 * var(--r) + var(--tile-w) / 3);
  --copy-shift: -50%;

  position: relative;
  height: var(--hero-h);
  overflow: hidden;
  background: var(--hero-bg);
  color: var(--hero-fg);
  font-family: "Poppins", system-ui, sans-serif;
  /* Horizontal swipes are ours (they turn the wheel); vertical ones stay
     with the browser so the page still scrolls normally. */
  touch-action: pan-y;
  -webkit-user-select: none;
  user-select: none;
}

/* Top bar */

.arcBar {
  position: absolute;
  inset: 0 0 auto 0;
  height: var(--bar-h);
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0 clamp(16px, 4vw, 48px);
  z-index: 6;
}

.arcBrand {
  display: inline-flex;
  align-items: center;
  gap: 12px;
  color: var(--hero-fg);
  text-decoration: none;
}

/* The old wheel's gradient core, kept as the mark. */
.arcMark {
  width: 38px;
  height: 38px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-size: 13px;
  font-weight: 600;
  letter-spacing: 0.04em;
  color: #fff;
  background: var(--base-grad);
  background-size: 400% 400%;
  animation: Gradient 15s ease infinite;
  box-shadow: 0 8px 20px -8px rgba(217, 14, 173, 0.7);
}

/* Wordmark in the brand gradient. Its two words are inline-blocks so that
   on phones (below) it breaks cleanly onto two lines instead of colliding
   with the nav. */
.arcWordmark {
  display: inline-block;
  font-size: 13px;
  font-weight: 600;
  line-height: 1.15;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  background: var(--base-grad);
  background-size: 400% 400%;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: Gradient 15s ease infinite;
}

.arcWordmark > span {
  display: inline-block;
  white-space: nowrap;
}

.arcNav {
  display: flex;
  gap: clamp(18px, 2.4vw, 36px);
}

.arcNav a {
  position: relative;
  padding: 6px 0;
  color: var(--hero-fg-2);
  text-decoration: none;
  font-size: 13px;
  font-weight: 500;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  transition: color 0.25s ease;
}

.arcNav a::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 2px;
  border-radius: 2px;
  background: var(--base-grad);
  background-size: 400% 400%;
  animation: Gradient 15s ease infinite;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.35s var(--turn-ease);
}

.arcNav a:hover,
.arcNav a:focus-visible {
  color: var(--hero-fg);
}

.arcNav a:hover::after,
.arcNav a:focus-visible::after {
  transform: scaleX(1);
}

/* Glow under the horizon -- the sun the arc rises over. Sits outside
   .arcStage so it isn't clipped at the horizon and can bleed up under
   the copy. */
.arcGlow {
  position: absolute;
  left: 50%;
  top: var(--pivot-y);
  width: calc(var(--r) * 3.2);
  height: calc(var(--r) * 1.7);
  translate: -50% -50%;
  border-radius: 50%;
  background: radial-gradient(
    closest-side,
    rgba(217, 14, 173, 0.5),
    rgba(231, 60, 126, 0.26) 32%,
    rgba(35, 166, 213, 0.12) 58%,
    transparent 100%
  );
  pointer-events: none;
  z-index: 1;
  animation: arcGlow 12s ease-in-out infinite alternate;
}

@keyframes arcGlow {
  from {
    opacity: 0.75;
    scale: 1;
  }
  to {
    opacity: 1;
    scale: 1.06;
  }
}

/* The wheel */

.arcStage {
  position: absolute;
  left: 0;
  top: 0;
  width: 100%;
  height: var(--pivot-y);
  overflow: hidden;
  z-index: 2;
}

.arcHorizon {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  height: 1px;
  background: linear-gradient(
    90deg,
    transparent,
    var(--hero-line) 18%,
    var(--hero-line) 82%,
    transparent
  );
}

.arcWheel {
  position: absolute;
  left: 50%;
  top: 100%;
  width: 0;
  height: 0;
  margin: 0;
  padding: 0;
  list-style: none;
  --step: calc(360deg / var(--n, 15));
  --drag: 0deg;
  --rot: calc(var(--drag) - var(--turn, 0) * var(--step));
  transform: rotate(var(--rot));
  transition: transform var(--turn-dur) var(--turn-ease);
  /* Entrance: the whole ring swings in from ~55deg back. fill-mode
     backwards only, so once it lands the plain transform rule takes over
     and turning the wheel transitions normally. */
  animation: arcSpinIn 1.6s cubic-bezier(0.16, 0.84, 0.3, 1) backwards;
}

@keyframes arcSpinIn {
  from {
    transform: rotate(calc(var(--rot) - 55deg));
  }
}

.arcTile {
  position: absolute;
  left: 0;
  top: 0;
  width: var(--tile-w);
  aspect-ratio: 3 / 4;
  /* Centre the tile's box on the pivot before the transform swings it out. */
  translate: -50% -50%;
  --a: calc(var(--i, 0) * var(--step) + var(--rot));
  --k: max(0, cos(var(--a)));
  transform: rotate(calc(var(--i, 0) * var(--step))) translateY(calc(-1 * var(--r)));
  transition: transform var(--turn-dur) var(--turn-ease), opacity var(--turn-dur) linear;
  will-change: transform;
  /* Staggered fade-in from the apex outwards (min() of the two ways round
     the ring is the tile's distance from the top at load). */
  animation: arcFadeIn 0.8s ease backwards;
  animation-delay: calc(0.12s + 0.05s * min(var(--i, 0), var(--n, 15) - var(--i, 0)));
}

@keyframes arcFadeIn {
  from {
    opacity: 0;
  }
}

/* Scale-by-angle and the fade near the horizon need CSS trig. Every
   current browser has it (Baseline 2023/24); anything older gets the
   plain version above -- uniform tiles, still clipped at the horizon.
   Kept behind @supports rather than as a second declaration because a
   var()-bearing value that fails only at computed-value time would wipe
   the property instead of falling back. */
@supports (transform: scale(pow(cos(0deg), 2))) {
  .arcTile {
    transform: rotate(calc(var(--i, 0) * var(--step))) translateY(calc(-1 * var(--r)))
      scale(calc(var(--s-min) + (1 - var(--s-min)) * pow(var(--k), var(--s-pow))));
    opacity: clamp(0, calc(var(--k) * 4), 1);
  }
}

.arcHero.is-dragging .arcWheel,
.arcHero.is-dragging .arcTile {
  transition: none;
}

.arcTileBtn {
  display: block;
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  border: 0;
  background: #1a1526;
  border-radius: calc(var(--tile-w) * 0.13);
  overflow: hidden;
  cursor: pointer;
  box-shadow: 0 24px 48px -18px rgba(0, 0, 0, 0.7);
  transition: translate 0.35s ease, box-shadow 0.35s ease;
  -webkit-tap-highlight-color: transparent;
}

/* Lift is along the tile's own up-axis, so it moves outward from the
   ring's centre -- reads as the card coming towards you. */
.arcTileBtn:hover {
  translate: 0 -6px;
  box-shadow: 0 32px 56px -16px rgba(0, 0, 0, 0.75), 0 0 0 2px rgba(255, 255, 255, 0.4);
}

.arcTileBtn:focus-visible {
  outline: 3px solid #fff;
  outline-offset: 4px;
}

.arcTileBtn img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  pointer-events: none;
  -webkit-user-drag: none;
  user-select: none;
}

/* Copy */

.arcCopy {
  position: absolute;
  left: 50%;
  top: var(--copy-y);
  translate: -50% var(--copy-shift);
  width: min(560px, calc(100% - 48px));
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  text-align: center;
  z-index: 3;
  /* Let drags that start over the text reach the hero; only the links
     take the pointer. */
  pointer-events: none;
}

.arcCopy a {
  pointer-events: auto;
}

/* Small gradient label, used as the eyebrow of every section. */
.arcEyebrow {
  font-family: "Poppins", system-ui, sans-serif;
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  background: var(--base-grad);
  background-size: 400% 400%;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: Gradient 15s ease infinite;
}

.arcTitle {
  font-size: clamp(38px, 5.4vw, 72px);
  font-weight: 600;
  line-height: 1.04;
  letter-spacing: -0.02em;
  text-wrap: balance;
}

.arcLede {
  max-width: 36ch;
  font-size: clamp(15px, 1.15vw, 18px);
  line-height: 1.55;
  color: var(--hero-fg-2);
  text-wrap: pretty;
}

.arcActions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  align-items: center;
  gap: 10px 22px;
  margin-top: 8px;
}

/* White pill; on hover a brand-gradient ring appears behind it. The ring
   and the fill are both pseudo-elements at negative z-index inside the
   button's own stacking context, so the text stays on top of both. */
.arcBtn {
  position: relative;
  isolation: isolate;
  display: inline-flex;
  align-items: center;
  padding: 0.9em 1.7em;
  border-radius: 999px;
  color: var(--hero-bg);
  font-family: "Poppins", system-ui, sans-serif;
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 0.02em;
  text-decoration: none;
  box-shadow: 0 12px 30px -12px rgba(0, 0, 0, 0.6);
  transition: transform 0.3s ease, color 0.3s ease;
}

.arcBtn::before,
.arcBtn::after {
  content: "";
  position: absolute;
  border-radius: inherit;
}

.arcBtn::before {
  inset: -3px;
  z-index: -2;
  background: var(--base-grad);
  background-size: 400% 400%;
  animation: Gradient 15s ease infinite;
  opacity: 0;
  transition: opacity 0.3s ease, inset 0.3s ease;
}

.arcBtn::after {
  inset: 0;
  z-index: -1;
  background: #fff;
  transition: opacity 0.3s ease;
}

/* Hover: the gradient ring appears and the label takes the same gradient.
   The label is a <span> (inline content paints above the negative-z fill
   and ring pseudos; a background clipped to the button's own text would
   sit underneath the white fill). */
.arcBtn > span {
  transition: color 0.3s ease;
}

.arcBtn:hover,
.arcBtn:focus-visible {
  transform: translateY(-2px);
}

.arcBtn:hover > span,
.arcBtn:focus-visible > span {
  background: var(--base-grad);
  background-size: 400% 400%;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: Gradient 15s ease infinite;
}

/* Glass variant (the hero's "See the projects"): the frosted dark pill of
   the bar, hairline border, white label. On hover the border becomes a 1px
   run of the wordmark's gradient -- a frosted layer clipped to the padding
   box over the gradient clipped to the border box -- and the label takes
   the same gradient via the rule above. No ring, no white fill. */
.arcBtn--glass {
  color: var(--hero-fg);
  border: 1px solid var(--hero-line);
  background: rgba(13, 11, 22, 0.55);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  box-shadow: 0 12px 30px -16px rgba(0, 0, 0, 0.7);
  transition: transform 0.3s ease, border-color 0.3s ease;
}

.arcBtn--glass::before,
.arcBtn--glass::after {
  display: none;
}

.arcBtn--glass:hover,
.arcBtn--glass:focus-visible {
  border-color: transparent;
  background:
    linear-gradient(rgba(13, 11, 22, 0.55), rgba(13, 11, 22, 0.55)) padding-box,
    var(--base-grad) border-box;
  background-size: 100% 100%, 400% 400%;
  animation: Gradient 15s ease infinite;
}

.arcBtn:hover::before,
.arcBtn:focus-visible::before {
  opacity: 1;
}

.arcBtn:focus-visible {
  outline: 3px solid #fff;
  outline-offset: 5px;
}

.arcLink {
  color: var(--hero-fg-2);
  font-family: "Poppins", system-ui, sans-serif;
  font-size: 14px;
  font-weight: 500;
  text-decoration: none;
  transition: color 0.25s ease;
}

.arcLink:hover,
.arcLink:focus-visible {
  background: var(--base-grad);
  background-size: 400% 400%;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: Gradient 15s ease infinite;
}

.arcHint {
  margin-top: 6px;
  font-size: 12px;
  color: var(--hero-fg-3);
}

/* Round arrow buttons -- positioned in the hero, in-flow on the shelf. */

.arcArrow {
  position: absolute;
  top: var(--copy-y);
  translate: 0 -50%;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  border: 1px solid var(--hero-line);
  background: rgba(255, 255, 255, 0.06);
  color: var(--hero-fg);
  display: grid;
  place-items: center;
  cursor: pointer;
  z-index: 4;
  -webkit-backdrop-filter: blur(6px);
  backdrop-filter: blur(6px);
  transition: background 0.25s ease, border-color 0.25s ease, transform 0.25s ease;
  -webkit-tap-highlight-color: transparent;
}

.arcArrow--prev {
  left: clamp(16px, 4vw, 56px);
}

.arcArrow--next {
  right: clamp(16px, 4vw, 56px);
}

.arcArrow--inline {
  position: static;
  translate: none;
}

/* Phones: the bar follows the page. Fixed positioning escapes the hero's
   overflow clipping (no ancestor has a transform), and the bar gets a
   frosted ground since it now sits over content further down. */
@media (max-width: 640px) {
  .arcBar {
    position: fixed;
    background: rgba(13, 11, 22, 0.62);
    -webkit-backdrop-filter: blur(14px);
    backdrop-filter: blur(14px);
    border-bottom: 1px solid var(--hero-line);
  }
}

.arcArrow:hover {
  background: rgba(255, 255, 255, 0.14);
  border-color: rgba(255, 255, 255, 0.4);
}

.arcArrow:active {
  transform: scale(0.94);
}

.arcArrow:focus-visible {
  outline: 3px solid #fff;
  outline-offset: 3px;
}

/* Scroll cue */

.arcScrollCue {
  position: absolute;
  left: 50%;
  bottom: 18px;
  translate: -50% 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  color: var(--hero-fg-3);
  text-decoration: none;
  font-size: 11px;
  letter-spacing: 0.2em;
  text-transform: uppercase;
  z-index: 4;
}

.arcScrollCueLine {
  width: 1px;
  height: 34px;
  background: linear-gradient(180deg, transparent, var(--hero-fg-2));
  transform-origin: top;
  animation: arcCue 2.2s ease-in-out infinite;
}

@keyframes arcCue {
  0%,
  100% {
    transform: scaleY(0.4);
    opacity: 0.4;
  }
  50% {
    transform: scaleY(1);
    opacity: 1;
  }
}

.arcScrollCue:hover {
  color: var(--hero-fg);
}

/* Short desktop windows (a 720p laptop, an iPad on its side): the space
   between the apex tile and the horizon is only ~270px, so the copy
   tightens up and drops its hint line, and the cue shrinks to fit under
   the horizon. */
@media (min-width: 1024px) and (max-height: 800px) {
  .arcCopy {
    gap: 11px;
  }

  .arcHint {
    display: none;
  }

  .arcScrollCue {
    bottom: 12px;
    gap: 6px;
  }

  .arcScrollCueLine {
    height: 22px;
  }
}

@media (max-height: 560px) {
  .arcScrollCue {
    display: none;
  }
}

/* ---------- Tablet & phone: the fan ---------- */

@media (max-width: 1023px) {
  .arcHero {
    --bar-h: 64px;
    --pivot-y: 50%;
    --r: max(120px, min(clamp(190px, 44vw, 340px), calc(var(--hero-h) * 0.5 - 210px)));
    --tile-w: clamp(112px, 19.5vw, 160px);
    --s-min: 0.7;
    --s-pow: 4;
    --copy-y: calc(var(--pivot-y) + 22px);
    --copy-shift: 0%;
  }

  .arcHero .arcArrow {
    top: calc(var(--pivot-y) + 24px);
    translate: 0 0;
    width: 44px;
    height: 44px;
  }
}

@media (max-width: 640px) {
  .arcHero {
    /* Capped below the full screen height on tall phones so the next
       section (and the fact that there's a page under this) shows without
       scrolling. */
    --hero-h: clamp(600px, 100vh, 780px);
    --hero-h: clamp(600px, 100svh, 780px);
    --pivot-y: 50%;
    /* Five cards across: each 24deg step must move a tile ~0.8 of its own
       width sideways, so at ~210px radius (85px per step) the tiles are
       ~105px wide -- the middle one the biggest, the outer two cut by the
       screen edge, like the reference. */
    --r: max(120px, min(clamp(170px, 54vw, 230px), calc(var(--hero-h) * 0.5 - 180px)));
    --tile-w: clamp(88px, 25vw, 112px);
    --copy-y: calc(var(--pivot-y) - 56px);
  }

  /* "Ben Jenkins" over "Photography": a grid, not a width cap, so the box
     always covers both words (text outside a background-clip: text box
     has no gradient behind it and disappears). */
  .arcWordmark {
    display: inline-grid;
    gap: 1px;
    font-size: 11px;
    letter-spacing: 0.1em;
  }

  .arcNav {
    gap: 14px;
  }

  .arcNav a {
    font-size: 12px;
    letter-spacing: 0.1em;
  }

  .arcCopy {
    gap: 10px;
  }

  .arcTitle {
    font-size: clamp(34px, 10vw, 44px);
  }

  /* Bottom corners, either side of the scroll cue. */
  .arcHero .arcArrow {
    top: auto;
    bottom: 14px;
    width: 42px;
    height: 42px;
  }

  .arcHero .arcArrow--prev {
    left: 16px;
  }

  .arcHero .arcArrow--next {
    right: 16px;
  }
}

/* ======================================================================
   Enlarged view
   ====================================================================== */

.arcLightbox {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: grid;
  grid-template-columns: 56px 1fr 56px;
  align-items: center;
  justify-items: center;
  gap: 8px;
  padding: 0 clamp(8px, 2vw, 28px);
  background: rgba(9, 7, 16, 0.8);
  -webkit-backdrop-filter: blur(16px);
  backdrop-filter: blur(16px);
  color: var(--hero-fg);
  font-family: "Poppins", system-ui, sans-serif;
  animation: lbFade 0.35s ease backwards;
}

/* Author styles beat the UA's [hidden]{display:none}, so restate it. */
.arcLightbox[hidden] {
  display: none;
}

@keyframes lbFade {
  from {
    opacity: 0;
  }
}

.lbFigure {
  grid-column: 2;
  margin: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  max-width: 100%;
}

/* The gradient backdrop that framed the enlarged photo in the old wheel
   (.photoBg), now a padded frame around the image. It fades in a beat
   after the photo lands, as before. */
.lbFrame {
  position: relative;
  padding: clamp(5px, 0.8vh, 9px);
  border-radius: 14px;
}

.lbFrame::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  background: var(--base-grad);
  background-size: 400% 400%;
  animation: Gradient 15s ease infinite, lbFrameIn 0.7s ease 0.25s backwards;
}

@keyframes lbFrameIn {
  from {
    opacity: 0;
  }
}

/* Sized by height so the box doesn't change when the thumbnail is
   swapped for the 1600px file; --lb-ar (width / height) is set inline
   by the script from the thumbnail's natural size. */
.lbImg {
  --lb-ar: 0.667;
  position: relative;
  display: block;
  height: min(80svh, calc((100vw - 190px) / var(--lb-ar)));
  width: auto;
  aspect-ratio: var(--lb-ar);
  object-fit: cover;
  border-radius: 9px;
  background: #1a1526;
  cursor: zoom-out;
}

.lbCaption {
  display: flex;
  align-items: baseline;
  gap: 18px;
  font-size: 13px;
  color: var(--hero-fg-2);
}

.lbCount {
  color: var(--hero-fg-3);
  font-variant-numeric: tabular-nums;
}

.lbClose,
.lbNav {
  width: 48px;
  height: 48px;
  border-radius: 50%;
  border: 1px solid var(--hero-line);
  background: rgba(255, 255, 255, 0.07);
  color: var(--hero-fg);
  display: grid;
  place-items: center;
  cursor: pointer;
  transition: background 0.25s ease, border-color 0.25s ease;
  -webkit-tap-highlight-color: transparent;
}

.lbClose {
  position: fixed;
  top: 16px;
  right: 16px;
}

.lbNav--prev {
  grid-column: 1;
}

.lbNav--next {
  grid-column: 3;
}

.lbClose:hover,
.lbNav:hover {
  background: rgba(255, 255, 255, 0.16);
  border-color: rgba(255, 255, 255, 0.4);
}

.lbClose:focus-visible,
.lbNav:focus-visible {
  outline: 3px solid #fff;
  outline-offset: 3px;
}

@media (max-width: 640px) {
  .arcLightbox {
    grid-template-columns: 1fr;
    padding: 0 16px;
  }

  .lbFigure {
    grid-column: 1;
  }

  .lbImg {
    height: min(72svh, calc((100vw - 46px) / var(--lb-ar)));
  }

  .lbNav {
    position: fixed;
    bottom: 18px;
  }

  .lbNav--prev {
    left: 18px;
  }

  .lbNav--next {
    right: 18px;
  }
}

/* The open/close morph. Both the tile's <img> and .lbImg are given
   view-transition-name: arc-photo by the script across the DOM change;
   these rules only set its pace and stop the two snapshots (3:4 tile,
   2:3 photo) from stretching while they cross-fade. */
::view-transition-group(arc-photo) {
  animation-duration: 0.6s;
  animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
}

::view-transition-old(arc-photo),
::view-transition-new(arc-photo) {
  animation-duration: 0.6s;
  height: 100%;
  width: 100%;
  object-fit: cover;
}

::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 0.5s;
}

/* ======================================================================
   About
   ====================================================================== */

.aboutArc,
.shelfSection {
  background: var(--hero-bg);
  color: var(--hero-fg);
  font-family: "Poppins", system-ui, sans-serif;
  border-top: 1px solid var(--hero-line);
}

.aboutArc {
  position: relative;
  isolation: isolate;
  overflow: hidden;
  padding: clamp(72px, 10vh, 140px) clamp(20px, 5vw, 64px);
}

/* Tablet & phone: a cooler cousin of the hero's glow, behind the portrait.
   (Desktop replaces this -- see below.) */
.aboutArc::before {
  content: "";
  position: absolute;
  z-index: -1;
  left: 24%;
  top: 50%;
  width: min(640px, 80vw);
  aspect-ratio: 1;
  translate: -50% -50%;
  border-radius: 50%;
  background: radial-gradient(
    closest-side,
    rgba(35, 166, 213, 0.26),
    rgba(217, 14, 173, 0.14) 45%,
    transparent 100%
  );
  pointer-events: none;
}

/* Desktop: the hero's bloom no longer stops at the section edge. This one
   element replaces .arcGlow: its first layer is the hero glow exactly as
   before (same size, same colours, centred on the horizon), and a second,
   cooler lobe carries the colour on down behind the About content. It
   hangs off the top of this section, reaching up to where the hero glow
   used to start; the hero's own stage, copy and controls all have a
   z-index and stay above it. */
@media (min-width: 1024px) {
  .arcGlow {
    display: none;
  }

  .aboutArc {
    /* Open vertically so the bloom can reach up into the hero, but clipped
       horizontally: at 3.2r it's wider than the page and would otherwise
       widen the document on mobile browsers. */
    overflow-x: clip;
    overflow-y: visible;
    border-top-color: transparent;
  }

  .aboutArc::before {
    /* The hero's horizon (92% of its height) in this section's coordinates,
       and half the hero glow's height. */
    --horizon: calc(-0.08 * var(--hero-h));
    --up: calc(0.85 * var(--r));
    left: 50%;
    top: calc(var(--horizon) - var(--up));
    width: calc(var(--r) * 3.2);
    height: calc(var(--up) + 940px);
    aspect-ratio: auto;
    translate: -50% 0;
    border-radius: 0;
    background:
      radial-gradient(
          closest-side,
          rgba(217, 14, 173, 0.5),
          rgba(231, 60, 126, 0.26) 32%,
          rgba(35, 166, 213, 0.12) 58%,
          transparent 100%
        )
        50% 0 / 100% calc(var(--r) * 1.7) no-repeat,
      radial-gradient(
          closest-side,
          rgba(35, 166, 213, 0.22),
          rgba(217, 14, 173, 0.1) 48%,
          transparent 100%
        )
        50% calc(var(--up) - 260px) / 80% 1200px no-repeat;
    animation: arcGlow 12s ease-in-out infinite alternate;
  }
}

.aboutInner {
  position: relative;
  max-width: 1100px;
  margin: 0 auto;
  display: grid;
  grid-template-columns: minmax(240px, 400px) 1fr;
  gap: clamp(32px, 6vw, 96px);
  align-items: center;
}

/* Same proportions and corner as an arc tile, tilted a touch. `rotate` is
   the individual property, so it composes with the data-reveal
   transform instead of fighting it. */
.aboutPortrait {
  position: relative;
  margin: 0;
  width: 100%;
  aspect-ratio: 4 / 5;
  border-radius: 28px;
  overflow: hidden;
  rotate: -3deg;
  border: 1px solid rgba(255, 255, 255, 0.14);
  box-shadow: 0 40px 80px -30px rgba(0, 0, 0, 0.85);
  background: #1a1526;
}

.aboutPortrait img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.aboutBadge {
  position: absolute;
  left: 14px;
  bottom: 14px;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 6px 14px 6px 6px;
  border-radius: 999px;
  background: rgba(13, 11, 22, 0.6);
  -webkit-backdrop-filter: blur(12px);
  backdrop-filter: blur(12px);
  border: 1px solid rgba(255, 255, 255, 0.22);
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.04em;
}

.aboutBadge .arcMark {
  width: 28px;
  height: 28px;
  font-size: 10px;
  box-shadow: none;
}

.aboutText {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 18px;
}

.aboutTitle {
  font-size: clamp(32px, 4.2vw, 56px);
  font-weight: 600;
  line-height: 1.05;
  letter-spacing: -0.02em;
  text-wrap: balance;
}

.aboutLead {
  max-width: 34ch;
  font-size: clamp(17px, 1.4vw, 21px);
  line-height: 1.5;
}

.aboutBody {
  max-width: 44ch;
  font-size: clamp(15px, 1.1vw, 17px);
  line-height: 1.6;
  color: var(--hero-fg-2);
}

/* Contact chips (About section and the projects foot), in the same glass
   as the hero button: frosted dark fill, hairline border, white text. On
   hover the border and the text take the brand gradient -- the border via
   the double-background trick, a frosted layer clipped to the padding box
   over the gradient clipped to the border box. */
.contactChips {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
}

.contactChips a {
  display: inline-flex;
  flex-direction: column;
  gap: 3px;
  padding: 12px 18px;
  border-radius: 14px;
  border: 1px solid var(--hero-line);
  background: rgba(13, 11, 22, 0.55);
  -webkit-backdrop-filter: blur(14px);
  backdrop-filter: blur(14px);
  color: var(--hero-fg);
  text-decoration: none;
  box-shadow: 0 12px 30px -16px rgba(0, 0, 0, 0.7);
  /* Same feel as the hero's glass button: a 0.3s lift, and the text
     fading to the gradient (below) rather than snapping. */
  transition: transform 0.3s ease, border-color 0.3s ease;
}

.contactChips a:hover,
.contactChips a:focus-visible {
  transform: translateY(-2px);
  border-color: transparent;
  background:
    linear-gradient(rgba(13, 11, 22, 0.55), rgba(13, 11, 22, 0.55)) padding-box,
    var(--base-grad) border-box;
  background-size: 100% 100%, 400% 400%;
  animation: Gradient 15s ease infinite;
}

.contactChips small {
  font-size: 10px;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--hero-fg-3);
  transition: color 0.3s ease;
}

.contactChips span {
  font-size: 15px;
  font-weight: 500;
  transition: color 0.3s ease;
}

.contactChips a:hover small,
.contactChips a:hover span,
.contactChips a:focus-visible small,
.contactChips a:focus-visible span {
  background: var(--base-grad);
  background-size: 400% 400%;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: Gradient 15s ease infinite;
}

.aboutText .arcActions {
  justify-content: flex-start;
  margin-top: 4px;
}

@media (max-width: 860px) {
  .aboutInner {
    grid-template-columns: 1fr;
  }

  .aboutArc::before {
    left: 50%;
    top: 22%;
  }

  .aboutPortrait {
    max-width: 300px;
    margin: 0 auto;
  }

  .aboutText {
    align-items: center;
    text-align: center;
  }

  .aboutContact,
  .aboutText .arcActions {
    justify-content: center;
  }
}

/* ======================================================================
   Projects: the shelf
   ====================================================================== */

.shelfSection {
  /* Box dimensions, plain px on purpose: the script reads them back to
     centre the active box. Retuned at the phone breakpoint below. */
  --spine-w: 64px;
  --front-w: 260px;
  --box-h: 360px;
  --shelf-gap: 20px;
  /* Clear space either side of the open box, so the cover swinging left
     and the photos ejecting right land in air, not on the neighbours.
     Asymmetric: the cover only travels ~40px, the photos ~150px. */
  --slot-pad-l: 44px;
  --slot-pad-r: 132px;
  --shelf-dur: 0.85s;

  position: relative;
  isolation: isolate;
  overflow: hidden;
  padding: clamp(64px, 9vh, 120px) clamp(16px, 4vw, 48px) clamp(56px, 8vh, 100px);
}

/* Ambient light behind the shelf: broad, dim and cool, so the warm pool
   under the open box (.shelfLedge::after) reads against it. Deliberately
   a different temperature from the hero's magenta. */
.shelfSection::before {
  content: "";
  position: absolute;
  z-index: -1;
  left: 50%;
  top: 42%;
  width: min(1500px, 130vw);
  height: 780px;
  translate: -50% -50%;
  border-radius: 50%;
  background: radial-gradient(
    closest-side,
    rgba(35, 166, 213, 0.2),
    rgba(112, 224, 236, 0.08) 42%,
    rgba(217, 14, 173, 0.05) 70%,
    transparent 100%
  );
  pointer-events: none;
}

.shelfHead {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
  text-align: center;
  margin-bottom: clamp(24px, 4vh, 48px);
}

.shelfTitle {
  font-size: clamp(28px, 3.6vw, 48px);
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: -0.02em;
  text-wrap: balance;
}

.shelfFilter {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 4px;
  padding: 5px;
  margin-top: 10px;
  border: 1px solid var(--hero-line);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.04);
}

.shelfChip {
  padding: 9px 16px;
  border: 0;
  border-radius: 999px;
  background: transparent;
  color: var(--hero-fg-2);
  font-family: "Poppins", system-ui, sans-serif;
  font-size: 12px;
  font-weight: 500;
  line-height: 1;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  cursor: pointer;
  transition: background 0.25s ease, color 0.25s ease;
  -webkit-tap-highlight-color: transparent;
}

.shelfChip:hover {
  color: var(--hero-fg);
}

.shelfChip.is-active {
  background: #fff;
  color: var(--hero-bg);
}

.shelfChip:focus-visible {
  outline: 2px solid #fff;
  outline-offset: 2px;
}

/* The stage supplies the perspective; everything from here down to the
   faces keeps transform-style: preserve-3d so the boxes share one 3D
   space and sort by depth against each other. */
.shelfStage {
  position: relative;
  height: calc(var(--box-h) + 72px);
  perspective: 1400px;
  perspective-origin: 50% 40%;
  touch-action: pan-y;
  -webkit-user-select: none;
  user-select: none;
}

/* The shelf the boxes stand on, with a pool of the brand glow under the
   active one. */
.shelfLedge {
  position: absolute;
  left: 8%;
  right: 8%;
  bottom: 30px;
  height: 1px;
  background: linear-gradient(
    90deg,
    transparent,
    var(--hero-line) 20%,
    var(--hero-line) 80%,
    transparent
  );
  pointer-events: none;
}

.shelfLedge::after {
  content: "";
  position: absolute;
  left: 22%;
  right: 22%;
  top: 1px;
  height: 90px;
  background: radial-gradient(ellipse at 50% 0%, rgba(217, 14, 173, 0.32), transparent 70%);
}

/* The row. Its left edge sits on the stage's centre line; the script
   shifts it left by --shelf-x, the distance from the row's left edge to
   the active box's centre. */
.shelf {
  position: absolute;
  left: 50%;
  top: 36px;
  width: 0;
  height: var(--box-h);
  margin: 0;
  padding: 0;
  list-style: none;
  transform: translateX(calc(-1 * var(--shelf-x, 0px)));
  transition: transform var(--shelf-dur) var(--turn-ease);
  transform-style: preserve-3d;
}

/* Each item is placed along the row by the script with translateX(--x):
   a spine occupies one spine width, the active box padding + front +
   padding. Transforms only -- no flex-basis or left animating -- so the
   slide runs on the compositor and stays smooth on phones. */
.shelfItem {
  position: absolute;
  left: 0;
  top: 0;
  width: var(--spine-w);
  height: 100%;
  transform: translateX(var(--x, 0px));
  transform-style: preserve-3d;
  transition: transform var(--shelf-dur) var(--turn-ease);
  will-change: transform;
}

.shelfItem[hidden] {
  display: none;
}

/* Set by the script for the very first layout so the boxes appear in
   place instead of sliding in from the row's left edge. */
.shelf.no-anim,
.shelf.no-anim .shelfItem,
.shelf.no-anim .shelfBox {
  transition: none;
}

/* The box's rotation axis: a zero-width line through the centre of the
   spine, half a spine in from the slot's right edge -- which is the slot's
   middle while the slot is a spine wide. When the slot grows to the front
   face's width plus padding, the axis slides to the right-hand padding's
   inner edge, so the turned-out front (which hangs off the axis to the
   left) lands with its padding either side.

   Rest:   rotateY(0)    -> spine faces you, front folded back behind it
   Active: rotateY(90)   -> front faces you, half a spine in front of the
                            shelf plane; spine edge-on */
.shelfBox {
  position: absolute;
  top: 0;
  left: 0;
  width: 0;
  height: 100%;
  display: block;
  color: var(--hero-fg);
  text-decoration: none;
  transform-style: preserve-3d;
  /* Axis in the middle of the spine's slot. */
  transform: translateX(calc(var(--spine-w) / 2)) rotateY(0deg);
  transition: transform var(--shelf-dur) var(--turn-ease);
  will-change: transform;
  cursor: pointer;
  -webkit-tap-highlight-color: transparent;
}

/* Axis slides to the right edge of the front (padding + front in from
   the slot's left) as the box turns, so the turned-out front lands with
   its padding either side. */
.shelfItem.is-active .shelfBox {
  transform: translateX(calc(var(--slot-pad-l) + var(--front-w))) rotateY(90deg);
}

.shelfBox:focus-visible {
  outline: none;
}

.shelfBox:focus-visible .boxSpine,
.shelfBox:focus-visible .boxCover {
  outline: 3px solid #fff;
  outline-offset: 3px;
}

/* Spine: on the shelf plane, centred on the axis. */
.boxSpine {
  position: absolute;
  top: 0;
  left: calc(var(--spine-w) / -2);
  width: var(--spine-w);
  height: 100%;
  backface-visibility: hidden;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: space-between;
  padding: 16px 0;
  border-radius: 7px;
  border: 1px solid rgba(255, 255, 255, 0.3);
  background-size: 400% 400%;
  animation: Gradient 15s ease infinite;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.12), 0 24px 40px -20px rgba(0, 0, 0, 0.85);
  transition: border-radius var(--shelf-dur) var(--turn-ease);
}

.boxSpine.istan,
.boxCase.istan {
  background-image: var(--istanbul-grad);
}

.boxSpine.edin,
.boxCase.edin {
  background-image: var(--edinburgh-grad);
  background-size: 600% 600%;
}

.boxSpine.soon,
.boxCase.soon {
  background-image: var(--base-grad);
  filter: saturate(0.55) brightness(0.9);
}

.spineYear {
  font-size: 10px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.85);
}

.spineTitle {
  writing-mode: vertical-rl;
  font-family: "Baskerville", "Georgia", serif;
  font-style: italic;
  font-weight: 300;
  font-size: clamp(15px, 1.3vw, 19px);
  letter-spacing: 3px;
  text-transform: uppercase;
  white-space: nowrap;
  color: #fff;
}

.spineMark {
  width: 24px;
  height: 24px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  font-size: 8px;
  font-weight: 600;
  color: #fff;
  background: rgba(13, 11, 22, 0.45);
  border: 1px solid rgba(255, 255, 255, 0.35);
}

/* Front face: folded 90deg back from the spine's left edge, so the closed
   box extends away from you into the shelf. Transform functions apply
   right-to-left: the face is first turned edge-on about its own centre,
   then moved so it hangs off the spine's left edge and reaches back a
   full front-width. */
.boxFront {
  position: absolute;
  top: 0;
  left: calc(var(--front-w) / -2);
  width: var(--front-w);
  height: 100%;
  transform: translate3d(calc(var(--spine-w) / -2), 0, calc(var(--front-w) / -2)) rotateY(-90deg);
  backface-visibility: hidden;
  transform-style: preserve-3d;
}

/* Inside the front: the gradient case (with the blurb), the photos, and
   the glass cover on top -- the same three layers as the old cards. */
.boxCase {
  position: absolute;
  inset: 0;
  padding: 18px;
  /* The blurb is texture behind the cover; on a short box it would run
     out of the bottom otherwise. */
  overflow: hidden;
  border-radius: 12px;
  border: 1px solid rgba(255, 255, 255, 0.22);
  background-size: 400% 400%;
  animation: Gradient 15s ease infinite;
  box-shadow: 0 34px 60px -26px rgba(0, 0, 0, 0.85);
  transition: border-radius var(--shelf-dur) var(--turn-ease);
}

.boxCase p {
  font-size: 9px;
  font-weight: 300;
  letter-spacing: 2px;
  text-transform: uppercase;
  line-height: 1.8;
  color: rgba(255, 255, 255, 0.85);
}

/* The three layers of the front -- case, photos, cover -- get a few pixels
   of real depth between them. Coplanar layers in a 3D context have no
   stable front-to-back order while the box turns, which showed as the
   photos flickering in front of the cover and then behind it. */
.boxPhotos {
  position: absolute;
  left: 13%;
  right: 13%;
  top: 10%;
  bottom: 10%;
  transform: translateZ(2px);
}

.boxPhoto {
  position: absolute;
  inset: 0;
  margin: 0;
  border-radius: 9px;
  overflow: hidden;
  border: 1px solid rgba(255, 255, 255, 0.35);
  box-shadow: 0 18px 34px -14px rgba(0, 0, 0, 0.75);
  background: #1a1526;
  transform-origin: 50% 85%;
  transition: transform 0.7s var(--turn-ease);
}

.boxPhoto img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

.boxPhoto figcaption {
  position: absolute;
  top: 10px;
  right: 10px;
  writing-mode: vertical-rl;
  font-family: "Baskerville", "Georgia", serif;
  font-size: 9px;
  font-weight: 600;
  letter-spacing: 2px;
  text-transform: uppercase;
  color: #fff;
  text-shadow: 0 1px 4px rgba(0, 0, 0, 0.7);
}

.boxPhoto--back {
  z-index: 1;
}

.boxPhoto--middle {
  z-index: 2;
}

.boxPhoto--front {
  z-index: 3;
}

/* The glass folder cover, hinged on its left edge. Its colour comes from
   two layers of its own -- the project's gradient at half strength
   (::before) under a white glaze (::after) -- rather than from
   backdrop-filter alone: inside a 3D rendering context browsers can't
   reliably sample what's behind a rotated element, which left the open
   cover clear while the unselected boxes' covers read as tinted glass.
   The blur stays as an extra where it does work. */
.boxCover {
  position: absolute;
  inset: 0;
  z-index: 4;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 24px 20px;
  border-radius: 12px;
  border: 1px solid rgba(255, 255, 255, 0.45);
  background: rgba(255, 255, 255, 0.66);
  -webkit-backdrop-filter: blur(22px) saturate(1.4);
  backdrop-filter: blur(22px) saturate(1.4);
  /* Inset highlight only. A drop shadow here extends the layer's bounds
     past the hinge into the space behind the case, and Chrome's 3D depth
     sort then paints the case over the ~27px of cover nearest the hinge.
     Verified: the shadow alone causes it; radius, border and backface
     don't. */
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.4);
  text-align: center;
  transform-origin: left center;
  transform: translateZ(5px);
  backface-visibility: hidden;
  transition: transform 0.8s var(--turn-ease), border-radius var(--shelf-dur) var(--turn-ease);
}

.boxCover::before,
.boxCover::after {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;
}

.boxCover::before {
  z-index: -2;
  background-image: var(--base-grad);
  background-size: 400% 400%;
  animation: Gradient 15s ease infinite;
  opacity: 0.95;
}

.boxCover::after {
  z-index: -1;
  background: linear-gradient(
    155deg,
    rgba(255, 255, 255, 0.36),
    rgba(255, 255, 255, 0.1) 45%,
    rgba(255, 255, 255, 0.24)
  );
}

.boxCase.istan ~ .boxCover::before {
  background-image: var(--istanbul-grad);
}

.boxCase.edin ~ .boxCover::before {
  background-image: var(--edinburgh-grad);
  background-size: 600% 600%;
}

.boxCase.soon ~ .boxCover::before {
  filter: saturate(0.55) brightness(0.9);
}

.coverTitle {
  font-family: "Baskerville", "Georgia", serif;
  font-style: italic;
  font-weight: 300;
  font-size: clamp(24px, 2.2vw, 30px);
  letter-spacing: 2px;
  text-transform: uppercase;
  color: #fff;
}

.coverDate {
  font-size: 15px;
  font-weight: 300;
  letter-spacing: 3px;
  color: rgba(255, 255, 255, 0.9);
}

.coverHint {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 20px;
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: rgba(255, 255, 255, 0.8);
  opacity: 0;
  transition: opacity 0.4s ease;
}

/* Open state, once the box faces you: the cover swings 40deg on its left
   hinge -- rotation only, no translate, so its left edge stays on the
   case's left edge like a real cover -- and the photos come out to the
   right. These are the positions touch devices (no hover) always show. */
.shelfItem.is-active .boxCover {
  transform: translateZ(5px) rotateY(-40deg);
}

.shelfItem.is-active .coverHint {
  opacity: 1;
}

.shelfItem.is-active .boxPhoto--front {
  transform: translate(21px, 0) rotate(8deg);
}

.shelfItem.is-active .boxPhoto--middle {
  transform: translate(47px, -12px) rotate(14deg);
}

.shelfItem.is-active .boxPhoto--back {
  transform: translate(73px, -26px) rotate(20deg);
}

/* The selection sequence -- turn, then cover, then photos one by one --
   is just these delays. They apply only while .is-opening, which the
   script removes ~1.5s after selection, so hovering in and out afterwards
   responds immediately instead of waiting on the stagger. */
.shelfItem.is-opening .boxCover {
  transition-delay: 0.5s;
}

.shelfItem.is-opening .coverHint {
  transition-delay: 0.9s;
}

.shelfItem.is-opening .boxPhoto--front {
  transition-delay: 0.6s;
}

.shelfItem.is-opening .boxPhoto--middle {
  transition-delay: 0.68s;
}

.shelfItem.is-opening .boxPhoto--back {
  transition-delay: 0.76s;
}

/* With a pointer that can hover, the open box rests with its photos
   mostly tucked inside the case (the middle one flush with its inset,
   the back one peeking past the edge), and the hover pushes them 47px
   further out to the positions above while the cover swings wider on the
   same hinge -- the old cards' fan-out, kept. */
@media (hover: hover) {
  .shelfItem.is-active .boxPhoto--front {
    transform: translate(-26px, 0) rotate(8deg);
  }

  .shelfItem.is-active .boxPhoto--middle {
    transform: translate(0, -12px) rotate(14deg);
  }

  .shelfItem.is-active .boxPhoto--back {
    transform: translate(26px, -26px) rotate(20deg);
  }

  .shelfItem.is-active .shelfBox:hover .boxCover {
    transform: translateZ(5px) rotateY(-60deg);
    transition-delay: 0s;
  }

  .shelfItem.is-active .shelfBox:hover .boxPhoto--front {
    transform: translate(21px, 0) rotate(8deg);
    transition-delay: 0s;
  }

  .shelfItem.is-active .shelfBox:hover .boxPhoto--middle {
    transform: translate(47px, -12px) rotate(14deg);
    transition-delay: 0.06s;
  }

  .shelfItem.is-active .shelfBox:hover .boxPhoto--back {
    transform: translate(73px, -26px) rotate(20deg);
    transition-delay: 0.12s;
  }
}

/* Boxes to the right of the open one show their side in perspective: the
   front face seen edge-on, meeting the spine at the fold. Two rounded
   faces meeting at a fold leave a notch at the top and bottom, so just
   the corners on the fold are squared -- the box's outer corners stay
   round -- and they round again as the box turns to face you. */
.shelfItem.is-active ~ .shelfItem .boxSpine {
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
}

.shelfItem.is-active ~ .shelfItem .boxCase,
.shelfItem.is-active ~ .shelfItem .boxCover {
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
}

/* Caption, arrows, counter */

.shelfInfo {
  display: grid;
  grid-template-columns: 52px 1fr 52px;
  align-items: center;
  gap: 16px;
  max-width: 820px;
  margin: 8px auto 0;
  text-align: center;
}

.shelfCaption {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 8px;
  transition: opacity 0.25s ease, transform 0.25s ease;
}

.shelfCaption.is-swapping {
  opacity: 0;
  transform: translateY(6px);
}

.shelfName {
  font-size: clamp(26px, 3vw, 40px);
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: -0.02em;
}

.shelfMeta {
  font-size: 12px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--hero-fg-3);
}

.shelfSummary {
  max-width: 54ch;
  font-family: "Baskerville", "Georgia", serif;
  font-style: italic;
  font-size: 18px;
  line-height: 1.6;
  color: var(--hero-fg-2);
  text-wrap: pretty;
}

.shelfCta {
  margin-top: 10px;
}

/* The project button inverts on hover: the gradient ring grows to fill
   the pill, the white fill fades, the label goes white. Overrides the
   general .arcBtn hover (gradient text) -- same specificity, later rule. */
.shelfCta:hover > span,
.shelfCta:focus-visible > span {
  background: none;
  color: #fff;
  animation: none;
}

.shelfCta:hover::before,
.shelfCta:focus-visible::before {
  inset: 0;
  opacity: 1;
}

.shelfCta:hover::after,
.shelfCta:focus-visible::after {
  opacity: 0;
}

/* Tablet: the summary gets ~22% more measure than on phones. */
@media (min-width: 641px) {
  .shelfSummary {
    max-width: 66ch;
  }
}

/* Desktop: the caption row spans nearly the full page so the arrows sit
   out by the edges (like the reference) and the summary gets room to run
   two lines instead of three. */
@media (min-width: 1024px) {
  .shelfInfo {
    max-width: 1180px;
    gap: 40px;
  }

  .shelfSummary {
    max-width: 85ch;
  }
}

/* Foot of the projects section, which is also the page's contact block:
   the name and copyright lines, the contact chips beneath them, and the
   project counter on the right. */
.shelfFoot {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: start;
  column-gap: 16px;
  row-gap: 12px;
  max-width: 1100px;
  margin: clamp(28px, 4vh, 48px) auto 0;
  padding-top: 16px;
  border-top: 1px solid var(--hero-line);
  font-size: 11px;
  font-weight: 500;
  letter-spacing: 0.16em;
  text-transform: uppercase;
  color: var(--hero-fg-3);
}

.shelfFootInfo {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 10px;
}

/* The chips take the whole width under the two lines and the counter, so
   on a phone they still sit side by side instead of wrapping in the
   narrow left column. */
.shelfContact {
  grid-column: 1 / -1;
  justify-content: center;
  margin-top: 4px;
  flex-wrap: nowrap;
  /* Undo the foot's small-caps setting for the chips. */
  letter-spacing: normal;
  text-transform: none;
  font-size: 14px;
}

.shelfContact a {
  min-width: 0;
}

.shelfContact span {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

@media (max-width: 640px) {
  .shelfContact a {
    padding: 12px 14px;
  }
}

/* Smallest phones: a size down keeps both numbers whole instead of
   ellipsised. */
@media (max-width: 360px) {
  .shelfContact span {
    font-size: 13px;
  }
}

.shelfCount {
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

@media (max-width: 640px) {
  /* Row of three = 44 + 12 + (16 + 184 + 56) + 12 + 44 = 368px, so it fits
     a 390px phone with the open box centred; the photos eject a shorter
     way to stay off the right-hand spine. */
  .shelfSection {
    --spine-w: 44px;
    --front-w: 184px;
    --box-h: 270px;
    --shelf-gap: 12px;
    --slot-pad-l: 16px;
    --slot-pad-r: 56px;
  }

  /* Keep all four chips on one row. */
  .shelfChip {
    padding: 8px 11px;
    font-size: 11px;
    letter-spacing: 0.08em;
  }

  .shelfInfo {
    grid-template-columns: 42px 1fr 42px;
    gap: 8px;
  }

  .shelfInfo .arcArrow {
    width: 42px;
    height: 42px;
  }

  .shelfItem.is-active .boxPhoto--front {
    transform: translate(10px, 0) rotate(6deg);
  }

  .shelfItem.is-active .boxPhoto--middle {
    transform: translate(26px, -10px) rotate(11deg);
  }

  .shelfItem.is-active .boxPhoto--back {
    transform: translate(42px, -20px) rotate(16deg);
  }
}

/* Narrow window with a mouse: same tucked-in rest / spilled hover pair,
   scaled to the smaller box (a 33px push instead of 47px). */
@media (hover: hover) and (max-width: 640px) {
  .shelfItem.is-active .boxPhoto--front {
    transform: translate(-23px, 0) rotate(6deg);
  }

  .shelfItem.is-active .boxPhoto--middle {
    transform: translate(-7px, -10px) rotate(11deg);
  }

  .shelfItem.is-active .boxPhoto--back {
    transform: translate(9px, -20px) rotate(16deg);
  }

  .shelfItem.is-active .shelfBox:hover .boxPhoto--front {
    transform: translate(10px, 0) rotate(6deg);
  }

  .shelfItem.is-active .shelfBox:hover .boxPhoto--middle {
    transform: translate(26px, -10px) rotate(11deg);
  }

  .shelfItem.is-active .shelfBox:hover .boxPhoto--back {
    transform: translate(42px, -20px) rotate(16deg);
  }
}

/* ---------- Verse ---------- */

.verseText {
  max-width: 900px;
  margin: clamp(56px, 8vh, 96px) auto 0;
  text-align: center;
  font-size: clamp(16px, 2vw, 24px);
  font-style: italic;
  font-weight: 500;
  line-height: 1.5;
  color: var(--hero-fg);
}

.verseText h1,
.verse h1 {
  font-size: inherit;
  font-weight: inherit;
}

/* Same device as before -- the surrounding words outlined, "seen" and
   "unseen" solid -- inverted for the dark ground. */
.verseSpan {
  -webkit-text-stroke: 1px rgba(246, 243, 251, 0.55);
  color: transparent;
}

.verse {
  margin-top: 14px;
  text-align: center;
  font-size: clamp(13px, 1.4vw, 18px);
  font-style: italic;
  color: var(--hero-fg-2);
}

/* ---------- Footer ---------- */


/* ---------- Reduced motion ---------- */

@media (prefers-reduced-motion: reduce) {
  .arcHero {
    --turn-dur: 0.01s;
  }

  .shelfSection {
    --shelf-dur: 0.01s;
  }

  .arcWheel,
  .arcTile,
  .arcGlow,
  .arcScrollCueLine,
  .arcLightbox {
    animation: none;
  }

  .lbFrame::before {
    animation: Gradient 15s ease infinite;
  }

  .boxCover,
  .boxPhoto,
  .coverHint,
  .shelfCaption {
    transition-duration: 0.01s !important;
    transition-delay: 0s !important;
  }

  ::view-transition-group(*),
  ::view-transition-old(*),
  ::view-transition-new(*) {
    animation: none !important;
  }
}
