:root {
    --pa-btn-bg: #fff;
  /* Prevent the browser's two-finger swipe-to-navigate gesture globally.
     e.preventDefault() on wheel handles it in JS, but this is the CSS
     belt-and-suspenders that covers all edge cases including Safari. */
  overscroll-behavior-x: none;

  --color-dark: #171919;
  --color-light: #e6e6e6;

  --color-bg: #ff5230;
  --color-secondary: #162cff;
  --color-tertiary: #e3ff96;
  --color-text: var(--color-dark);
  --color-wireframe: #ededed;

  --font-title: "Montserrat Alternates", sans-serif;
  --font-body: "Forum", serif;
  --font-accent: "Yrsa", serif;

  --font-accent-weight: 100;
  --font-accent-hover-weight: 800;

  --font-accent-opsz-small: 18;
  --font-accent-opsz-large: 512;
}

.home__profession {
  position: fixed;
  top: 50%;
  right: 8%;
  transform: translateY(-50%);
  z-index: 3;

  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 9px;

  pointer-events: none;
  opacity: 0;
}


.home__profession-line {
  position: relative;
  display: inline-block;

  font-family: var(--font-accent);
  font-weight: var(--font-accent-weight);
  font-variation-settings:
    "opsz" var(--font-accent-opsz-small),
    "wght" var(--font-accent-weight);

  font-size: clamp(1.42rem, 2.85vw, 2.1rem);
  line-height: 0.95;
  color: var(--profession-text-color, var(--color-text));
  white-space: nowrap;
}

.home__profession-lineText {
  position: relative;
  display: inline-block;
  z-index: 2;
  white-space: pre;
}

.home__profession-lineBg {
  position: absolute;
  z-index: 1;
  right: 0px;
  left: -3px;
  top: -18px;
  height: calc(100% + 14px);
  width: 0;
  background: var(--profession-bg-color, var(--color-tertiary));
  pointer-events: none;
}

.home__profession-char {
  display: inline-block;
}

@media (max-width: 900px) {
  .home__profession {
    right: 5%;
  }

  .home__profession-line {
    font-size: clamp(1.2rem, 3.6vw, 1.7rem);
  }
}

@media (max-width: 640px) {
  .home__profession {
    right: 3.5%;
    gap: 7px;
  }

  .home__profession-line {
    font-size: clamp(1rem, 5vw, 1.3rem);
  }
}

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

html,
body {
  margin: 0;
  padding: 0;
  width: 100%;
  min-height: 100%;
}

body {
  background: var(--color-bg);
  color: var(--profession-text-color, var(--color-text));
  font-family: var(--font-body);
}

.home {
  position: relative;
  width: 100%;
  background: var(--color-bg);
}

.home__scene {
  position: fixed;
  inset: 0;
  z-index: 1;
}

.home__hero {
  position: relative;
  min-height: 100vh;
  z-index: 3;
}

.home__scene canvas {
  display: block;
  width: 100%;
  height: 100%;
}

.home__intro-mask {
  position: absolute;
  inset: 0;
  background: #ffffff;
  z-index: 2;
  pointer-events: none;
}

.home__name {
  position: fixed;
  left: 20px;
  margin: 0;
  color: var(--profession-text-color, var(--color-text));
  font-family: var(--font-title);
  font-size: clamp(3.36rem, 9.6vw, 6.72rem);
  line-height: 0.9;
  letter-spacing: -0.04em;
  font-weight: 500;
  text-transform: lowercase;
  z-index: 3;
  pointer-events: none;
  will-change: transform;
  transition: transform 1s cubic-bezier(0.05, 0.88, 0.10, 1);
}

.home__name--top {
  top: 20px;
  --intro-shift: -140%;
  transform: translateY(var(--intro-shift));
}

.home__name--bottom {
  bottom: 20px;
  --intro-shift: 140%;
  --scroll-shift: 0px;
  transform: translateY(calc(var(--intro-shift) + var(--scroll-shift)));
}

.home__name--entered {
  --intro-shift: 0px;
}

@media (max-width: 640px) {
  .home__name {
    left: 16px;
    font-size: clamp(2.4rem, 11vw, 4.2rem);
  }

  .home__name--top {
    top: 16px;
  }

  .home__name--bottom {
    bottom: 16px;
  }
}



.home__content {
  position: relative;
  z-index: 4;
  min-height: 140vh;
  padding-top: 100vh;
}

.home__current {
  position: relative;
  padding: 0 20px 120px 20px;
}


.home__current-box {
  display: inline-block;
  background: var(--profession-bg-color, var(--color-tertiary));
  color: var(--profession-text-color, var(--color-text));
  font-family: var(--font-accent);
  font-weight: var(--font-accent-weight);
  font-variation-settings:
    "opsz" var(--font-accent-opsz-small),
    "wght" var(--font-accent-weight);
  font-size: clamp(1.1rem, 2.2vw, 1.5rem);
  line-height: 1.2;
  white-space: pre-line;
  padding: 16px 18px 18px 18px;
  max-width: min(540px, calc(100vw - 40px));
}


.home__profession-warp {
  position: fixed;
  z-index: 3;
  pointer-events: none;
  opacity: 0;
}

.home__profession-warp--visible {
  opacity: 1;
}

.home__profession-warpCopy {
  position: absolute;
  top: 0;
  left: 0;

  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 9px;

  width: 100%;
  height: 100%;
  pointer-events: none;
  will-change: transform, clip-path;
}

.home__profession-warp .home__profession-line {
  position: relative;
  display: inline-block;

  font-family: var(--font-accent);
  font-weight: var(--font-accent-weight);
  font-variation-settings:
    "opsz" var(--font-accent-opsz-small),
    "wght" var(--font-accent-weight);

  font-size: clamp(1.42rem, 2.85vw, 2.1rem);
  line-height: 0.95;
  color: var(--profession-text-color, var(--color-text));
  white-space: nowrap;
}

.home__profession-warp .home__profession-lineText {
  position: relative;
  display: inline-block;
  z-index: 2;
  white-space: pre;
}

.home__profession-warp .home__profession-lineBg {
  position: absolute;
  z-index: 1;
  right: 0px;
  left: -3px;
  top: -18px;
  height: calc(100% + 14px);
  width: 0;
  background: var(--profession-bg-color, var(--color-tertiary));
  pointer-events: none;
}

.home__profession-warp .home__profession-char {
  display: inline-block;
}

@media (max-width: 640px) {
  .home__profession-warpCopy {
    gap: 7px;
  }
}


.home__profession-warpBridge {
  position: absolute;
  left: 0;
  height: 1px;
  width: 100%;
  overflow: hidden;
  pointer-events: none;
  transform-origin: top left;
  will-change: transform, top;
}

.home__profession-warpBridgeInner {
  position: absolute;
  top: 0;
  left: 0;

  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 9px;

  width: 100%;
  height: 100%;
  pointer-events: none;
  will-change: transform;
}

@media (max-width: 640px) {
  .home__profession-warpBridgeInner {
    gap: 7px;
  }
}

.home__profession-warpCopy--top {
  transform-origin: bottom left;
  will-change: transform, clip-path;
}





.home-nav {
  position: fixed;
  inset: 0;
  z-index: 6;
  pointer-events: none;
}

.home-nav__toggle {
  position: fixed;
  top: 50%;
  right: 20px;
  transform: translateY(-50%) translateX(0);
  z-index: 8;

  display: inline-flex;
  align-items: center;
  justify-content: center;

  width: 42px;
  height: 42px;
  margin: 0;
  padding: 0;
  border: 0;
  background: transparent;
  appearance: none;
  cursor: pointer;
  color: var(--color-wireframe);
  overflow: visible;

  opacity: 0;
  pointer-events: none;

  transition:
    opacity 0.7s cubic-bezier(0.05, 0.88, 0.10, 1),
    transform 0.9s cubic-bezier(0.05, 0.88, 0.10, 1);
}

.is-home-nav-ready .home-nav__toggle {
  opacity: 1;
  pointer-events: auto;
}

.is-home-nav-open .home-nav__toggle {
  transform: translateY(-50%) translateX(calc(-100vw + 120px));
}

.home-nav__lines {
  display: block;
  overflow: visible;
}

.home-nav__line {
  fill: var(--color-wireframe);
  transform-box: fill-box;
  transform-origin: center;
  transition:
    transform 0.5s cubic-bezier(0.05, 0.88, 0.10, 1),
    opacity 0.3s ease;
}

.is-home-nav-open .home-nav__line:nth-child(1) {
  transform: translateY(7.25px) rotate(45deg);
}

.is-home-nav-open .home-nav__line:nth-child(2) {
  opacity: 0;
  transform: scaleX(0);
}

.is-home-nav-open .home-nav__line:nth-child(3) {
  transform: translateY(-7.25px) rotate(-45deg);
}

.home-nav__panel {
  position: fixed;
  inset: 0;
  z-index: 7;
  pointer-events: none;

  transform: translateX(100%);
  opacity: 0;

  transition:
    transform 0.9s cubic-bezier(0.05, 0.88, 0.10, 1),
    opacity 0.9s cubic-bezier(0.05, 0.88, 0.10, 1);
}

.is-home-nav-open .home-nav__panel {
  transform: translateX(0);
  opacity: 1;
  pointer-events: auto;
}

.home-nav__inner {
  position: absolute;
  inset: 20px 20px 20px 20px;

  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: flex-end;

  height: calc(100vh - 40px);
}

.home-nav__link {
  display: inline-block;

  color: var(--color-secondary);
  text-decoration: none;
  text-align: right;
  text-transform: lowercase;

  font-family: var(--font-accent);
  font-weight: var(--font-accent-weight);
  font-variation-settings:
    "opsz" var(--font-accent-opsz-large),
    "wght" var(--font-accent-weight);

  font-size: clamp(4.8rem, 15vh, 14rem);
  line-height: 0.68;
  letter-spacing: -0.03em;

  transform: translateX(60px);
  opacity: 0;

  transition-property: transform, opacity, font-variation-settings;
  transition-duration: 0.9s, 0.65s, 0.18s;
  transition-timing-function: cubic-bezier(0.05, 0.88, 0.10, 1), ease, ease;
  transition-delay: 0s, 0s, 0s;
}

.is-home-nav-open .home-nav__link:nth-child(1) { transition-delay: 0.06s; }
.is-home-nav-open .home-nav__link:nth-child(2) { transition-delay: 0.11s; }
.is-home-nav-open .home-nav__link:nth-child(3) { transition-delay: 0.16s; }
.is-home-nav-open .home-nav__link:nth-child(4) { transition-delay: 0.21s; }
.is-home-nav-open .home-nav__link:nth-child(5) { transition-delay: 0.26s; }
.is-home-nav-open .home-nav__link:nth-child(6) { transition-delay: 0.31s; }
.is-home-nav-open .home-nav__link:nth-child(7) { transition-delay: 0.36s; }


.home-nav__link.is-active {
  text-decoration: underline;
  text-underline-offset: 0.12em;
  text-decoration-thickness: 0.04em;
}

.is-home-nav-open .home-nav__link {
  transform: translateX(0);
  opacity: 1;
}

.home__scene,
.home__hero,
.home__content {
  transition:
    filter 0.8s cubic-bezier(0.05, 0.88, 0.10, 1),
    transform 0.8s cubic-bezier(0.05, 0.88, 0.10, 1),
    opacity 0.8s cubic-bezier(0.05, 0.88, 0.10, 1);
}

.is-home-nav-open .home__scene,
.is-home-nav-open .home__hero,
.is-home-nav-open .home__content {
  filter: blur(10px);
}

@media (max-width: 640px) {
  .home-nav__toggle {
    right: 16px;
  }

  .is-home-nav-open .home-nav__toggle {
    transform: translateY(-50%) translateX(calc(-100vw + 96px));
  }

  .home-nav__inner {
    inset: 16px;
  }

  .home-nav__link {
    font-size: clamp(2.8rem, 10vh, 5.5rem);
    line-height: 0.8;
  }
}


.home-nav__link:hover,
.home-nav__link:focus-visible {
  font-variation-settings:
    "opsz" var(--font-accent-opsz-large),
    "wght" var(--font-accent-hover-weight);
}

/* Neutral launch homepage
   Temporary stable homepage without labyrinth / intro system.
*/

.neutral-home {
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  padding: clamp(1.25rem, 2vw, 2rem);
  background: #f7f5ef;
  color: var(--color-dark);
  font-family: "Yrsa", Georgia, serif;
}

.neutral-home a {
  color: currentColor;
  text-decoration: none;
}

.neutral-home a:hover {
  text-decoration: underline;
  text-underline-offset: 0.18em;
}

.neutral-home__header {
  display: flex;
  justify-content: space-between;
  gap: 2rem;
  align-items: flex-start;
  font-family: "Montserrat Alternates", system-ui, sans-serif;
  font-size: 0.82rem;
  letter-spacing: 0.02em;
}

.neutral-home__name {
  font-weight: 500;
}

.neutral-home__nav {
  display: flex;
  gap: 1rem;
  flex-wrap: wrap;
  justify-content: flex-end;
}

.neutral-home__intro {
  margin-block: auto;
  max-width: 48rem;
  padding-block: 6rem;
}

.neutral-home__intro h1 {
  margin: 0;
  font-family: "Forum", Georgia, serif;
  font-size: clamp(4rem, 14vh, 12rem);
  line-height: 0.86;
  font-weight: 400;
  letter-spacing: -0.06em;
}

.neutral-home__intro p {
  max-width: 38rem;
  margin: 2rem 0 0;
  font-size: clamp(1.25rem, 2vw, 1.8rem);
  line-height: 1.25;
}

.neutral-home__info {
  max-width: 42rem;
  font-size: 1.15rem;
  line-height: 1.45;
}

.neutral-home__current > *:first-child {
  margin-top: 0;
}

.neutral-home__current > *:last-child {
  margin-bottom: 0;
}

.neutral-home__footer {
  display: flex;
  gap: 1rem;
  flex-wrap: wrap;
  margin-top: 4rem;
  padding-top: 1rem;
  font-family: "Montserrat Alternates", system-ui, sans-serif;
  font-size: 0.82rem;
  letter-spacing: 0.02em;
}

@media (max-width: 700px) {
  .neutral-home__header {
    display: block;
  }

  .neutral-home__nav {
    justify-content: flex-start;
    margin-top: 1rem;
  }

  .neutral-home__intro {
    padding-block: 5rem;
  }

  .neutral-home__footer {
    display: grid;
    gap: 0.45rem;
  }
}

/* ========================================================================
   Portfolio Atlas v0.1
   ======================================================================== */

:root {
  --pa-bg: linear-gradient(to bottom, #e5e5e5, #f2f2f2);
  --pa-fg: var(--color-dark);
  --pa-muted: #6b6b66;
  --pa-accent: var(--color-dark);
  --pa-pad-x: clamp(1.25rem, 8vw, 7rem);
  --pa-pad-y: clamp(1.25rem, 6vh, 4rem);
  --pa-anim: 420ms cubic-bezier(0.22, 0.61, 0.36, 1);

  /* ── Tape Generator settings (overridable by inline style from PHP
     template + JS panel). Defaults here are the absolute fallbacks. */
  --tape-text-top-offset: clamp(3rem, 12vh, 7rem);
  --tape-text-col-pad-x: clamp(1.1rem, 2.2vw, 2.4rem);
  --tape-overlay-card-bg: var(--color-light);
  --tape-overlay-card-fg: var(--color-dark);
  --tape-overlay-card-pad: 1rem;
  --tape-overlay-card-max-w: min(26rem, 82%);
  --tape-overlay-card-inset: clamp(1rem, 3vw, 2.5rem);
  --tape-model3d-scale: 1;
  --tape-model3d-offset-x: 0px;
  --tape-model3d-offset-y: 0px;
  --tape-caption-font-size: 0.62rem;
  --tape-caption-line-height: 1.08;
  --tape-caption-inset: 0.75rem;
  --tape-floating-img-inset: clamp(0.5rem, 1.2vw, 1.25rem);
  --tape-short-text-font-size: clamp(1.05rem, 1.55vw, 1.7rem);
  --tape-short-text-line-height: 1.18;
  --tape-short-text-shadow: none;

  /* ── Mobile (smartphone) tuning. Used only under
       html.touch-native.is-mobile-narrow — see overrides further down.
       Centralized here as variables so the Mobile Tape Generator (M9–M12)
       can override them from the panel/admin later without changing CSS. */
  --tape-mobile-safe-pad-top:    0px;    /* M11: extra inward inset at top of viewport */
  --tape-mobile-safe-pad-bottom: 25px;    /* M12: extra inward inset at bottom of viewport */
  --tape-mobile-footer-font-size:   clamp(0.95rem, 4.2vw, 1.15rem); /* M9 */
  --tape-mobile-footer-line-height: 1.08;                            /* M10 */

  /* Computed safe-area helpers (configurable pad + iOS browser-chrome inset).
     Used by every mobile content rule so the two boundaries are honoured
     consistently. Defined globally so calc() resolves to 0 on non-mobile. */
  --tape-mobile-safe-top:    calc(var(--tape-mobile-safe-pad-top)    + env(safe-area-inset-top,    0px));
  --tape-mobile-safe-bottom: calc(var(--tape-mobile-safe-pad-bottom) + env(safe-area-inset-bottom, 0px));
}

body.portfolio-body {
  background: linear-gradient(to bottom, #e5e5e5, #f2f2f2) fixed;
  color: var(--pa-fg);
  font-family: var(--font-body, "Forum", serif);
  overflow: hidden;
}

.portfolio-app {
  position: fixed;
  inset: 0;
  overflow: hidden;
  background: transparent;
  color: var(--pa-fg);
}

.portfolio-app__viewport {
  position: absolute;
  inset: 0;
}

.portfolio-app__noscript {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  padding: 2rem;
  text-align: center;
}

.portfolio-app__hud {
  position: fixed;
  left: var(--pa-pad-x);
  bottom: 1.25rem;
  display: flex;
  gap: 1.5rem;
  font-family: var(--font-title, sans-serif);
  font-size: 0.75rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--pa-muted);
  z-index: 5;
  pointer-events: none;
}

/* Menu toggle (hamburger-ish) */
.portfolio-app__menu-toggle {
  position: fixed;
  top: 1.25rem;
  left: 1.25rem;
  z-index: 60;
  width: 2.25rem;
  height: 2.25rem;
  display: grid;
  place-items: center;
  background: transparent;
  border: 0;
  cursor: pointer;
  padding: 0;
}
.portfolio-app__menu-lines {
  display: block;
  overflow: visible;
}
.portfolio-app__menu-line {
  fill: var(--pa-fg);
  transform-box: fill-box;
  transform-origin: center;
  transition:
    fill 0.5s ease,
    transform 0.5s cubic-bezier(0.05, 0.88, 0.10, 1),
    opacity 0.3s ease;
}
.portfolio-app__menu-toggle.is-on-dark .portfolio-app__menu-line {
  fill: var(--color-light);
}
.is-menu-open .portfolio-app__menu-line:nth-child(1) {
  transform: translateY(8.75px) rotate(45deg);
}
.is-menu-open .portfolio-app__menu-line:nth-child(2) {
  opacity: 0;
  transform: scaleX(0);
}
.is-menu-open .portfolio-app__menu-line:nth-child(3) {
  transform: translateY(-8.75px) rotate(-45deg);
}

/* Views */
.portfolio-view {
  position: absolute;
  inset: 0;
  display: grid;
  grid-template-columns: var(--pa-pad-x) minmax(0, 1fr) var(--pa-pad-x);
  grid-template-rows: var(--pa-pad-y) minmax(0, 1fr) var(--pa-pad-y);
  z-index: 0;
}

.portfolio-view--landing {
  z-index: 1;
}

/* Strip — horizontal tape of all panels for one work */
[data-portfolio-viewport] {
  overflow: hidden;
}
.portfolio-strip {
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  display: flex;
  will-change: transform;
  background: var(--pa-bg);
  /* JS handles horizontal; pan-y lets the browser handle vertical natively
     for nested scrollable areas (e.g. .portfolio-cv-inner). Without this,
     iOS Safari may conflict with the horizontal JS gesture handler. */
  touch-action: pan-y;
}

/* Strip panels — flex children of the strip */
.portfolio-panel {
  position: relative;
  flex-shrink: 0;
  height: 100%;
  display: grid;
  grid-template-columns: var(--pa-pad-x) minmax(0, 1fr) var(--pa-pad-x);
  grid-template-rows: var(--pa-pad-y) minmax(0, 1fr) var(--pa-pad-y);
}
.portfolio-panel--full    { width: 100vw; }
.portfolio-panel--half    { width: 50vw; }
.portfolio-panel--third   { width: calc(100vw / 3); }

/* Landing+CV combined strip: white background so the CV columns share the
   landing page's white canvas (not the default grey strip backdrop). */
.portfolio-landing-strip { background: #ffffff; }

.portfolio-view__inner {
  grid-column: 2;
  grid-row: 2;
  max-width: 72rem;
  width: 100%;
  align-self: center;
  justify-self: start;
}


/* Cover view */
.portfolio-view--cover {
  display: flex;
  align-items: center;
  justify-content: flex-end;
}
.portfolio-cover__bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  background: var(--image-bg, #1a1a1a);
}
.portfolio-cover__bg .portfolio-img {
  width: 100%;
  height: 100%;
}
.portfolio-view--cover .portfolio-view__inner {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  text-align: right;
  padding-right: clamp(2rem, 8vw, 7rem);
}
.portfolio-cover__title {
  display: block;
  height: auto;
  overflow: visible;
  color: var(--color-dark);
  user-select: none;
  cursor: default;
}
.portfolio-cover__title-letter {
  font-family: 'Montserrat Alternates', sans-serif;
  font-weight: 600;
  font-size: 220px;
  letter-spacing: -0.01em;
  fill: currentColor;
  opacity: 1;
  text-transform: lowercase;
}
.portfolio-view--cover.is-light-title .portfolio-cover__title {
  color: var(--color-light);
}
.portfolio-cover__meta {
  display: none;
}
.portfolio-cover__sep {
  opacity: 0.4;
}

/* Short text */
.portfolio-eyebrow {
  font-family: var(--font-title, sans-serif);
  font-size: 0.75rem;
  text-transform: lowercase;
  letter-spacing: 0.08em;
  margin: 0 0 1.5rem;
  color: var(--pa-muted);
}
.portfolio-short-text {
  margin: 0;
  font-size: clamp(1.4rem, 3vw, 2.4rem);
  line-height: 1.25;
  max-width: 32em;
}

/* Long text segments */
.portfolio-long-text {
  font-family: 'RooneySans', 'Forum', serif;
  font-weight: 400;
  font-size: 12pt;
  line-height: 1.55;
  max-width: 42em;
  color: var(--color-dark);
}
.portfolio-long-text p { margin: 0 0 1em; }
.portfolio-long-text p:last-child { margin-bottom: 0; }

/* Image — full-bleed variant: edge-to-edge fill (any width) */
.portfolio-view--image.is-full-bleed {
  display: block;
  overflow: hidden;
}
.portfolio-view--image.is-full-bleed .portfolio-view__inner {
  position: absolute;
  inset: 0;
  display: flex;
  align-items: stretch;
  /* Reset base `.portfolio-view__inner` `place-self: center start;`. Without
     this, modern browsers size the absolutely-positioned inner to its content
     (image's intrinsic height) instead of stretching to the article's 431px,
     so the image overflows and gets clipped by the article's overflow:hidden. */
  place-self: stretch;
  max-width: none;
  width: auto;
  padding: 0;
}
.portfolio-view--image.is-full-bleed .portfolio-figure {
  flex: 1;
  min-width: 0;
}
.portfolio-view--image.is-full-bleed .portfolio-figure .portfolio-img {
  width: 100%;
  height: 100%;
}

/* Image — floating variant: padded, contain (any width allowed: half or full) */
.portfolio-view--image.is-floating {
  overflow: hidden;
}
.portfolio-view--image.is-floating .portfolio-view__inner {
  display: flex;
  align-items: stretch;
  align-self: stretch;     /* fill the full panel content row — gives figure a definite height */
  justify-self: center;    /* explicit horizontal centering (width:100% already achieves this) */
  max-width: none;
  width: 100%;
  /* Inset controlled by Tape Generator → Tool 5 (floating image inset). */
  padding: var(--tape-floating-img-inset);
}
.portfolio-view--image.is-floating .portfolio-figure {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;  /* stack image above caption */
  align-items: flex-start; /* left-align caption per spec ("bottom left underneath the image") */
  justify-content: flex-start;
  min-width: 0;
}
.portfolio-view--image.is-floating .portfolio-figure .portfolio-img {
  /* Flex-grow to take all remaining height after the caption.
     The figure has a definite height (inner is align-self:stretch), so flex:1 and
     height:100% both resolve correctly. object-fit:cover fills the allocated space
     without dark borders, cropping the center only when the image AR diverges greatly
     from the allocated div ratio. */
  flex: 1;
  min-height: 0;     /* allow the flex item to shrink below its intrinsic height */
  width: 100%;
  height: 100%;
  max-width: 100%;
}

/* Overlay panels — short_text/long_text on image, and pull-quotes */
.portfolio-panel.portfolio-view--short_text.is-overlay-text,
.portfolio-panel.portfolio-view--long_text_segment.is-overlay-card-top,
.portfolio-panel.portfolio-view--long_text_segment.is-overlay-card-bottom,
.portfolio-panel.portfolio-view--long_text_segment.is-overlay-card-left,
.portfolio-panel.portfolio-view--long_text_segment.is-overlay-card-right,
.portfolio-panel.portfolio-view--quote {
  position: relative;
  display: block;
  overflow: hidden;
}
.portfolio-overlay-bg {
  position: absolute;
  inset: 0;
  z-index: 0;
  background: var(--image-bg, #1a1a1a);
}
.portfolio-overlay-bg .portfolio-img {
  width: 100%;
  height: 100%;
}

/* Short-text overlay (no card) — color/shadow driven by image luminance. */
.portfolio-short-text--overlay {
  position: absolute;
  z-index: 1;
  inset: auto clamp(1.5rem, 4vw, 3rem) clamp(2rem, 8vh, 5rem) clamp(1.5rem, 4vw, 3rem);
  margin: 0;
  /* Default color (overridden by .is-bg-light / .is-bg-dark below). */
  color: var(--color-light);
  font-size: var(--tape-short-text-font-size);
  line-height: var(--tape-short-text-line-height);
  text-shadow: var(--tape-short-text-shadow);
}
.portfolio-short-text--overlay.is-bg-light { color: var(--color-dark); }
.portfolio-short-text--overlay.is-bg-dark  { color: var(--color-light); }
/* Top-anchored variant — used when the image's focus point sits in the lower half. */
.portfolio-short-text--overlay.is-top {
  inset: clamp(2rem, 8vh, 5rem) clamp(1.5rem, 4vw, 3rem) auto clamp(1.5rem, 4vw, 3rem);
}

/* White card overlaying an image (long text) — top or bottom (legacy) */
.portfolio-overlay-card {
  position: absolute;
  z-index: 1;
  left: var(--tape-overlay-card-inset);
  right: var(--tape-overlay-card-inset);
  padding: var(--tape-overlay-card-pad);
  background: var(--tape-overlay-card-bg);
  color: var(--tape-overlay-card-fg);
}
.is-overlay-card-top .portfolio-overlay-card {
  top: clamp(1.5rem, 6vh, 3.5rem);
}
.is-overlay-card-bottom .portfolio-overlay-card {
  bottom: clamp(1.5rem, 6vh, 3.5rem);
}
/* Vertical overlay cards — tall narrow strip, upright text, anchored left or right */
.is-overlay-card-left .portfolio-overlay-card,
.is-overlay-card-right .portfolio-overlay-card {
  left: auto;
  right: auto;
  top: clamp(1.5rem, 6vh, 3.5rem);
  bottom: clamp(1.5rem, 6vh, 3.5rem);
  width: clamp(14rem, 22%, 20rem);
  overflow-y: auto;
}
.is-overlay-card-left .portfolio-overlay-card {
  left: clamp(1rem, 3vw, 2.5rem);
}
.is-overlay-card-right .portfolio-overlay-card {
  right: clamp(1rem, 3vw, 2.5rem);
}
.is-overlay-card-left .portfolio-overlay-card .portfolio-long-text,
.is-overlay-card-right .portfolio-overlay-card .portfolio-long-text {
  max-width: none;
}
.portfolio-overlay-card--text .portfolio-long-text {
  max-width: none;
}

/* Pull-quote overlaying an image */
.portfolio-quote {
  position: relative;
  z-index: 1;
  margin: 0;
  padding: clamp(2rem, 8vh, 5rem) clamp(1.5rem, 4vw, 3rem);
  height: 100%;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  font-family: 'Montserrat Alternates', sans-serif;
  font-weight: 600;
  font-size: clamp(2rem, 4.5vw, 4rem);
  line-height: 1.02;
  letter-spacing: -0.01em;
  color: #fff;
  text-shadow: 0 2px 18px rgba(0, 0, 0, 0.35);
}
.portfolio-quote__mark {
  display: block;
  font-family: 'Forum', serif;
  font-size: 1.6em;
  line-height: 0.6;
  margin-bottom: 0.1em;
  opacity: 0.75;
}

/* Image view */
.portfolio-figure {
  margin: 0;
  display: grid;
  gap: 0.75rem;
}

/* Progressive image wrapper — stages 0..3 of the loading pipeline.
   Wrapper drives the morph-fade: classes `.has-placeholder` and `.is-loaded`
   (toggled in portfolio-app.js) cross-fade the layers below. */
.portfolio-img {
  position: relative;
  display: block;
  overflow: hidden;
  /* Stage 0: persistent dominant color (computed server-side, see
     lz_image_dominant_color()). Visible before any pixel data arrives. */
  background-color: var(--image-bg, #1a1a1a);
}
/* Stage 1: blurry tiny thumbnail layer. Absolutely positioned so it sits
   underneath the real image without competing for layout space. */
.portfolio-img__placeholder {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  object-position: center;
  z-index: 0;
  opacity: 0;
  filter: blur(22px);
  transform: scale(1.12);
  transition: opacity 360ms ease;
  pointer-events: none;
  will-change: opacity;
}
.portfolio-img.has-placeholder .portfolio-img__placeholder {
  opacity: 0.9;
}
/* Stage 2 + 3: real responsive image. Kept IN FLOW (not absolute) so it
   contributes intrinsic dimensions to floating image wrappers — without this,
   the wrapper collapses to 0 size whenever its parent isn't absolutely
   positioned (e.g. .portfolio-view--image.is-floating). Fills the wrapper at
   width/height 100 % so the placeholder underneath aligns exactly. */
.portfolio-img__real {
  position: relative;
  z-index: 1;
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  opacity: 0;
  transform: scale(1.014);
  transition:
    opacity 520ms ease,
    transform 760ms cubic-bezier(0.22, 1, 0.36, 1);
  will-change: opacity, transform;
}
.portfolio-img.is-loaded .portfolio-img__real {
  opacity: 1;
  transform: scale(1);
}
.portfolio-img.is-loaded .portfolio-img__placeholder {
  opacity: 0;
}
/* Floating image variant: real img should be "contain" so transparent edges
   don't crop. The wrapper already matches the intrinsic aspect ratio so the
   blurred placeholder still fills it exactly. */
.portfolio-img--contain .portfolio-img__real {
  object-fit: contain;
}

@media (prefers-reduced-motion: reduce) {
  .portfolio-img__real {
    transform: none;
    transition: opacity 180ms ease;
  }
  .portfolio-img.is-loaded .portfolio-img__real {
    transform: none;
  }
  .portfolio-img__placeholder {
    transition: opacity 180ms ease;
  }
}

.portfolio-figure figcaption {
  display: flex;
  flex-direction: column;
  gap: 0;
  font-size: var(--tape-caption-font-size);
  line-height: var(--tape-caption-line-height);
  color: var(--pa-muted);
  /* Tape Generator → Tool 4 (Off-image distance). Controls gap between
     image and caption for floating panels. On-image figcaptions reset this
     to 0 via the `.is-on-image` rule below. */
  margin-top: var(--tape-caption-off-image-distance, 0.4rem);
}
.portfolio-figure__caption { display: block; }
.portfolio-figure__credit { display: block; color: var(--pa-muted); }

/* Full-bleed images get an on-image caption pinned bottom-left. Color is
   auto-picked from the image's dominant luminance via JS-applied helpers.
   `--tape-caption-on-image-shadow` is set by the Tape Generator settings
   panel (Tool 4 → “Shadow on credit (on-image)”) — default `none`. */
.portfolio-figure figcaption.is-on-image {
  position: absolute;
  left: var(--tape-caption-inset);
  bottom: var(--tape-caption-inset);
  margin: 0;
  z-index: 2;
  color: var(--color-light);
  text-shadow: var(--tape-caption-on-image-shadow, none);
  max-width: calc(100% - 2 * var(--tape-caption-inset));
}
.portfolio-figure figcaption.is-on-image .portfolio-figure__caption,
.portfolio-figure figcaption.is-on-image .portfolio-figure__credit {
  color: inherit;
}
.portfolio-figure figcaption.is-on-image.is-bg-light {
  color: var(--color-dark);
}
.portfolio-figure figcaption.is-on-image.is-bg-dark {
  color: var(--color-light);
}
.portfolio-view--image.is-full-bleed .portfolio-figure { position: relative; }

/* ── Credit position (Tape Generator → Tool 4) ─────────────────────────
   Six classes (tl/tc/tr/bl/bc/br) drive where the on-image figcaption sits.
   Floating images don't get absolute positioning — for them the same class
   only controls text-align so the credit row aligns under the image. */
.portfolio-figure figcaption.is-on-image.is-pos-tl { top:    var(--tape-caption-inset); left:  var(--tape-caption-inset); bottom: auto; right: auto; text-align: left;   }
.portfolio-figure figcaption.is-on-image.is-pos-tc { top:    var(--tape-caption-inset); left:  var(--tape-caption-inset); right: var(--tape-caption-inset); bottom: auto; text-align: center; max-width: calc(100% - 2 * var(--tape-caption-inset)); }
.portfolio-figure figcaption.is-on-image.is-pos-tr { top:    var(--tape-caption-inset); right: var(--tape-caption-inset); bottom: auto; left:  auto; text-align: right;  }
.portfolio-figure figcaption.is-on-image.is-pos-bl { bottom: var(--tape-caption-inset); left:  var(--tape-caption-inset); top:   auto; right: auto; text-align: left;   }
.portfolio-figure figcaption.is-on-image.is-pos-bc { bottom: var(--tape-caption-inset); left:  var(--tape-caption-inset); right: var(--tape-caption-inset); top:   auto; text-align: center; max-width: calc(100% - 2 * var(--tape-caption-inset)); }
.portfolio-figure figcaption.is-on-image.is-pos-br { bottom: var(--tape-caption-inset); right: var(--tape-caption-inset); top:   auto; left:  auto; text-align: right;  }
.portfolio-figure figcaption.is-pos-tc:not(.is-on-image),
.portfolio-figure figcaption.is-pos-bc:not(.is-on-image) { text-align: center; }
.portfolio-figure figcaption.is-pos-tr:not(.is-on-image),
.portfolio-figure figcaption.is-pos-br:not(.is-on-image) { text-align: right; }

/* Overlay-text panels (short_text / long_text_segment / quote with a bg
   image) render the credit as a separate absolute element so it never
   disappears under the overlay panel.  Uses the same six position classes.
   `is-bg-{light|dark}` mirrors the figcaption color contract. */
.portfolio-overlay-credit {
  position: absolute;
  z-index: 3;
  margin: 0;
  font-size: var(--tape-caption-font-size);
  line-height: var(--tape-caption-line-height);
  color: var(--color-light);
  text-shadow: var(--tape-caption-on-image-shadow, none);
  max-width: calc(100% - 2 * var(--tape-caption-inset));
  pointer-events: none;
}
.portfolio-overlay-credit.is-bg-light { color: var(--color-dark);  }
.portfolio-overlay-credit.is-bg-dark  { color: var(--color-light); }
.portfolio-overlay-credit.is-pos-tl { top:    var(--tape-caption-inset); left:  var(--tape-caption-inset); text-align: left;   }
.portfolio-overlay-credit.is-pos-tc { top:    var(--tape-caption-inset); left:  var(--tape-caption-inset); right: var(--tape-caption-inset); text-align: center; }
.portfolio-overlay-credit.is-pos-tr { top:    var(--tape-caption-inset); right: var(--tape-caption-inset); text-align: right;  }
.portfolio-overlay-credit.is-pos-bl { bottom: var(--tape-caption-inset); left:  var(--tape-caption-inset); text-align: left;   }
.portfolio-overlay-credit.is-pos-bc { bottom: var(--tape-caption-inset); left:  var(--tape-caption-inset); right: var(--tape-caption-inset); text-align: center; }
.portfolio-overlay-credit.is-pos-br { bottom: var(--tape-caption-inset); right: var(--tape-caption-inset); text-align: right;  }

/* Video link */
.portfolio-video-link {
  font-size: clamp(1.4rem, 3vw, 2rem);
  color: var(--pa-fg);
  text-decoration: none;
  border-bottom: 1px solid var(--pa-fg);
}

/* Landing screen */
.portfolio-view--landing {
  display: flex;
  align-items: center;
  justify-content: flex-end;
  background: #ffffff;
}

/* Landing inner overrides the default left-aligned, max-72rem inner so the
   logo can sit flush to the right edge of the viewport (with proper padding). */
.portfolio-view--landing .portfolio-view__inner {
  max-width: none;
  width: 100%;
  justify-self: stretch;
  display: flex;
  justify-content: flex-end;
  align-items: center;
}

/* Logo as inline SVG — each letter is a <text> whose outline is animated
   via stroke-dashoffset for a smooth, per-letter "drawn by hand" effect.
   The SVG scales as a single unit via its viewBox, so letters AND their
   kerning shrink proportionally on smaller viewports. */
.portfolio-landing__name {
  display: block;
  /* Responsive width — the viewBox carries proportional internal metrics. */
  width: min(57vw, 770px);
  height: auto;
  overflow: visible;
  user-select: none;
}

.portfolio-landing__letter {
  font-family: 'Montserrat Alternates', sans-serif;
  font-weight: 600;
  /* This drives the SVG's intrinsic viewBox size (measured via getBBox).
     Internal units only — CSS width on the SVG controls the rendered size. */
  font-size: 220px;
  letter-spacing: -0.02em;
  fill: #ffffff;
  stroke: var(--color-dark);
  stroke-width: 1.25;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
  /* JS animates stroke-width (hairline → 1.25) and the SVG displacement-map
     scale on intro. Opacity is flipped from 0 → 1 inline by animateLetter(). */
  opacity: 0;
}

/* Once JS has measured glyphs and called animateLetter(), it flips opacity
   inline and ramps stroke-width via requestAnimationFrame. */
.portfolio-landing__name.is-ready .portfolio-landing__letter {
  /* nothing extra needed — opacity and stroke-width are set inline by JS */
}

@media (prefers-reduced-motion: reduce) {
  .portfolio-landing__name.is-ready .portfolio-landing__letter {
    opacity: 1 !important;
  }
}

/* Puffy Panel — experimental landing-page motion tuner.
   Visible only when localStorage 'lz_motion_experimental' === '1'. */
.puffy-panel {
  position: fixed;
  top: 1rem;
  left: 1rem;
  width: 480px;
  max-height: calc(100vh - 2rem);
  overflow: auto;
  z-index: 9999;
  background: rgba(255, 255, 255, 0.94);
  color: var(--color-dark);
  border: 1px solid var(--color-dark);
  font-family: 'Montserrat Alternates', sans-serif;
  font-size: 11px;
  line-height: 1.4;
  user-select: none;
  -webkit-backdrop-filter: blur(8px);
  backdrop-filter: blur(8px);
}
.puffy-panel__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.4rem 0.6rem;
  border-bottom: 1px solid var(--color-dark);
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
}
.puffy-panel__close {
  background: none;
  border: 0;
  font-size: 16px;
  line-height: 1;
  cursor: pointer;
  padding: 0 4px;
  color: inherit;
}
.puffy-panel__body {
  padding: 0.4rem 0.6rem;
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 0 0.75rem;
}
.puffy-panel__row {
  display: grid;
  grid-template-columns: 1fr;
  gap: 2px;
  padding: 4px 0;
  border-bottom: 1px dashed rgba(0,0,0,0.1);
}
.puffy-panel__label {
  display: flex;
  justify-content: space-between;
  font-size: 10px;
  opacity: 0.8;
}
.puffy-panel__val {
  font-variant-numeric: tabular-nums;
  opacity: 0.7;
  margin-left: 6px;
}
.puffy-panel input[type=range] { width: 100%; accent-color: var(--color-dark); }
.puffy-panel__foot {
  display: flex;
  gap: 4px;
  padding: 0.4rem 0.6rem;
  border-top: 1px solid var(--color-dark);
  grid-column: 1 / -1;
}
.puffy-panel__foot button {
  flex: 1;
  background: var(--color-dark);
  color: #fff;
  border: 0;
  padding: 5px 6px;
  font: inherit;
  font-size: 10px;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  cursor: pointer;
}
.puffy-panel__foot button:hover { opacity: 0.85; }

/* Year tag — fixed at bottom of viewport, same style as cover title */
.portfolio-year-tag {
  position: fixed;
  right: clamp(2rem, 8vw, 7rem);
  bottom: clamp(1rem, 3vh, 2rem);
  font-family: 'Montserrat Alternates', sans-serif;
  font-weight: 600;
  font-size: 20pt;
  line-height: 1;
  letter-spacing: -0.01em;
  color: var(--color-dark);
  z-index: 60;
  opacity: 0;
  pointer-events: none;
  transition: opacity 120ms ease;
}
.portfolio-location-tag {
  position: fixed;
  left: clamp(2rem, 8vw, 7rem);
  bottom: clamp(1rem, 3vh, 2rem);
  font-family: 'Montserrat Alternates', sans-serif;
  font-weight: 600;
  font-size: 20pt;
  line-height: 1;
  letter-spacing: -0.01em;
  color: var(--color-dark);
  z-index: 60;
  opacity: 0;
  pointer-events: none;
  transition: opacity 120ms ease;
}
.portfolio-location-tag.is-visible {
  opacity: 1;
  transition: opacity 180ms ease 80ms;
}
.portfolio-location-tag.is-visible.is-scrolling {
  opacity: 0;
  transition: opacity 180ms ease;
}
.portfolio-location-tag.is-light-title {
  color: var(--color-light);
}
.portfolio-year-tag.is-visible {
  opacity: 1;
  transition: opacity 180ms ease 80ms;
}
.portfolio-year-tag.is-visible.is-scrolling {
  opacity: 0;
  transition: opacity 180ms ease;
}
.portfolio-year-tag.is-light-title {
  color: var(--color-light);
}

/* Touch-native: JS drives footer opacity continuously via inline style. Kill
   the CSS opacity transition so it doesn't fight the per-frame updates. */
html.touch-native .portfolio-year-tag,
html.touch-native .portfolio-location-tag {
  transition: opacity 0ms;
}

/* Mobile footer: location stays bottom-LEFT but sits one line above the
   year (penultimate row); year stays bottom-RIGHT on the last row. Roles
   are preserved from desktop; only the vertical offset of location moves
   up so the two never share a row. Typography is driven by the mobile
   footer CSS variables (M9/M10) and the bottom offset adds the safe-area
   pad so neither tag touches the browser UI on mobile. */
html.is-mobile-narrow .portfolio-year-tag,
html.is-mobile-narrow .portfolio-location-tag {
  font-size: var(--tape-mobile-footer-font-size);
  line-height: var(--tape-mobile-footer-line-height);
  letter-spacing: -0.01em;
}
html.is-mobile-narrow .portfolio-year-tag {
  right: clamp(1rem, 5vw, 2rem);
  left: auto;
  bottom: calc(clamp(1rem, 3vh, 2rem) + var(--tape-mobile-safe-bottom));
  text-align: right;
  white-space: nowrap;
}
html.is-mobile-narrow .portfolio-location-tag {
  left: clamp(1rem, 5vw, 2rem);
  right: auto;
  /* Penultimate line: sits one line above the year. 1.25em ≈ line-height
     × current font-size. Includes the safe-area pad so location too stays
     clear of mobile browser UI. */
  bottom: calc(clamp(1rem, 3vh, 2rem) + var(--tape-mobile-safe-bottom) + 1.25em);
  text-align: left;
  /* Allow location to wrap upward; cap width so it can never extend into
     the bottom row where the year lives. */
  max-width: calc(100vw - 2 * clamp(1rem, 5vw, 2rem));
  word-break: break-word;
  overflow-wrap: anywhere;
}

/* ──────────────────────────────────────────────────────────────────────
   Mobile content safe-area (smartphone only — html.touch-native.is-mobile-narrow)

   The previous implementation tried to enforce the safe area by expanding
   .portfolio-panel's grid rows. That had two flaws:
     (1) `.portfolio-view__inner` is `align-self: center` inside row 2, so
         a Δ increase to the bottom row only shifted centered content up
         by Δ/2 — the other Δ/2 visibly became extra space at the top,
         which felt like the variable was creating top padding.
     (2) Many view types bypass the panel grid entirely (full-bleed image,
         overlay cards, overlay text, pull-quote, model3d, on-image
         caption), so the grid-rows override had no effect on them at all.

   New approach: apply REAL padding (or absolute insets) to each element
   that actually paints content, sized by --tape-mobile-safe-top /
   --tape-mobile-safe-bottom. With global box-sizing: border-box this gives
   a true 1:1 hard boundary — increasing --tape-mobile-safe-pad-bottom
   visibly moves every content's lower edge upward by that amount.

   Full-bleed images remain edge-to-edge by design; their on-image
   captions/credits move with the safe insets.
   ──────────────────────────────────────────────────────────────────── */

/* 1a. Text panels (standalone short_text / long_text_segment).

   Why absolute positioning (not padding)?
   ──────────────────────────────────────
   `overflow: hidden` clips at the PADDING edge (= element's outer box),
   not the content edge. So a `padding-bottom: var(--tape-mobile-safe-bottom)`
   on an otherwise-full-height inner does NOT prevent child text from
   painting INTO the padding-bottom region — text only gets clipped at the
   panel's row 2 bottom, which on mobile is `100vh` (i.e. UNDER the
   browser's URL bar). Result: visually, long text still slid under the UI.

   Real fix: take .portfolio-view__inner out of the panel grid and anchor
   it absolutely to the panel (.portfolio-panel already has
   `position: relative`). Then `top` and `bottom` define a true geometric
   box; `overflow: hidden` clips at that box's outer edge. Increasing
   --tape-mobile-safe-pad-bottom shrinks the box from the bottom 1:1, and
   the first line stays exactly at `top`. */
html.touch-native.is-mobile-narrow .portfolio-view--short_text:not(.is-overlay-text) .portfolio-view__inner,
html.touch-native.is-mobile-narrow .portfolio-view--long_text_segment:not(.is-overlay-card-top):not(.is-overlay-card-bottom):not(.is-overlay-card-left):not(.is-overlay-card-right):not(.is-overlay-corner-top-left):not(.is-overlay-corner-top-right):not(.is-overlay-corner-bottom-left):not(.is-overlay-corner-bottom-right) .portfolio-view__inner {
  position: absolute;
  /* Top baseline is the existing text-top offset (on mobile this resolves
     to --tape-mobile-text-top-offset, default 18vh). Add safe-top so a
     non-zero M11 protects against the top of the browser UI. With M11 = 0
     this is identical to --tape-text-top-offset, so the first line of
     text sits at exactly the same y as before. */
  top:    calc(var(--tape-text-top-offset) + var(--tape-mobile-safe-top));
  right:  var(--tape-text-col-pad-x);
  bottom: var(--tape-mobile-safe-bottom);
  left:   var(--tape-text-col-pad-x);
  /* Cancel inherited grid metrics & base padding. */
  grid-column: auto;
  grid-row:    auto;
  align-self:  auto;
  justify-self: auto;
  width:       auto;
  max-width:   none;
  height:      auto;
  min-height:  0;
  max-height:  none;
  padding:     0;
  /* Top-anchored natural flow. */
  display:  block;
  overflow: hidden;
}

/* 1a-i. Belt-and-braces: ensure the inner text element itself cannot grow
        past its absolutely-positioned container's content box. With the
        absolute inner above, .portfolio-long-text / .portfolio-short-text
        flow naturally from the top and are clipped at the bottom by the
        inner's overflow:hidden. Note: text that overflows is intentionally
        clipped without internal scrolling — segmentation thresholds should
        prevent panels from being overlong. See site/snippets/portfolio-*
        text segmentation. TODO: if clipping fires often in production,
        tighten Tape Generator Tool 2 / text-segment thresholds rather
        than introducing per-panel scroll. */
html.touch-native.is-mobile-narrow .portfolio-view--long_text_segment:not(.is-overlay-card-top):not(.is-overlay-card-bottom):not(.is-overlay-card-left):not(.is-overlay-card-right):not(.is-overlay-corner-top-left):not(.is-overlay-corner-top-right):not(.is-overlay-corner-bottom-left):not(.is-overlay-corner-bottom-right) .portfolio-long-text,
html.touch-native.is-mobile-narrow .portfolio-view--short_text:not(.is-overlay-text) .portfolio-short-text {
  max-height: 100%;
  overflow:   hidden;
}

/* 1b. Centered media panels (video). Vertical centring inside the safe
      area is intentional here. (Floating images get their own padding
      rule below; full-bleed images and cover stay edge-to-edge.) */
html.touch-native.is-mobile-narrow .portfolio-view--video .portfolio-view__inner {
  align-self: stretch;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-top:    var(--tape-mobile-safe-top);
  padding-bottom: var(--tape-mobile-safe-bottom);
}

/* 2. Floating images: real padding directly on the inner. .portfolio-img is
      `flex:1; height:100%` inside, so when the padded box shrinks at the
      bottom by Δ, the image's lower edge moves up by exactly Δ. */
html.touch-native.is-mobile-narrow .portfolio-view--image.is-floating .portfolio-view__inner {
  /* keep existing align-self:stretch + tape-floating-img-inset; add safe pad */
  padding-top:    calc(var(--tape-floating-img-inset) + var(--tape-mobile-safe-top));
  padding-bottom: calc(var(--tape-floating-img-inset) + var(--tape-mobile-safe-bottom));
}

/* 3. Pull-quote panels (`display:block` — bypass panel grid). The quote is
      `height:100%` with its own padding; extend the vertical padding with
      the safe vars so the bottom-anchored quote text never slides under
      the browser UI. */
html.touch-native.is-mobile-narrow .portfolio-view--quote .portfolio-quote {
  padding-top:    calc(clamp(2rem, 8vh, 5rem) + var(--tape-mobile-safe-top));
  padding-bottom: calc(clamp(2rem, 8vh, 5rem) + var(--tape-mobile-safe-bottom));
}

/* 4. Overlay cards (long_text on image, top/bottom/left/right variants).
      They use `position:absolute` with their own clamp() insets; just add
      the safe vars to top / bottom.
      NOTE: `touch-native` is intentionally NOT required here. The safe-area
      concern is viewport-width-driven, not touch-capability-driven. Devices
      with stylus support may have (any-pointer:fine)=true and therefore NOT
      receive the `touch-native` class, yet still need bottom-card protection
      on a narrow screen. Using only `is-mobile-narrow` covers all cases. */
html.is-mobile-narrow .is-overlay-card-top .portfolio-overlay-card {
  top: calc(clamp(1.5rem, 6vh, 3.5rem) + var(--tape-mobile-safe-top));
}
html.is-mobile-narrow .is-overlay-card-bottom .portfolio-overlay-card {
  bottom: calc(clamp(1.5rem, 6vh, 3.5rem) + var(--tape-mobile-safe-bottom));
}
html.is-mobile-narrow .is-overlay-card-left .portfolio-overlay-card,
html.is-mobile-narrow .is-overlay-card-right .portfolio-overlay-card {
  top:    calc(clamp(1.5rem, 6vh, 3.5rem) + var(--tape-mobile-safe-top));
  bottom: calc(clamp(1.5rem, 6vh, 3.5rem) + var(--tape-mobile-safe-bottom));
}

/* 4a. Overlay cards — CORNER variants (long_text on image, anchored to one
       corner via --tape-overlay-card-inset). The directional rules above do
       NOT match these; the base rule at the bottom of the file uses only
       --tape-overlay-card-inset, so a bottom-corner card on mobile sits at
       `bottom: 1rem..2.5rem` and slides under the browser UI. Add the safe
       vars to the active corner edge so the card always clears the UI. */
html.is-mobile-narrow .is-overlay-corner-top-left    .portfolio-overlay-card,
html.is-mobile-narrow .is-overlay-corner-top-right   .portfolio-overlay-card {
  top: calc(var(--tape-overlay-card-inset) + var(--tape-mobile-safe-top));
}
html.is-mobile-narrow .is-overlay-corner-bottom-left  .portfolio-overlay-card,
html.is-mobile-narrow .is-overlay-corner-bottom-right .portfolio-overlay-card {
  bottom: calc(var(--tape-overlay-card-inset) + var(--tape-mobile-safe-bottom));
}

/* 4b. Overlay-card max-height (all variants, mobile only). Position alone is
       not enough — if a card is tall enough that
       `top + height > viewportH - safe-bottom`, its lower edge still ends up
       under the browser UI. Cap the card height to the safe content area
       and clip overflow so a too-long card cannot extend behind the UI.
       Tape generation should prefer non-overlay variants for very long text;
       this rule is the visual safety net. */
html.is-mobile-narrow .is-overlay-card-top    .portfolio-overlay-card,
html.is-mobile-narrow .is-overlay-card-bottom .portfolio-overlay-card,
html.is-mobile-narrow .is-overlay-card-left   .portfolio-overlay-card,
html.is-mobile-narrow .is-overlay-card-right  .portfolio-overlay-card,
html.is-mobile-narrow .is-overlay-corner-top-left     .portfolio-overlay-card,
html.is-mobile-narrow .is-overlay-corner-top-right    .portfolio-overlay-card,
html.is-mobile-narrow .is-overlay-corner-bottom-left  .portfolio-overlay-card,
html.is-mobile-narrow .is-overlay-corner-bottom-right .portfolio-overlay-card {
  max-height: calc(
    100% - var(--tape-mobile-safe-top) - var(--tape-mobile-safe-bottom)
         - 2 * var(--tape-overlay-card-inset)
  );
  overflow: hidden;
}

/* 5. Short-text overlay (no card) — bottom-anchored by default; top-anchored
      variant via .is-top. Add safe pads to the active edge. */
html.is-mobile-narrow .portfolio-short-text--overlay {
  inset: auto clamp(1.5rem, 4vw, 3rem)
         calc(clamp(2rem, 8vh, 5rem) + var(--tape-mobile-safe-bottom))
         clamp(1.5rem, 4vw, 3rem);
}
html.is-mobile-narrow .portfolio-short-text--overlay.is-top {
  inset: calc(clamp(2rem, 8vh, 5rem) + var(--tape-mobile-safe-top))
         clamp(1.5rem, 4vw, 3rem)
         auto
         clamp(1.5rem, 4vw, 3rem);
}

/* 6. On-image figcaption (full-bleed images). Default bottom-left position
      via base rules; explicit position classes (.is-pos-*) override. Add
      safe pads so on-image captions clear the home-bar / safe-area-inset. */
html.touch-native.is-mobile-narrow .portfolio-figure figcaption.is-on-image {
  bottom: calc(var(--tape-caption-inset) + var(--tape-mobile-safe-bottom));
}
html.touch-native.is-mobile-narrow .portfolio-figure figcaption.is-on-image.is-pos-bl,
html.touch-native.is-mobile-narrow .portfolio-figure figcaption.is-on-image.is-pos-bc,
html.touch-native.is-mobile-narrow .portfolio-figure figcaption.is-on-image.is-pos-br {
  bottom: calc(var(--tape-caption-inset) + var(--tape-mobile-safe-bottom));
  top: auto;
}
html.touch-native.is-mobile-narrow .portfolio-figure figcaption.is-on-image.is-pos-tl,
html.touch-native.is-mobile-narrow .portfolio-figure figcaption.is-on-image.is-pos-tc,
html.touch-native.is-mobile-narrow .portfolio-figure figcaption.is-on-image.is-pos-tr {
  top: calc(var(--tape-caption-inset) + var(--tape-mobile-safe-top));
  bottom: auto;
}

/* 7. Overlay credit (shown on overlay-text panels with bg image). Same six
      position classes as on-image figcaption. */
html.touch-native.is-mobile-narrow .portfolio-overlay-credit.is-pos-bl,
html.touch-native.is-mobile-narrow .portfolio-overlay-credit.is-pos-bc,
html.touch-native.is-mobile-narrow .portfolio-overlay-credit.is-pos-br {
  bottom: calc(var(--tape-caption-inset) + var(--tape-mobile-safe-bottom));
}
html.touch-native.is-mobile-narrow .portfolio-overlay-credit.is-pos-tl,
html.touch-native.is-mobile-narrow .portfolio-overlay-credit.is-pos-tc,
html.touch-native.is-mobile-narrow .portfolio-overlay-credit.is-pos-tr {
  top: calc(var(--tape-caption-inset) + var(--tape-mobile-safe-top));
}

/* 8. 3D-model panels (.portfolio-model3d is absolutely positioned and oversized
      horizontally with `top:0; height:100%`). Inset the canvas vertically by
      the safe pads so the model doesn't render under browser UI. The canvas
      is `width:100%/height:100%` inside, so shrinking the wrapper shrinks the
      visible model area 1:1. The wrapper transform/offsets remain unchanged. */
html.touch-native.is-mobile-narrow .portfolio-model3d {
  top:    var(--tape-mobile-safe-top);
  height: calc(100% - var(--tape-mobile-safe-top) - var(--tape-mobile-safe-bottom));
}

/* 9. Cover view: keep the cover image full-bleed (background of every work)
      but inset the right-aligned cover title text vertically so it doesn't
      overlap the location/year footer on phones. Note: the title is centered
      via `display:flex; align-items:center` on the view; we add inner
      padding so the centered text's vertical extent shrinks within the
      safe area. */
html.touch-native.is-mobile-narrow .portfolio-view--cover .portfolio-view__inner {
  padding-top:    var(--tape-mobile-safe-top);
  padding-bottom: var(--tape-mobile-safe-bottom);
  align-self: stretch;
  justify-content: center;
}

/* Section overlays (CV / Contact / Newsletter) */
.portfolio-view--section .portfolio-section__title {
  font-family: var(--font-title, sans-serif);
  font-weight: 500;
  font-size: clamp(1.8rem, 4vw, 3rem);
  margin: 0 0 1.5rem;
}
.portfolio-section__content {
  font-size: clamp(1rem, 1.4vw, 1.2rem);
  line-height: 1.55;
  max-width: 42em;
}
.portfolio-section__content p { margin: 0 0 1em; }

/* Site menu */
.site-menu {
  position: fixed;
  inset: 0;
  z-index: 100;
  pointer-events: none;
}
.site-menu__backdrop {
  position: absolute;
  inset: 0;
  background: rgba(20, 20, 20, 0.25);
  opacity: 0;
  transition: opacity var(--pa-anim);
}
.site-menu__panel {
  position: absolute;
  inset: 0 auto 0 0;
  width: min(32rem, 100vw);
  background: var(--pa-bg);
  border-right: 1px solid rgba(0,0,0,0.08);
  padding: 1.5rem var(--pa-pad-x);
  display: flex;
  flex-direction: column;
  gap: 2rem;
  overflow-y: auto;
  transform: translateX(-100%);
  transition: transform var(--pa-anim);
}
.site-menu.is-open {
  pointer-events: auto;
}
.site-menu.is-open .site-menu__panel {
  transform: translateX(0);
}
.site-menu.is-open .site-menu__backdrop {
  opacity: 1;
}

.site-menu__close {
  position: absolute;
  top: 1rem;
  right: 1rem;
  background: transparent;
  border: 0;
  font-size: 1.5rem;
  line-height: 1;
  cursor: pointer;
  color: var(--pa-fg);
}

.site-menu__header {
  display: flex;
  flex-direction: row;
  align-items: flex-end;
  justify-content: space-between;
  gap: 1rem;
  margin-top: 1.5rem;
}

.site-menu__brand {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0;
  background: transparent;
  border: 0;
  padding: 0;
  cursor: pointer;
  font-family: var(--font-title, 'Montserrat Alternates', sans-serif);
  font-weight: 600;
  font-size: clamp(2rem, 5vw, 2.8rem);
  line-height: 0.84;
  letter-spacing: -0.01em;
  text-transform: lowercase;
  color: transparent;
  -webkit-text-stroke: 0.5pt var(--pa-fg, var(--color-dark));
  transition: opacity 150ms ease;
}
.site-menu__brand:hover {
  opacity: 0.6;
}

.site-menu__lang {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  font-family: var(--font-title, sans-serif);
  font-size: 0.85rem;
  padding-bottom: 0.2rem;
}
.site-menu__lang button {
  background: transparent;
  border: 0;
  padding: 0;
  cursor: pointer;
  color: var(--pa-muted);
  font: inherit;
  letter-spacing: 0.05em;
}
.site-menu__lang button[aria-pressed="true"] { color: var(--pa-fg); }
.site-menu__sep { color: var(--pa-muted); }

.site-menu__contact-box {
  background: #333;
  padding: 1.4rem 1rem;
}
.site-menu__contact-box a {
  color: #e0e0e0;
  text-decoration: none;
  font-family: var(--font-title, sans-serif);
  font-size: 0.85rem;
  letter-spacing: 0.03em;
  text-align: center;
  display: block;
}
.site-menu__contact-box a:hover {
  text-decoration: underline;
}
.site-menu__contact-reveal {
  background: none;
  border: 0;
  padding: 0;
  cursor: pointer;
  color: #e0e0e0;
  font-family: var(--font-title, sans-serif);
  font-size: 0.85rem;
  letter-spacing: 0.03em;
  text-align: center;
  display: block;
  width: 100%;
}
.site-menu__contact-reveal:hover,
.site-menu__contact-reveal:focus-visible {
  text-decoration: underline;
  outline: none;
}
.site-menu__contact-reveal.is-revealed {
  text-decoration: underline;
  user-select: text;
}
.site-menu__contact-status {
  margin-top: 0.35rem;
  font-size: 0.72rem;
  letter-spacing: 0.02em;
  text-align: center;
  color: #b0b0b0;
  opacity: 0;
  transform: translateY(0.25rem);
  pointer-events: none;
  transition: opacity 180ms ease, transform 180ms ease;
}
.site-menu__contact-status.is-visible {
  opacity: 1;
  transform: translateY(0);
}

.site-menu__quicklinks {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}
.site-menu__quicklink-row {
  display: flex;
  align-items: baseline;
  gap: 0.6rem;
}
.site-menu__quicklink {
  background: transparent;
  border: 0;
  padding: 0;
  cursor: pointer;
  color: var(--pa-fg);
  text-decoration: none;
  font-family: var(--font-body, 'Forum', serif);
  font-size: 1rem;
  text-transform: lowercase;
  text-align: left;
}
.site-menu__quicklink:hover { text-decoration: underline; }
.site-menu__newsletter-input {
  background: transparent;
  border: 0;
  border-bottom: 1px solid var(--pa-muted);
  padding: 0.1rem 0;
  font-family: var(--font-body, 'Forum', serif);
  font-size: 1rem;
  color: var(--pa-fg);
  outline: none;
  min-width: 0;
  width: 10rem;
}
.site-menu__newsletter-input::placeholder { color: var(--pa-muted); }
.site-menu__newsletter-input:focus { border-bottom-color: var(--pa-fg); }
.site-menu__newsletter-msg {
  display: block;
  font-family: var(--font-body, 'Forum', serif);
  font-size: 0.85rem;
  color: var(--pa-muted);
  margin-top: 0.3rem;
  max-width: 18rem;
}
.site-menu__newsletter-msg[data-state="error"] { color: inherit; }

.site-menu__index ul {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
}
.site-menu__index a {
  color: var(--pa-fg);
  text-decoration: none;
  font-size: 1rem;
  text-transform: lowercase;
}
.site-menu__index a:hover { text-decoration: underline; }

/* ---------------------------------------------------------------------------
   3D model panel — outline-only OBJ viewer, oversized canvas that paints
   underneath neighboring strip panels.
--------------------------------------------------------------------------- */
.portfolio-view--model3d {
  /* Allow the canvas to spill past the panel borders. */
  overflow: visible;
}
.portfolio-model3d {
  position: absolute;
  /* Oversize horizontally — extends past both left/right panel borders.
     200% means the canvas is twice as wide as the panel, centered. */
  top: 0;
  left: -50%;
  width: 200%;
  height: 100%;
  /* Tape Generator → Tool 3 (3D model size/position). */
  transform: translate(var(--tape-model3d-offset-x), var(--tape-model3d-offset-y))
             scale(var(--tape-model3d-scale));
  transform-origin: center center;
  pointer-events: none;
  /* Negative z so neighbor panels (z-index auto / 0) paint on top of the
     overflowing portions. The parent .portfolio-panel uses position:relative
     with z-index auto, which does NOT create a stacking context, so this
     descendant is composited against the panel's siblings. */
  z-index: -1;
  opacity: 0;
  transition: opacity 320ms ease;
}
.portfolio-model3d.is-loaded { opacity: 1; }
.portfolio-model3d__canvas {
  display: block;
  width: 100%;
  height: 100%;
  pointer-events: none; /* events go to the capture overlay, not the canvas */
}
/* Transparent capture overlay – sits at normal z-index inside the panel,
   above the z:-1 canvas, so it actually receives pointer events. */
.portfolio-model3d__capture {
  position: absolute;
  inset: 0;
  z-index: 1;
  cursor: grab;
}

/* Loading spinner — shown until the model file is parsed and rendered. */
@keyframes lz-model-spin {
  to { transform: translate(-50%, -50%) rotate(360deg); }
}
.portfolio-model3d__spinner {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 1.25rem;
  height: 1.25rem;
  border: 1.5px solid rgba(0, 0, 0, 0.15);
  border-top-color: #000;
  border-radius: 50%;
  transform: translate(-50%, -50%);
  animation: lz-model-spin 0.75s linear infinite;
  pointer-events: none;
  z-index: 2;
}

@media (max-width: 640px) {
  .portfolio-view {
    grid-template-columns: 1.25rem minmax(0, 1fr) 1.25rem;
  }
  .portfolio-app__hud { left: 1.25rem; font-size: 0.7rem; gap: 1rem; }
}

/* ---------------------------------------------------------------------------
   CV strip — horizontal panel strip to the right of the landing page.
   Each panel is quarter-width (25vw); the .portfolio-cv-inner stacks CV
   sections vertically inside the standard grid cell.
--------------------------------------------------------------------------- */

.portfolio-cv-inner {
  display: flex;
  flex-direction: column;
  gap: 2.5em;
  overflow-y: auto;
  /* Remove the default center-alignment so content starts from the top. */
  align-self: start;
  max-width: none;
}

/* Section block */
.cv-section {
  display: flex;
  flex-direction: column;
  gap: 0.55em;
}

.cv-section__heading {
  font-family: 'Montserrat Alternates', sans-serif;
  font-weight: 500;
  font-size: 0.65rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--pa-muted, #888);
  margin: 0 0 0.4em;
  line-height: 1.2;
}

/* Entry list */
.cv-section__entries {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 0;
}

/* Single entry: year left, text right, inline-flow */
.cv-entry {
  display: grid;
  grid-template-columns: 4.5em 1fr;
  column-gap: 0.6em;
  row-gap: 0;
  font-size: 0.78rem;
  line-height: 1.5;
  color: var(--color-dark, #111);
  padding: 0.1em 0;
}

.cv-entry__year {
  color: var(--pa-muted, #888);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  padding-top: 0.05em;
}

.cv-entry__text {
  /* Wraps naturally */
}

/* Publication detail lines (indented sub-list) */
.cv-entry__details {
  grid-column: 2;
  list-style: none;
  padding: 0.15em 0 0.3em;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 0;
  font-size: 0.72rem;
  color: var(--pa-muted, #888);
  line-height: 1.45;
}

.cv-entry__details li {
  padding: 0;
}

@media (max-width: 640px) {
  .cv-entry {
    grid-template-columns: 3.8em 1fr;
    font-size: 0.74rem;
  }
}

/* ---------------------------------------------------------------------------
   Info page (lightweight overview rendered in the landing-strip's
   half-width panel). Bio + selected CV + downloads + contact, stacked
   vertically inside .portfolio-cv-inner.
--------------------------------------------------------------------------- */

.info-page {
  /* Override the standard grid-cell placement:
     span all rows and columns so the scroll area runs edge-to-edge
     and the scrollbar sits flush at the right panel border. */
  grid-column: 1 / -1;
  grid-row: 1 / -1;
  align-self: stretch;
  /* Re-apply padding on left, top and bottom only — right is reserved for the scrollbar. */
  padding: var(--pa-pad-y) 0 var(--pa-pad-y) var(--pa-pad-x);
  /* Compact, calm column. Reuses portfolio-cv-inner for scroll behaviour. */
  gap: 2em;
}

.info-intro {
  font-size: 0.92rem;
  line-height: 1.55;
  color: var(--color-dark, #111);
  max-width: 38em;
}

.info-intro p { margin: 0; }

.info-selected-cv {
  display: flex;
  flex-direction: column;
  gap: 1.75em;
}

.info-links ul {
  list-style: none;
  padding: 0;
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: 0.25em;
  font-size: 0.82rem;
}

.info-links li:last-child {
  margin-top: 0.6em;
}

.info-links a {
  color: var(--color-dark, #111);
  text-decoration: none;
  border-bottom: 1px solid currentColor;
  padding-bottom: 1px;
}

.info-links a:hover {
  opacity: 0.65;
}

/* ---------------------------------------------------------------------------
   Landing — Current / Upcoming events section
--------------------------------------------------------------------------- */

.landing-current {
  position: absolute;
  left: var(--pa-pad-x);
  bottom: var(--pa-pad-y);
  max-width: 50vw;
  z-index: 2;
  pointer-events: none;
}

.landing-current__list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.6em;
}

.landing-current__item {
  display: flex;
  flex-direction: column;
  gap: 0;
}

.landing-current__line1 {
  font-family: 'Montserrat Alternates', sans-serif;
  font-weight: 600;
  font-size: 20pt;
  line-height: 1.15;
  letter-spacing: -0.01em;
  color: var(--color-dark);
}

.landing-current__line2 {
  font-family: 'Montserrat Alternates', sans-serif;
  font-weight: 600;
  font-size: 20pt;
  line-height: 1.15;
  letter-spacing: -0.01em;
  color: var(--color-dark);
}

.landing-current__block {
  display: flex;
  flex-direction: column;
  gap: 0;
  text-decoration: none;
  color: inherit;
}

a.landing-current__block:hover {
  text-decoration: underline;
  text-underline-offset: 0.15em;
}

@media (max-width: 640px) {
  .landing-current {
    max-width: calc(100vw - 2 * var(--pa-pad-x));
  }
}

/* Internal work cross-reference links (rendered via renderMarkdownLinks) */
.portfolio-work-ref {
  color: inherit;
  text-decoration: underline;
  text-underline-offset: 0.15em;
  cursor: pointer;
}
.portfolio-work-ref:not(.portfolio-work-ref--unavailable):hover {
  opacity: 0.65;
}
.portfolio-work-ref--unavailable {
  text-decoration: none;
  cursor: default;
}

/* ────────────────────────────────────────────────────────────────────── */
/* Touch-native scroll mode (smartphones / tablets)                         */
/*                                                                          */
/* When <html> has class .touch-native, the custom JS gesture engine is     */
/* bypassed. The viewport contains one empty placeholder slot per work      */
/* (so vertical scroll geometry stays full-length) and the JS runtime       */
/* lazily mounts the actual strip content for prev/current/next only.       */
/* Each strip scrolls horizontally natively. NO CSS scroll-snap — the user  */
/* experience is completely free native scroll: panels and works can stop   */
/* anywhere; browser provides momentum, rubber-band, all inertia.           */
/* Desktop styles (no .touch-native class) are unaffected.                  */
/* ────────────────────────────────────────────────────────────────────── */

html.touch-native,
html.touch-native body {
  height: 100%;
  overflow: hidden;
  overscroll-behavior: none;
}

html.touch-native [data-portfolio-viewport] {
  overflow-x: hidden;
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-y: contain;
  touch-action: pan-y;
  scrollbar-width: none;
}
html.touch-native [data-portfolio-viewport]::-webkit-scrollbar { display: none; }

/* One placeholder slot per work (+ landing). Always present so vertical
   scroll position maps deterministically to work index. The actual strip
   content is appended into the slot only when the slot is within the
   mount window (current ± 1).

   Height: --tape-touch-vh is set in JS from window.visualViewport.height so
   it matches the ACTUALLY VISIBLE browser viewport (i.e. excludes the area
   behind iOS Safari / Chrome mobile address bar + bottom UI). Fallback
   100dvh is the spec equivalent for browsers without the JS hook running
   yet. Plain 100vh is intentionally avoided here: on mobile it expands to
   the largest possible viewport, which on iOS Safari pushes slot bottoms
   under the browser chrome. */
html.touch-native .portfolio-tn-slot {
  position: relative;
  width: 100vw;
  height: var(--tape-touch-vh, 100dvh);
  min-height: var(--tape-touch-vh, 100dvh);
  background: var(--pa-bg);
}
html.touch-native .portfolio-tn-slot.portfolio-tn-slot--landing {
  background: #ffffff;
}

/* When a strip is mounted inside a slot, it fills the slot and becomes
   its own native horizontal scroll container. No scroll-snap. */
html.touch-native .portfolio-tn-slot > .portfolio-strip,
html.touch-native .portfolio-tn-slot > .portfolio-landing-strip {
  position: absolute;
  inset: 0;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: flex;
  overflow-x: auto;
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  overscroll-behavior-x: contain;
  /* Prevent any vertical chain back to the viewport when an almost-horizontal
     swipe carries a small vertical component (iOS) — keeps strip-internal
     gestures from yanking the page back toward landing. */
  overscroll-behavior-y: contain;
  touch-action: pan-x pan-y;
  /* Defeat any leftover inline transforms from the custom engine. */
  transform: none !important;
  filter: none !important;
  will-change: scroll-position;
  scrollbar-width: none;
}
html.touch-native .portfolio-tn-slot > .portfolio-strip::-webkit-scrollbar,
html.touch-native .portfolio-tn-slot > .portfolio-landing-strip::-webkit-scrollbar { display: none; }

/* The custom engine sets is-exiting on strips for its slide-out animation;
   in native mode strips never slide, so neutralise it. */
html.touch-native .portfolio-strip.is-exiting,
html.touch-native .portfolio-landing-strip.is-exiting {
  display: flex;
}

/* Mounted strip sits above any slot background (z-index:0) when present. */
html.touch-native .portfolio-tn-slot > .portfolio-strip,
html.touch-native .portfolio-tn-slot > .portfolio-landing-strip {
  z-index: 1;
}

/* ===========================================================================
   Tape Generator — Phase 1 additions
   ---------------------------------------------------------------------------
   These rules complete what the tape-* CSS variables (declared in the
   portfolio :root block above) control. Defined at the end of the file so
   they win over earlier rules without resorting to !important.
=========================================================================== */

/* Standalone text columns — text top-aligned (per spec), with horizontal
   padding controlled by Tape Generator → Tool 2. Excludes:
   - overlay-text variants (positioned absolutely inside an image panel)
   - overlay-card corner variants (also absolutely positioned)
   - cover / landing / image / model3d / quote views (own inner layout) */
.portfolio-view--short_text:not(.is-overlay-text) .portfolio-view__inner,
.portfolio-view--long_text_segment:not(.is-overlay-card-top):not(.is-overlay-card-bottom):not(.is-overlay-card-left):not(.is-overlay-card-right):not(.is-overlay-corner-top-left):not(.is-overlay-corner-top-right):not(.is-overlay-corner-bottom-left):not(.is-overlay-corner-bottom-right) .portfolio-view__inner {
  /* Span the full panel width so Tool 2 controls the *total* horizontal
     inset (not just the gap inside the default --pa-pad-x grid gutter). */
  grid-column: 1 / -1;
  grid-row: 2;
  justify-self: stretch;
  max-width: none;
  align-self: start;
  padding-top: var(--tape-text-top-offset);
  padding-left: var(--tape-text-col-pad-x);
  padding-right: var(--tape-text-col-pad-x);
}

/* ── Overlay card — corner-aware variants ──────────────────────────────── */
.portfolio-panel.is-overlay-corner-top-left,
.portfolio-panel.is-overlay-corner-top-right,
.portfolio-panel.is-overlay-corner-bottom-left,
.portfolio-panel.is-overlay-corner-bottom-right {
  position: relative;
  display: block;
  overflow: hidden;
}
.is-overlay-corner-top-left    .portfolio-overlay-card,
.is-overlay-corner-top-right   .portfolio-overlay-card,
.is-overlay-corner-bottom-left .portfolio-overlay-card,
.is-overlay-corner-bottom-right .portfolio-overlay-card {
  /* Reset legacy edge-to-edge stretching. */
  left: auto;
  right: auto;
  top: auto;
  bottom: auto;
  width: auto;
  max-width: var(--tape-overlay-card-max-w);
}
.is-overlay-corner-top-left    .portfolio-overlay-card {
  top: var(--tape-overlay-card-inset);
  left: var(--tape-overlay-card-inset);
}
.is-overlay-corner-top-right   .portfolio-overlay-card {
  top: var(--tape-overlay-card-inset);
  right: var(--tape-overlay-card-inset);
}
.is-overlay-corner-bottom-left .portfolio-overlay-card {
  bottom: var(--tape-overlay-card-inset);
  left: var(--tape-overlay-card-inset);
}
.is-overlay-corner-bottom-right .portfolio-overlay-card {
  bottom: var(--tape-overlay-card-inset);
  right: var(--tape-overlay-card-inset);
}
.is-overlay-corner-top-left    .portfolio-overlay-card .portfolio-long-text,
.is-overlay-corner-top-right   .portfolio-overlay-card .portfolio-long-text,
.is-overlay-corner-bottom-left .portfolio-overlay-card .portfolio-long-text,
.is-overlay-corner-bottom-right .portfolio-overlay-card .portfolio-long-text {
  max-width: none;
}

/* ===========================================================================
   Tape Generator Settings panel (overlay UI) — mirrors .puffy-panel skeleton.
=========================================================================== */
.tape-panel {
  position: fixed;
  right: clamp(0.75rem, 1.5vw, 1.5rem);
  top: clamp(0.75rem, 1.5vh, 1.5rem);
  z-index: 9999;
  width: clamp(18rem, 26vw, 24rem);
  max-height: calc(100vh - 3rem);
  background: rgba(255, 255, 255, 0.97);
  color: var(--color-dark);
  border: 1px solid rgba(0, 0, 0, 0.1);
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
  border-radius: 6px;
  font-family: var(--font-body, "Forum", serif);
  font-size: 13px;
  line-height: 1.3;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
.tape-panel__header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  padding: 0.5rem 0.75rem;
  border-bottom: 1px solid rgba(0, 0, 0, 0.08);
  background: rgba(0, 0, 0, 0.025);
}
.tape-panel__title {
  font-family: var(--font-title, sans-serif);
  font-size: 11px;
  letter-spacing: 0.06em;
  text-transform: lowercase;
  color: var(--pa-muted);
}
.tape-panel__close {
  appearance: none;
  border: 0;
  background: transparent;
  font: inherit;
  font-size: 14px;
  line-height: 1;
  cursor: pointer;
  color: var(--pa-muted);
  padding: 0.15rem 0.4rem;
}
.tape-panel__body {
  padding: 0.75rem 0.75rem 0.5rem;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
.tape-panel__group {
  border-top: 1px dashed rgba(0, 0, 0, 0.1);
  padding-top: 0.5rem;
}
.tape-panel__group:first-child { border-top: 0; padding-top: 0; }
.tape-panel__group-title {
  font-family: var(--font-title, sans-serif);
  font-size: 10px;
  letter-spacing: 0.07em;
  text-transform: lowercase;
  color: var(--pa-muted);
  margin: 0 0 0.35rem;
}
/* Optional helper hint shown under a group title (e.g. Tool 7 explainer). */
.tape-panel__group-hint {
  font-size: 10px;
  color: var(--pa-muted);
  margin: 0 0 0.45rem;
  line-height: 1.35;
  opacity: 0.85;
}
/* ── Image-presets repeatable list ─────────────────────────────────── */
.tape-panel__preset-head,
.tape-panel__preset-row {
  display: grid;
  grid-template-columns: 1.4fr 0.8fr 1.1fr 0.6fr auto;
  gap: 0.3rem;
  align-items: center;
}
.tape-panel__preset-head {
  font-size: 9px;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: var(--pa-muted);
  padding: 0 0 0.2rem;
}
.tape-panel__preset-row { padding: 0.15rem 0; }
.tape-panel__preset-row select,
.tape-panel__preset-row input[type="number"] {
  font: inherit;
  font-size: 11px;
  padding: 0.2rem 0.35rem;
  border: 1px solid rgba(0, 0, 0, 0.18);
  background: #fff;
  border-radius: 3px;
  width: 100%;
  min-width: 0;
}
.tape-panel__preset-row [data-preset-action="remove"] {
  appearance: none;
  border: 1px solid rgba(0, 0, 0, 0.18);
  background: #fff;
  width: 1.4rem;
  height: 1.4rem;
  border-radius: 3px;
  cursor: pointer;
  font-size: 12px;
  line-height: 1;
  color: var(--pa-muted);
}
.tape-panel__preset-row [data-preset-action="remove"]:hover {
  color: #b00020;
  border-color: #b00020;
}
.tape-panel__preset-actions { margin-top: 0.35rem; }
.tape-panel__preset-actions button {
  appearance: none;
  background: #fff;
  border: 1px dashed rgba(0, 0, 0, 0.25);
  border-radius: 3px;
  font: inherit;
  font-size: 11px;
  padding: 0.25rem 0.5rem;
  cursor: pointer;
  color: var(--color-dark);
}
.tape-panel__preset-actions button:hover { border-style: solid; }
.tape-panel__row {
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: center;
  gap: 0.4rem;
  padding: 0.15rem 0;
}
/* Range rows: label | live readout | full-width slider on its own line. */
.tape-panel__row--range {
  grid-template-columns: 1fr auto;
  grid-template-areas:
    "label readout"
    "slider slider";
  row-gap: 0.2rem;
}
.tape-panel__row--range > label   { grid-area: label; }
.tape-panel__row--range > .tape-panel__readout { grid-area: readout; }
.tape-panel__row--range > input[type="range"]  { grid-area: slider; width: 100%; }
.tape-panel__readout {
  font-family: ui-monospace, "SF Mono", Menlo, monospace;
  font-size: 10px;
  color: var(--pa-muted);
  text-align: right;
  min-width: 4em;
}
.tape-panel__row label {
  font-size: 11px;
  color: var(--color-dark);
}
.tape-panel__row input[type="text"],
.tape-panel__row input[type="number"],
.tape-panel__row select {
  font: inherit;
  font-size: 11px;
  padding: 0.2rem 0.35rem;
  border: 1px solid rgba(0, 0, 0, 0.18);
  background: #fff;
  border-radius: 3px;
  width: 9.5rem;
}
.tape-panel__row input[type="range"] {
  -webkit-appearance: none;
  appearance: none;
  height: 3px;
  background: rgba(0, 0, 0, 0.18);
  border-radius: 3px;
  outline: none;
  margin: 0.1rem 0;
}
.tape-panel__row input[type="range"]::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--color-dark);
  border: 0;
  cursor: pointer;
}
.tape-panel__row input[type="range"]::-moz-range-thumb {
  width: 12px;
  height: 12px;
  border-radius: 50%;
  background: var(--color-dark);
  border: 0;
  cursor: pointer;
}
.tape-panel__row input[type="checkbox"] {
  justify-self: end;
}

/* 2\u00d73 radio grid used for Tape Generator \u2192 Tool 4 (Credit position).
   Six small buttons laid out as Top row / Bottom row; the checked one
   highlights so admins can see at a glance which corner is picked. */
.tape-panel__row--radio-grid > label { align-self: center; }
.tape-panel__radio-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 3px;
  padding: 3px;
  background: rgba(0, 0, 0, 0.04);
  border-radius: 4px;
}
.tape-panel__radio-cell {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 4px 0;
  font-size: 10px;
  letter-spacing: 0.04em;
  color: var(--color-dark);
  background: #fff;
  border: 1px solid rgba(0, 0, 0, 0.15);
  border-radius: 3px;
  cursor: pointer;
  user-select: none;
}
.tape-panel__radio-cell input { position: absolute; opacity: 0; pointer-events: none; }
.tape-panel__radio-cell:has(input:checked) {
  background: var(--color-dark);
  color: var(--color-light);
  border-color: var(--color-dark);
}
.tape-panel__actions {
  display: flex;
  gap: 0.4rem;
  padding: 0.5rem 0.75rem;
  border-top: 1px solid rgba(0, 0, 0, 0.08);
  background: rgba(0, 0, 0, 0.025);
}
.tape-panel__actions button {
  appearance: none;
  font: inherit;
  font-size: 11px;
  padding: 0.35rem 0.6rem;
  border: 1px solid rgba(0, 0, 0, 0.2);
  background: #fff;
  color: var(--color-dark);
  border-radius: 3px;
  cursor: pointer;
}
.tape-panel__actions button.is-primary {
  background: var(--color-dark);
  color: var(--color-light);
  border-color: var(--color-dark);
}
.tape-panel__status {
  font-size: 10px;
  color: var(--pa-muted);
  padding: 0 0.75rem 0.5rem;
  min-height: 1em;
}

/* ────────────────────────────────────────────────────────────────────────
 * Phase 2 of Tape Generator Rules — Responsive layer
 *
 * Three modes are toggled by JS as classes on <html>:
 *   .touch-native       — coarse pointer, no fine pointer (existing)
 *   .is-tablet          — touch-native AND viewport > 640px
 *   .is-mobile-narrow   — viewport <= 640px (typically also touch-native)
 *
 * Tablet inherits desktop layout but allows targeted overrides via the
 * "Tablet Tape Generator" admin tab (e.g. landing-info width).
 *
 * Smartphone (.is-mobile-narrow) collapses every non-image panel to 100vw.
 * Image panels can opt-in to 200vw via the .portfolio-panel--mobile-double-full
 * width class, which the generator emits from the mobile image-preset list.
 * ──────────────────────────────────────────────────────────────────────── */

/* ── Tablet ─────────────────────────────────────────────────────────────
 * Landing-info panel widening. Default keeps current third-vw width; the
 * Tablet Tape Generator admin tab overrides --tape-tablet-landing-info-width.
 */
html.is-tablet {
  --tape-tablet-landing-info-width: calc(100vw / 3);
}
html.is-tablet .portfolio-view--landing-info {
  width: var(--tape-tablet-landing-info-width);
}

/* ── Smartphone (narrow) ────────────────────────────────────────────────
 * Width normalization. Every standard width class collapses to 100vw so
 * non-image panels never appear as narrow columns on a phone. Image panels
 * may opt-in to .portfolio-panel--mobile-double-full (200vw) — that class
 * is emitted by pickMobileImagePreset() in the generator.
 */
html.is-mobile-narrow .portfolio-panel--full,
html.is-mobile-narrow .portfolio-panel--half,
html.is-mobile-narrow .portfolio-panel--third,
html.is-mobile-narrow .portfolio-panel--quarter,
html.is-mobile-narrow .portfolio-panel--two-thirds {
  width: 100vw;
  min-width: 100vw;
  max-width: 100vw;
  flex-basis: 100vw;
}

.portfolio-panel--mobile-double-full {
  width: 200vw;
}
html.is-mobile-narrow .portfolio-panel--mobile-double-full {
  width: 200vw;
  min-width: 200vw;
  max-width: 200vw;
  flex-basis: 200vw;
}

/* Landing info on smartphone fills the viewport too. */
html.is-mobile-narrow .portfolio-view--landing-info {
  width: 100vw;
  min-width: 100vw;
  max-width: 100vw;
}

/* Mobile CSS-variable overrides driven by the Mobile Tape Generator tab.
 * These mirror a subset of the desktop variables; they only apply inside
 * .is-mobile-narrow, so desktop and tablet keep the values written by
 * applyTapeSettingsToCss().
 */
html.is-mobile-narrow {
  /* Sensible defaults. Each variable can be re-written by JS from
   * window.__TAPE_MOBILE_SETTINGS__ via applyMobileTapeSettingsToCss(). */
  --tape-text-top-offset:  var(--tape-mobile-text-top-offset,  18vh);
  --tape-text-col-pad-x:   var(--tape-mobile-text-col-pad-x,   1.25rem);
  --tape-floating-img-inset: var(--tape-mobile-floating-img-inset, 2.5rem);
}

/* -------------------------------------------------------------------------- */
/* Floating media player (Vimeo + audio)                                      */
/* -------------------------------------------------------------------------- */
/* One global element appended to <body>. Hidden by default; reveals as a
 * compact pill once the user moves past the cover panel of a work that has
 * one or more media items. For Vimeo items, the main button expands a 16:9
 * panel with a lazily-mounted iframe (created on open, removed on close so
 * playback fully stops). For audio items, the main button toggles play/pause
 * on an HTMLAudioElement (no native controls, no panel). The ">>" button
 * cycles to the next item; it is hidden when only one item exists. */
.floating-media {
  position: fixed;
  left: 1.75rem;
  right: auto;
  bottom: calc(env(safe-area-inset-bottom, 0px) + 1.75rem);
  z-index: 80;
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--pa-anim);
  font-family: var(--font-body);
  color: var(--pa-fg);
}

.floating-media.is-visible {
  opacity: 1;
  pointer-events: auto;
}

.floating-media__compact {
  display: inline-flex;
  align-items: stretch;
  gap: 0.35rem;
  /* Sit ABOVE the absolutely-positioned __panel so during the closing morph
   * the compact buttons can fade in WHILE the panel is still shrinking. This
   * is what makes the close feel like one continuous morph (panel becomes
   * button) rather than two sequential states (panel hides, button shows). */
  position: relative;
  z-index: 1;
}

/* Drag handle — subtle vertical grip on the left of the compact bar. Only
 * pointerdown on this element starts a drag; pointer/mouse drag is then
 * captured for the entire gesture. The drag handle translates upward to
 * align with the vertical center of the open Vimeo panel via JS-set
 * `--floating-media-drag-y` custom property. */
.floating-media__drag {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0 0.45rem;
  min-width: 1.4rem;
  min-height: 2rem;
  border: 1px solid var(--pa-fg);
  border-radius: 4px;
  background: var(--pa-bg);
  color: var(--pa-fg);
  cursor: grab;
  touch-action: none;
  transition: background var(--pa-anim), color var(--pa-anim);
  -webkit-appearance: none;
  appearance: none;
}

.floating-media__drag:hover,
.floating-media__drag:focus-visible {
  background: var(--pa-fg);
  color: var(--color-light);
  outline: none;
}

.floating-media__drag-grip {
  display: inline-block;
  width: 4px;
  height: 14px;
  background-image: radial-gradient(currentColor 1px, transparent 1.4px);
  background-size: 4px 4px;
  background-position: center;
  background-repeat: repeat-y;
}

.floating-media.is-dragging {
  cursor: grabbing;
}
.floating-media.is-dragging .floating-media__drag {
  cursor: grabbing;
}
body.floating-media-dragging,
body.floating-media-dragging * {
  user-select: none !important;
  -webkit-user-select: none !important;
}

/* True crossfade for compact-bar buttons: a `::before` pseudo holds the
 * inverted (hover) background, fading in on hover/focus over the base. Text
 * color also transitions on a matched timing. */
.floating-media__main,
.floating-media__next {
  display: inline-flex;
  align-items: center;
  gap: 0.5em;
  min-height: 2rem;
  padding: 0.35rem 0.85rem;
  border: 1px solid var(--pa-fg);
  border-radius: 4px;
  background: var(--pa-bg);
  color: var(--pa-fg);
  font: inherit;
  font-size: 0.95rem;
  line-height: 1;
  letter-spacing: 0.01em;
  cursor: pointer;
  position: relative;
  isolation: isolate;
  overflow: hidden;
  /* Color transitions on hover; opacity is intentionally NOT transitioned
   * so the button snap-appears/snap-disappears together with the panel
   * morph, making the open/close feel like a single element transforming. */
  transition: color var(--pa-anim);
  -webkit-appearance: none;
  appearance: none;
}

.floating-media__main::before,
.floating-media__next::before {
  content: "";
  position: absolute;
  inset: 0;
  background: var(--pa-fg);
  opacity: 0;
  z-index: -1;
  transition: opacity var(--pa-anim);
  pointer-events: none;
}

.floating-media__main:hover::before,
.floating-media__main:focus-visible::before,
.floating-media__next:hover::before,
.floating-media__next:focus-visible::before {
  opacity: 1;
}

.floating-media__main:hover,
.floating-media__main:focus-visible,
.floating-media__next:hover,
.floating-media__next:focus-visible {
  color: var(--color-light);
  outline: none;
}

.floating-media__next {
  padding: 0.35rem 0.55rem;
  min-width: 2rem;
  justify-content: center;
}

/* Icons rendered as CSS masks so they inherit currentColor and invert on
 * hover/focus together with the button text. */
.floating-media__icon {
  display: inline-block;
  line-height: 1;
}

.floating-media__icon--play,
.floating-media__icon--skip,
.floating-media__icon--stop {
  width: 1em;
  height: 1em;
  background-color: currentColor;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-position: center;
          mask-position: center;
  -webkit-mask-size: contain;
          mask-size: contain;
}

.floating-media__icon--play {
  -webkit-mask-image: url('/assets/media-play.svg');
          mask-image: url('/assets/media-play.svg');
}

.floating-media__icon--skip {
  -webkit-mask-image: url('/assets/media-skip.svg');
          mask-image: url('/assets/media-skip.svg');
  width: 1.05em;
  height: 1.05em;
}

.floating-media__icon--stop {
  -webkit-mask-image: url('/assets/media-stop.svg');
          mask-image: url('/assets/media-stop.svg');
  display: none;
}

.floating-media.is-playing .floating-media__icon--play {
  display: none;
}

.floating-media.is-playing .floating-media__icon--stop {
  display: inline-block;
}

.floating-media__label {
  display: inline-block;
}

/* While the Vimeo panel is open, the compact bar's main + skip buttons are
 * hidden. During the closing morph the same buttons fade BACK IN with
 * staggered delays (see rules near the end of this section), so they
 * re-emerge inside the shrinking panel — making the close feel like one
 * continuous morph. */
.floating-media.is-open .floating-media__main,
.floating-media.is-open .floating-media__next {
  opacity: 0;
  pointer-events: none;
}

/* Drag handle moves up to align with the vertical center of the open panel.
 * JS sets the inline transform; transitions provide the easing. */
.floating-media__drag {
  transition:
    transform var(--pa-anim),
    background var(--pa-anim),
    color var(--pa-anim);
}

/* Panel morphs out of the media button's position: starts at the button's
 * exact box (set via inline width/height/left) and grows to its target size.
 * JS orchestrates the geometry directly; CSS only supplies the transition.
 */
.floating-media__panel {
  position: absolute;
  bottom: 0;
  left: 0;
  width: 0;
  height: 0;
  background: var(--pa-bg);
  border: 1px solid var(--pa-fg);
  border-radius: 4px;
  overflow: hidden;
  opacity: 0;
  pointer-events: none;
  background-color: var(--pa-fg); /* fallback for morph, will animate */
  transition:
    width  var(--pa-anim),
    height var(--pa-anim),
    left   var(--pa-anim),
    opacity var(--pa-anim),
    background-color var(--pa-anim);
}

.floating-media.is-open .floating-media__panel {
    background-color: var(--pa-fg); /* dark */
  opacity: 1;
  pointer-events: auto;
}

/* Closing morph: panel keeps full opacity all the way until the geometry
 * has reached the button's box. This way the user perceives one shape
 * shrinking, not a vimeo panel fading out plus a button fading in. */
.floating-media.is-closing .floating-media__panel {
    background-color: var(--pa-btn-bg); /* morph to bright */
  opacity: 1;
  pointer-events: none;
}

/* During an interactive resize, disable transitions on the panel + drag
 * handle so the geometry tracks pointer motion 1:1. */
.floating-media.is-resizing .floating-media__panel {
  transition: none;
}
.floating-media.is-resizing .floating-media__drag {
  transition: background var(--pa-anim), color var(--pa-anim);
}

.floating-media__frame {
  width: 100%;
  height: 100%;
  background: #000;
  /* During the closing morph the iframe contents are destroyed; the frame's
   * own black background is faded out fast so the panel's bright bg-color
   * morph (dark → var(--pa-btn-bg)) reads through cleanly. */
  transition: opacity 140ms ease-out;
}
.floating-media.is-closing .floating-media__frame {
  opacity: 0;
}

.floating-media__frame iframe {
  width: 100%;
  height: 100%;
  border: 0;
  display: block;
}

/* Resize handle in the panel's top-right corner. Hidden until the panel is
 * fully open (avoids covering the morph animation). nesw-resize cursor on
 * hover (the diagonal arrow oriented along the top-right ↔ bottom-left axis,
 * matching the corner being grabbed). */
.floating-media__resize {
  position: absolute;
  top: 0;
  right: 0;
  width: 18px;
  height: 18px;
  z-index: 2;
  cursor: nesw-resize;
  touch-action: none;
  background: transparent;
  border: 0;
  padding: 0;
  opacity: 0;
  transition: opacity var(--pa-anim);
}

/* Subtle diagonal-arrow glyph rendered with CSS lines so we don't require an
 * extra asset. Two short strokes form a NE-SW arrow. */
.floating-media__resize::before,
.floating-media__resize::after {
  content: "";
  position: absolute;
  background: var(--pa-fg);
}
.floating-media__resize::before {
  /* short diagonal line oriented top-right ↔ bottom-left (outer edge) */
  top: 5px;
  right: 3px;
  width: 8px;
  height: 1px;
  transform-origin: 100% 50%;
  transform: rotate(45deg);
}
.floating-media__resize::after {
  /* second parallel diagonal line slightly offset */
  top: 10px;
  right: 8px;
  width: 5px;
  height: 1px;
  transform-origin: 100% 50%;
  transform: rotate(45deg);
}

.floating-media.is-open .floating-media__resize {
  opacity: 0.6;
}
.floating-media.is-open .floating-media__resize:hover {
  opacity: 1;
}

/* Close button overlaid in the panel's top-left corner. */
.floating-media__close {
  position: absolute;
  top: 0.35rem;
  left: 0.35rem;
  z-index: 1;
  width: 1.75rem;
  height: 1.75rem;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--pa-fg);
  border-radius: 4px;
  background: var(--pa-bg);
  color: var(--pa-fg);
  font: inherit;
  font-size: 1.1rem;
  line-height: 1;
  cursor: pointer;
  transition: background var(--pa-anim), color var(--pa-anim), opacity var(--pa-anim);
  -webkit-appearance: none;
  appearance: none;
}
/* Close ✕ fades out immediately at the start of the collapse. */
.floating-media.is-closing .floating-media__close {
  opacity: 0;
  pointer-events: none;
  transition: opacity 120ms ease-out 0ms;
}

/* ── Close morph: compact contents overlap the shrinking panel ─────────────
 *
 * The compact bar (.floating-media__compact) is layered ABOVE the panel via
 * z-index (set earlier in this file). During the close morph we therefore
 * fade the compact's contents back IN over a delayed window so they reveal
 * inside the still-shrinking, dark→bright panel — making the close read
 * as one continuous shape transformation instead of two sequential states.
 *
 * Timeline (close, total ~580ms — JS timeout matches):
 *   t=0      add .is-closing; remove .is-open
 *            - close ✕         : 1 → 0      (120ms ease-out)
 *            - iframe frame    : 1 → 0      (140ms ease-out)
 *            - panel bg        : dark → bright (~420ms via --pa-anim)
 *            - panel geometry  : open size → button box (~420ms)
 *   t=220    skip (__next) begins fading 0 → 1 (220ms) — peeks out
 *            from behind the still-collapsing panel.
 *   t=420    panel has reached compact-button size.
 *            play-icon + label + main shell begin fading 0 → 1 (140ms).
 *   t=560    play-icon + label + main shell fully visible.
 *   t=580    JS removes .is-closing; panel snaps to width/height 0
 *            without a visible flash — compact button is already fully
 *            visible and in place.
 */
.floating-media__main {
  transition:
    color   var(--pa-anim),
    opacity 220ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
.floating-media__next {
  transition:
    color   var(--pa-anim),
    opacity 220ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
.floating-media.is-closing .floating-media__main {
  opacity: 1;
  pointer-events: none;
  transition:
    color   var(--pa-anim),
    opacity 140ms cubic-bezier(0.22, 0.61, 0.36, 1) 420ms;
}
.floating-media.is-closing .floating-media__next {
  opacity: 1;
  pointer-events: none;
  transition:
    color   var(--pa-anim),
    opacity 220ms cubic-bezier(0.22, 0.61, 0.36, 1) 220ms;
}

.floating-media__label {
  opacity: 1;
  transition: opacity 220ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
.floating-media__icon--play {
  transition: opacity 220ms cubic-bezier(0.22, 0.61, 0.36, 1);
}
.floating-media.is-open .floating-media__label,
.floating-media.is-open .floating-media__icon--play {
  opacity: 0;
  transition: opacity 120ms ease-out 0ms;
}
.floating-media.is-closing .floating-media__label,
.floating-media.is-closing .floating-media__icon--play {
  opacity: 1;
  transition: opacity 140ms cubic-bezier(0.22, 0.61, 0.36, 1) 420ms;
}

/* Smooth GPU compositing for the root's opacity fade-in/out and for the
 * panel/drag handle morph geometry. Positioning math is unchanged — left/top
 * remain authoritative; these are only compositor hints. */
.floating-media {
  will-change: opacity;
}
.floating-media__panel,
.floating-media__drag {
  will-change: transform, width, height;
}
