/* Reset and base styles */
:root {
  /* Shared colors */
  --vusion-blue: #c2dde5;
  --vusion-blue-bright: #d1f6ff;
  --vusion-blue-glow: rgba(194, 221, 229, 0.4);
  --accent-color: #c2dde5; /* Main accent color */

  /* Dark mode (default) */
  --bg-color: #080808;
  --bg-gradient: radial-gradient(
    circle at 50% 50%,
    #141414 0%,
    #080808 70%,
    #050505 100%
  );
  --text-color: #ffffff;
  --text-color-secondary: rgba(255, 255, 255, 0.9);
  --dark-bg: rgba(15, 15, 15, 0.7);
  --darker-bg: rgba(25, 25, 25, 0.9);
  --light-accent: rgba(255, 255, 255, 0.85);
  --border-light: rgba(255, 255, 255, 0.15);
  --border-accent: rgba(194, 221, 229, 0.4);
  --text-shadow: 0 0 25px rgba(194, 221, 229, 0.3);
  --card-glow: 0 5px 30px rgba(194, 221, 229, 0.15);
  --card-bg: rgba(20, 20, 20, 0.7);
  --card-bg-gradient: linear-gradient(
    125deg,
    rgba(30, 30, 30, 0.4) 0%,
    rgba(15, 15, 15, 0.7) 70%
  );
  --input-bg: rgba(25, 25, 25, 0.7);
  --input-border: #333;
  --overlay-opacity: 0.03;
  --section-image-opacity: 0.03;
  --background-blend: screen;

  /* ===== Motion =====
     Three easings because there are exactly three intentions. Bare `ease` stays
     on colour crossfades — the curve is imperceptible there, and a token for it
     would be a token for its own sake. */
  --ease-out: cubic-bezier(0.23, 1, 0.32, 1);  /* committed brand curve — all UI */
  --ease-cine: cubic-bezier(0.16, 1, 0.3, 1);  /* longer tail — film-strip settle only */

  --dur-press: 150ms;
  --dur-base: 250ms;
  --dur-slow: 300ms;

  --dur-entrance: 640ms; /* hero credits */
  --dur-title: 900ms;    /* the name resolving */
  --dur-rule: 700ms;
  /* --dur-dissolve / --dur-settle removed 2026-09-27: the hero frame no longer
     animates (it is at opacity 1, scale 1 from first paint). */
  --dur-ambient: 2600ms; /* chevron drift loop */

  --stagger: 90ms;       /* the hero's entire timing derives from this */

  /* One list, many call sites. The .cta-btn:before duplicate-declaration bug
     came from pasting this by hand. */
  --transition-ui:
    transform var(--dur-base) var(--ease-out),
    color var(--dur-base) ease,
    background-color var(--dur-base) ease,
    border-color var(--dur-base) ease,
    box-shadow var(--dur-base) ease,
    opacity var(--dur-base) ease;

  /* ===== Measure =====
     ONE left edge for the whole page. There used to be six competing ones
     (1200/1100/1000 across .container, .about-grid, .journey-rows,
     .reel-feature, .portfolio-links, .connect-grid) — inside #portfolio the
     reel and the cards beneath it were 68px apart. A left-aligned page whose
     left edge moves is worse than a centred one: centring at least commits.
     1100 because About is the approved reference and must not move. */
  --measure: 1100px;
  --gutter: clamp(1.25rem, 4vw, 2rem);
}






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

/* Native anchor scrolling. Declarative reduced-motion (no JS branch to forget),
   and scroll-margin-top replaces a hardcoded offset — so deep links like
   /#about no longer land underneath the fixed nav. */
@media (prefers-reduced-motion: no-preference) {
  html {
    scroll-behavior: smooth;
  }
}

section[id],
#main-content {
  scroll-margin-top: 80px;
}

body {
  background-color: var(--bg-color);
  background-image: var(--bg-gradient);
  color: var(--text-color);
  font-family: "Onest", sans-serif;
  line-height: 1.5;
  position: relative;
}

/* Add subtle texture overlay */
body::before {
  content: "";
  position: fixed;
  width: 100%;
  height: 100%;
  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 250 250' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
  opacity: var(--overlay-opacity);
  pointer-events: none;
  z-index: 1000;
}

/* Utility classes */
.container {
  width: 100%;
  max-width: var(--measure);
  margin: 0 auto;
  /* Was `padding: 0`, and nothing else supplied a gutter — body copy rendered
     flush against the bezel at 390px. */
  padding-inline: var(--gutter);
}

.section {
  padding: clamp(4rem, 10vw, 8rem) 0;
  position: relative;
}

/* Typography.
   Left, not centre. The page has ONE left axis — nav logo, hero name, every
   heading and all body copy share it. Centred headings over left-aligned
   content was the page's loudest tell: two competing axes reads as undesigned
   no matter how good the individual pieces are. */
h1,
h2,
h3,
h4 {
  font-weight: 500;
  letter-spacing: -0.5px;
  text-align: left;
}

h1 {
  font-size: clamp(2.75rem, 7.5vw, 5.5rem);
  line-height: 1.2;
  font-family: "Onest", sans-serif;
  letter-spacing: 2px;
  font-weight: 400;
}

/* Specific styling for hero section title */
.hero h1 {
  position: relative;
  display: inline-block;
  padding: 0 0 12px;
  margin-bottom: 10px;
  letter-spacing: 3px;
  text-align: left;
  /* Over a full-bleed frame the old 0 2px 4px/0.2 shadow was far too weak. */
  text-shadow: 0 1px 24px rgba(0, 0, 0, 0.65), 0 1px 3px rgba(0, 0, 0, 0.5);
}

/* Left-anchored accent rule under the name. transform-origin (not transform)
   sets the draw direction, leaving transform free for the entrance animation —
   never use transform for layout if you also want it for motion. */
.hero h1::after {
  content: "";
  position: absolute;
  bottom: 0;
  left: 0;
  width: 64px;
  height: 1px;
  background-color: var(--vusion-blue);
  transform-origin: left;
}

/* No --text-shadow glow on headings. A 25px blue bloom at 30% behind white
   text isn't light, it's fuzz — it softens Onest's already-gentle terminals
   and spreads the accent into a large soft field, the one thing PRODUCT.md
   reserves it against. The accent does precise work in the 1px rules instead.
   (The hero h1 keeps its shadow: that one is black and buys contrast over a
   photograph, which is a job, not decoration.) */
h2 {
  font-size: clamp(2rem, 5vw, 2.5rem);
  margin-bottom: clamp(2rem, 3.5vw, 3rem);
  letter-spacing: 1px;
  font-weight: 400;
}

h3 {
  margin-bottom: 20px;
  color: var(--light-accent);
}

p {
  margin-bottom: 1.5rem;
  font-weight: 300;
  font-size: clamp(1rem, 1.25vw, 1.1rem);
  line-height: 1.6;
  color: var(--text-color-secondary);
}

a {
  color: var(--text-color);
  text-decoration: none;
  transition: var(--transition-ui);
}

/* No blanket `a:hover { opacity: .9 }`. It silently dimmed every considered
   hover on the page — including the social icons, at the exact moment they
   turn accent-blue and should be at their brightest. */

/* Skill Lists */
ul {
  list-style-type: none;
  margin-bottom: 1.5rem;
  text-align: left;
  padding-left: 1rem;
}

ul li {
  position: relative;
  padding-left: 1.5rem;
  margin-bottom: 0.8rem;
  font-weight: 300;
}

ul li::before {
  content: "";
  position: absolute;
  left: 0;
}

/* Hero: the whole frame remains uncropped and at full opacity. The title,
   credits, socials and CTA sit on the dark left of the photograph.
   Roy, 2026-09-27, three times: "make sure to shew teh full hero image now
   its cropped"; then, after a contain-only fix that still veiled it, "th
   emain hero image is not fully shown pls fix"; then "make sure its not
   faded it should be 100% opacity". Full means SEEN: no crop, no scrim, no
   pseudo-element, no nav, no arrow on the picture, no filter, no
   fade or scale entrance. The frame is a rain-wet night underpass (one-point
   perspective, a hooded figure at x 0.66-0.70 against the bright opening).
   Its dark edges are the photograph's own vignette, not CSS.

   THE VISIBLE-FRAME RULE. The hero is a plain column on the page black:
     header (the fixed nav, solid black, sits on the hero's top padding)
     -> the frame, 16:9, as large as the first screen allows
     -> the scroll cue
     -> the title card (tagline, name, icons, CTA) inside the frame.
   `.hero-media` is a 16:9 box: as wide as the page, or as wide as 16:9 of
   (viewport height - header - cue), whichever is smaller, centred. Wider
   screens get explicit black pillars; nothing is stretched or cropped.
   `object-fit: contain` on a box that is already 16:9 is the rounding
   guarantee. Portrait (<= 3:4): the frame is full width under the header,
   with the card over the dark left and the cue below.
   All hero information overlays the dark side of the image, on one left axis.
   Measurements: PRODUCT.md -> Hero, DESIGN.md -> The Hero Title Card. */
.hero {
  /* header height: .nav-container 15px + 37.6px link + 15px, plus the nav's
     1px bottom border = 68.6px, rounded UP so the frame never tucks under it */
  --nav-h: 69px;
  /* the scroll arrow's own box: 12px + 32px glyph + 12px */
  --hero-cue: 56px;
  --hero-gap: clamp(1.25rem, 3svh, 2rem);
  position: relative;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  padding-top: var(--nav-h);
  overflow: hidden;
  background-color: var(--bg-color);
}

.hero-media {
  position: relative;
  order: 0;
  flex: none;
  margin-inline: auto;
  /* contain-fit, done on the BOX: page width, or 16:9 of the height the
     first screen has left after the header and the cue */
  width: min(100%, calc((100vh - var(--nav-h) - var(--hero-cue)) * 16 / 9));
  width: min(100%, calc((100svh - var(--nav-h) - var(--hero-cue)) * 16 / 9));
  aspect-ratio: 16 / 9;
  /* Above the page grain (body::before: fixed, z 1000, noise at 3%), which
     otherwise lays a pseudo-element over every pixel of the picture. Same z,
     later in the tree, so the frame paints over it; the nav (1001) stays on
     top when the page scrolls. */
  z-index: 1000;
  pointer-events: none;
}

.hero-media picture {
  display: block;
  height: 100%;
}

/* Never animated, never filtered, never overlaid: opacity 1 from first paint. */
.hero-media img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: contain;
  object-position: 50% 50%;
}

/* Same left axis as the page content: the frame is narrower than the viewport
   on wide screens, but both use the same 1100px measure and gutter. */
.hero .hero-media .hero-overlay {
  position: absolute;
  inset: 0;
  z-index: 1;
  display: flex;
  align-items: flex-end;
  width: 100%;
  margin: 0 auto;
  padding-bottom: clamp(1rem, 4%, 3.5rem);
  pointer-events: none; /* only the links inside receive events */
}
.hero .hero-overlay .hero-content {
  width: 55%;
  pointer-events: auto;
}
.hero .hero-overlay h1 {
  max-width: 100%;
  font-size: clamp(2.5rem, 5vw, 5.5rem);
  line-height: 1.05;
}
@media (max-width: 600px) {
  .hero .hero-media .hero-overlay { padding-bottom: 0.5rem; }
  .hero .hero-overlay h1 {
    max-width: 100%;
    font-size: clamp(1.55rem, 7vw, 2rem);
    overflow-wrap: normal;
  }
  .hero .hero-overlay .tagline { font-size: 0.57rem; line-height: 1.25; margin-bottom: 0.25rem; letter-spacing: 1px; }
  .hero .hero-overlay .social-icons { margin-top: 0.25rem; gap: 1rem; }
  .hero .hero-overlay .social-icons a { font-size: 1rem; }
  .hero .hero-overlay .cta-btn { margin-top: 0.35rem; padding: 0.35rem 0.6rem; font-size: 0.65rem; letter-spacing: 1px; }
}

.hero-content {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  text-align: left;
}

.tagline {
  font-size: 1.2rem;
  text-transform: uppercase;
  letter-spacing: 3px;
  margin-bottom: 20px;
  display: flex;
  justify-content: center;
  align-items: center;
  color: #fff;
  flex-wrap: wrap;
}

/* Tagline acts as an eyebrow above the name in the hero. */
.hero .tagline {
  justify-content: flex-start;
  font-size: clamp(0.72rem, 0.9vw, 0.85rem);
  color: rgba(255, 255, 255, 0.72);
  margin-bottom: 14px;
}

.hero .social-icons {
  justify-content: flex-start;
}

/* The cue is in the flow, under the frame, never floated over it. */
.hero .scroll-down-arrow {
  position: relative;
  order: 1;
  bottom: auto;
  left: auto;
  align-self: center;
  flex: none;
  transform: none;
}

.hero .scroll-down-arrow:active {
  transform: scale(0.92);
}

/* Portrait: the cue follows the full-frame image and its overlaid credits. */
@media (max-aspect-ratio: 3 / 4) {
  .hero .scroll-down-arrow {
    order: 2;
    margin-top: 0.5rem;
  }
}

@media (max-width: 768px) {
  .hero {
    --nav-h: 60px; /* the mobile nav bar is 59px */
  }
}

/* Short landscape (phones on their side): a compact header and a smaller cue
   leave the frame as much of the 390px height as possible. */
@media (max-height: 540px) and (min-aspect-ratio: 4 / 3) {
  .hero {
    --nav-h: 51px; /* 6 + 37.6 + 6 + 1px border = 50.6 */
    --hero-cue: 40px; /* 8 + 24 + 8 */
  }

  .hero .hero-overlay h1 { font-size: clamp(1.75rem, 4vw, 2.25rem); }
  .hero .hero-overlay .tagline { font-size: 0.65rem; line-height: 1.3; margin-bottom: 0.4rem; letter-spacing: 1px; }
  .hero .hero-overlay .social-icons { margin-top: 0.4rem; }
  .hero .hero-overlay .cta-btn { margin-top: 0.4rem; padding: 0.5rem 0.9rem; }

  .hero .scroll-down-arrow {
    padding-block: 8px;
    font-size: 1.5rem;
  }

  /* the header goes compact to match. `.main-nav` in the selector because
     the base `.nav-container` rule comes later in this file. */
  .main-nav .nav-container {
    padding-block: 6px;
  }
}

.tagline-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  background-color: var(--vusion-blue);
  border-radius: 50%;
  margin: 0 15px;
}

.social-icons {
  display: flex;
  justify-content: center;
  margin-top: 30px;
  gap: 30px;
}

.social-icons a {
  font-size: 24px;
  color: #fff;
  /* was plain `ease` — the only live element moving off the brand curve */
  transition: transform var(--dur-slow) var(--ease-out), color var(--dur-slow) ease;
}

@media (hover: hover) {
  .social-icons a:hover {
    transform: translateY(-4px);
    color: var(--vusion-blue);
  }
}

.cta-btn {
  display: inline-block;
  background-color: transparent;
  border: 1px solid #fff;
  color: #fff;
  padding: 12px 30px;
  margin-top: 40px;
  font-size: 14px;
  text-transform: uppercase;
  letter-spacing: 2px;
  transition: transform 0.25s cubic-bezier(0.23, 1, 0.32, 1), color 0.25s ease, background-color 0.25s ease,
    border-color 0.25s ease, box-shadow 0.25s ease, opacity 0.25s ease;
  overflow: hidden;
  position: relative;
  z-index: 1;
}

/* One transition, declared once. There used to be two here — the second
   silently overrode the intended 300ms wipe to 250ms. */
.cta-btn:before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  transform: translateX(-100%);
  background-color: var(--vusion-blue);
  transition: transform var(--dur-slow) var(--ease-out);
  z-index: -1;
}

/* #080808 on the blue fill, not #fff. White here was 1.42:1 — invisible. */
.cta-btn:hover,
.cta-btn:focus-visible {
  color: #080808;
  border-color: var(--vusion-blue);
}

@media (hover: hover) {
  .cta-btn:hover:before {
    transform: translateX(0);
  }
}

/* Ungated: a keyboard user gets the same affordance as a mouse user. */
.cta-btn:focus-visible:before {
  transform: translateX(0);
}

/* About section */
.about-content {
  padding: 60px;
  border-radius: 8px;
  margin-bottom: 100px;
  text-align: center;
  max-width: 900px;
  margin-left: auto;
  margin-right: auto;
  position: relative;
  overflow: hidden;
  background-color: transparent;
  transform: none;
  transition: none;
}

/* Remove hover effects */
.about-content:hover {
  transform: none;
  box-shadow: none;
  border-color: transparent;
}

.about-content p {
  font-size: 1.4rem;
  line-height: 1.7;
  max-width: 800px;
  margin: 0 auto;
  padding: 0 20px;
  widows: 2;
  orphans: 2;
  hyphens: auto;
  overflow-wrap: break-word;
  text-align: center;
  text-wrap: pretty;
  transition: none;
  background: none;
  transform: none;
}

.about-content p:hover {
  transform: none;
  background: none;
}

/* Quote banner */
.quote-banner {
  padding: 40px 20px;
  text-align: center;
  background-color: var(--darker-bg);
  margin: 60px auto;
  transition: transform 0.3s ease, background-color 0.3s ease,
    box-shadow 0.3s ease;
  max-width: 100%;
  border-radius: 8px;
  border: 1px solid var(--border-accent);
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}

.quote-banner:hover {
  transform: scale(1.01);
  box-shadow: 0 15px 40px rgba(0, 0, 0, 0.4), 0 0 20px rgba(194, 221, 229, 0.1);
}

.quote-title {
  font-size: 2.2rem;
  margin-bottom: 20px;
  letter-spacing: 1px;
  position: relative;
  display: inline-block;
  line-height: 1.3;
  color: var(--vusion-blue-bright);
  text-shadow: 0 0 15px rgba(194, 221, 229, 0.2);
  font-weight: 400;
}

.quote-title::after {
  content: "";
  position: absolute;
  bottom: -8px;
  left: 0;
  display: block;
  width: 100%;
  height: 2px;
  background-color: var(--vusion-blue);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.5s ease;
}

.quote-title:hover::after {
  transform: scaleX(1);
}

.quote-text {
  max-width: 500px;
  margin-left: auto;
  margin-right: auto;
  line-height: 1.7;
  color: var(--text-color);
}

/* Skills section */
.skills-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 30px;
  max-width: 1200px;
  margin: 0 auto;
  position: relative;
  z-index: 1;
}

.skills-grid-two {
  grid-template-columns: repeat(2, 1fr);
}

/* Skills background */
#skills {
  position: relative;
  overflow: hidden;
}

@media (max-width: 992px) {
  .skills-grid:not(.skills-grid-two) {
    grid-template-columns: repeat(2, 1fr);
  }
}

@media (max-width: 768px) {
  .skills-grid,
  .skills-grid-two {
    grid-template-columns: 1fr;
  }
}

.skill-card {
  background-color: var(--card-bg);
  background-image: var(--card-bg-gradient);
  padding: 40px 30px;
  border-radius: 8px;
  transition: transform 0.3s cubic-bezier(0.23, 1, 0.32, 1), box-shadow 0.3s cubic-bezier(0.23, 1, 0.32, 1), border-color 0.3s ease;
  position: relative;
  z-index: 1;
  overflow: hidden;
  text-align: center;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  border: 1px solid var(--border-light);
  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}

.skill-content {
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
}

/* New skill section styling */
.skill-trio {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
  margin-bottom: 15px;
}

.skill-keyword {
  background-color: rgba(194, 221, 229, 0.1);
  padding: 6px 12px;
  border-radius: 20px;
  font-size: 0.85rem;
  letter-spacing: 1px;
  color: var(--vusion-blue-bright);
  transition: transform 0.25s cubic-bezier(0.23, 1, 0.32, 1), color 0.25s ease, background-color 0.25s ease,
    border-color 0.25s ease, box-shadow 0.25s ease, opacity 0.25s ease;
  border: 1px solid rgba(194, 221, 229, 0.2);
  text-shadow: 0 0 10px rgba(209, 246, 255, 0.2);
}

.skill-card:hover .skill-keyword {
  background-color: rgba(194, 221, 229, 0.2);
  transform: translateY(-2px);
  box-shadow: 0 4px 8px rgba(194, 221, 229, 0.1);
}

.skill-summary {
  font-size: 0.95rem;
  color: #ccc;
  margin-bottom: 20px;
  font-weight: 300;
  font-style: italic;
}

.skill-details {
  width: 100%;
  margin-top: 10px;
}

.details-toggle {
  cursor: pointer;
  color: var(--vusion-blue);
  font-size: 0.85rem;
  transition: transform 0.25s cubic-bezier(0.23, 1, 0.32, 1), color 0.25s ease, background-color 0.25s ease,
    border-color 0.25s ease, box-shadow 0.25s ease, opacity 0.25s ease;
  display: inline-flex;
  align-items: center;
  gap: 5px;
}

.details-toggle:hover {
  opacity: 0.8;
}

.details-toggle i {
  font-size: 0.7rem;
  transition: transform 0.3s ease;
}

.extended-details {
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.5s ease, opacity 0.5s ease, transform 0.5s ease;
  opacity: 0;
  transform: translateY(-10px);
  margin-top: 0;
}

.details-toggle.active i {
  transform: rotate(180deg);
}

.extended-details.active {
  max-height: 300px;
  opacity: 1;
  transform: translateY(0);
  margin-top: 15px;
}

.skill-card:before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: radial-gradient(
    circle at center,
    rgba(194, 221, 229, 0.1) 0%,
    transparent 70%
  );
  opacity: 0;
  transition: opacity 0.5s ease;
  z-index: -1;
}

.skill-card:hover {
  transform: translateY(-10px);
  box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
  border-color: var(--border-accent);
}

.skill-card:hover:before {
  opacity: 1;
}

.skill-icon {
  font-size: 40px;
  color: var(--vusion-blue);
  margin-bottom: 20px;
  transition: transform 0.3s cubic-bezier(0.23, 1, 0.32, 1), background-color 0.3s ease, border-color 0.3s ease,
    box-shadow 0.3s ease, opacity 0.3s ease;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 80px;
  width: 80px;
  border-radius: 50%;
  background-color: rgba(15, 15, 15, 0.5);
  border: 1px solid rgba(255, 255, 255, 0.05);
}

.skill-card:hover .skill-icon {
  transform: scale(1.1);
  background-color: rgba(25, 25, 25, 0.8);
  border-color: var(--border-accent);
  box-shadow: 0 0 20px rgba(194, 221, 229, 0.2);
}

/* Skill icon flip effect */
.skill-icon i {
  backface-visibility: hidden;
  transition: transform 0.3s cubic-bezier(0.23, 1, 0.32, 1), background-color 0.3s ease, border-color 0.3s ease,
    box-shadow 0.3s ease, opacity 0.3s ease;
}

.skill-icon .icon-front {
  position: absolute;
  transform: rotateY(0deg);
}

.skill-icon .icon-back {
  position: absolute;
  transform: rotateY(180deg);
  opacity: 0;
}

.skill-card:hover .skill-icon .icon-front {
  transform: rotateY(180deg);
  opacity: 0;
}

.skill-card:hover .skill-icon .icon-back {
  transform: rotateY(0deg);
  opacity: 1;
}

/* Portfolio links */
#portfolio {
  position: relative;
  overflow: hidden;
}

/* Portfolio index — rows, not cards.
   The card anatomy is gone: border + radius + gradient + a black box-shadow
   that resolved to #050505 over #080808 (a 3/255 delta smeared across 40px —
   a paint pass that rendered nothing). With it goes the 10px hover lift (a
   link is not "elevated"), two broken .portfolio-img::after rules that set
   top+bottom+height and so painted the WRONG HALF of every image, two
   hotlinks to vusion.art, and three images doing no work. This now rhymes
   with .journey-row: the lower page becomes one idea instead of three. */
.portfolio-index {
  list-style: none;
  max-width: var(--measure);
  margin: clamp(56px, 6vw, 80px) auto 0;
  padding: 0;
  position: relative;
  z-index: 1;
}

.portfolio-item {
  border-top: 1px solid var(--border-light);
  margin: 0;
  padding: 0;
}

.portfolio-item:last-child {
  border-bottom: 1px solid var(--border-light);
}

.portfolio-item::before {
  content: none;
}

.portfolio-link {
  display: block;
  padding: clamp(24px, 3vw, 36px) 0;
  /* Colour only, 150ms. An index is scanned tens of times per visit — Emil's
     frequency table says reduce, not decorate. No movement. */
  transition: color var(--dur-press) ease;
}

.portfolio-link h3 {
  margin-bottom: 10px;
  font-size: clamp(1.15rem, 1.6vw, 1.45rem);
  color: var(--text-color);
  transition: color var(--dur-press) ease;
}

.portfolio-link p {
  margin: 0;
  max-width: 62ch;
  font-size: 0.95rem;
  color: rgba(255, 255, 255, 0.65);
}

@media (hover: hover) {
  .portfolio-link:hover h3 {
    color: var(--vusion-blue-bright);
  }
}

.portfolio-link:active {
  opacity: 0.6;
}

/* Skills & Journey Combined Section */
.skills-journey-container {
  max-width: 1200px;
  margin: 0 auto;
  position: relative;
  padding: 0px 0;
  z-index: 1;
}

/* Timeline background - keeping similar styling */
#timeline {
  position: relative;
  overflow: hidden;
}

.skills-journey-cards {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 30px;
  max-width: 1200px;
  width: 100%;
  margin: 0 auto;
}

.journey-card {
  width: 100%;
  max-width: 1200px;
  margin: 0 auto;
  background-color: var(--card-bg);
  background-image: var(--card-bg-gradient);
  border-radius: 8px;
  border: 1px solid var(--border-light);
  overflow: hidden;
  transition: transform 0.3s cubic-bezier(0.23, 1, 0.32, 1), box-shadow 0.3s cubic-bezier(0.23, 1, 0.32, 1), border-color 0.3s ease;
  height: 100%;
  box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
}

.journey-card:hover {
  transform: translateY(-10px);
  box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
  border-color: var(--border-accent);
}

.journey-card-content {
  width: 100%;
  padding: 40px;
  border-radius: 8px;
  text-align: center;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
}

.journey-card-header {
  display: flex;
  flex-direction: column;
  align-items: center;
  margin-bottom: 20px;
  position: relative;
}

.journey-period {
  color: var(--vusion-blue);
  font-size: 14px;
  font-weight: 500;
  margin-top: 5px;
  transition: transform 0.25s cubic-bezier(0.23, 1, 0.32, 1), color 0.25s ease, background-color 0.25s ease,
    border-color 0.25s ease, box-shadow 0.25s ease, opacity 0.25s ease;
}

.journey-card:hover .journey-period {
  transform: scale(1.05);
  text-shadow: 0 0 10px rgba(194, 221, 229, 0.3);
}

/* Journey skills styling */
/* Six more boxes. With the portfolio cards gone these pills were the heaviest
   thing left in Journey — a third typographic register competing with the h3
   and the paragraph. A middot line says the same and weighs nothing. */
.journey-skills {
  display: flex;
  flex-wrap: wrap;
  gap: 0;
  margin: 15px 0;
}
.journey-skill {
  padding: 0;
  border: none;
  border-radius: 0;
  background: none;
  text-shadow: none;
  font-size: 0.85rem;
  letter-spacing: 0.02em;
  color: rgba(255, 255, 255, 0.55);
}
.journey-skill + .journey-skill::before {
  content: "\00b7";
  margin: 0 0.6em;
  color: rgba(255, 255, 255, 0.3);
}

.journey-card:hover .journey-skill {
  background-color: rgba(194, 221, 229, 0.2);
  transform: translateY(-2px);
  box-shadow: 0 4px 8px rgba(194, 221, 229, 0.1);
}

.journey-summary {
  font-size: 0.9rem;
  color: #ccc;
  margin-bottom: 15px;
  font-weight: 300;
  font-style: italic;
}

.journey-card [itemprop="description"] {
  font-size: 0.95rem;
  line-height: 1.5;
  margin-bottom: 15px;
  text-align: center;
}

.journey-details {
  width: 100%;
  margin-top: 5px;
  border-top: 1px solid rgba(194, 221, 229, 0.1);
  padding-top: 15px;
}

/* Journey timeline styling for detail lists */
.skill-category {
  font-size: 1rem;
  color: var(--vusion-blue);
  margin-bottom: 12px;
  text-transform: uppercase;
  letter-spacing: 1px;
  font-weight: 500;
  text-align: center;
}

.extended-details ul {
  margin: 10px 0;
  text-align: left;
  padding-left: 10px;
}

.extended-details ul li {
  position: relative;
  padding-left: 20px;
  margin-bottom: 10px;
  font-size: 0.9rem;
  line-height: 1.4;
  color: var(--text-color-secondary);
}

.extended-details ul li::before {
  content: "•";
  position: absolute;
  left: 0;
  color: var(--vusion-blue);
  font-size: 1.2rem;
  line-height: 1;
  top: 0.1em;
}

.timeline-icon {
  font-size: 24px;
  margin-bottom: 20px;
  color: var(--vusion-blue);
  transition: transform 0.3s cubic-bezier(0.23, 1, 0.32, 1), background-color 0.3s ease, border-color 0.3s ease,
    box-shadow 0.3s ease, opacity 0.3s ease;
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  height: 80px;
  width: 80px;
  border-radius: 50%;
  background-color: rgba(15, 15, 15, 0.5);
  border: 1px solid rgba(255, 255, 255, 0.05);
}

.journey-card:hover .timeline-icon {
  transform: scale(1.1);
  background-color: rgba(25, 25, 25, 0.8);
  border-color: var(--border-accent);
  box-shadow: 0 0 20px rgba(194, 221, 229, 0.2);
}

/* Timeline icon flip effect */
.timeline-icon i {
  backface-visibility: hidden;
  transition: transform 0.3s cubic-bezier(0.23, 1, 0.32, 1), background-color 0.3s ease, border-color 0.3s ease,
    box-shadow 0.3s ease, opacity 0.3s ease;
  font-size: 30px;
}

.timeline-icon .icon-front {
  position: absolute;
  transform: rotateY(0deg);
}

.timeline-icon .icon-back {
  position: absolute;
  transform: rotateY(180deg);
  opacity: 0;
}

.journey-card:hover .timeline-icon .icon-front {
  transform: rotateY(180deg);
  opacity: 0;
}

.journey-card:hover .timeline-icon .icon-back {
  transform: rotateY(0deg);
  opacity: 1;
}

@media (max-width: 768px) {
  .skills-journey-cards {
    grid-template-columns: 1fr;
  }
}

/* Contact section */
#contact {
  position: relative;
  overflow: hidden;
}

.contact-intro {
  text-align: center;
  max-width: 600px;
  margin: 0 auto 30px;
  position: relative;
  z-index: 1;
}

.contact-form {
  max-width: 600px;
  margin: 0 auto;
  background-color: var(--dark-bg);
  padding: 40px;
  border-radius: 8px;
  border: 1px solid var(--border-light);
  transition: transform 0.3s ease, box-shadow 0.3s ease;
}

.contact-form:hover {
  border-color: var(--border-accent);
  transform: translateY(-5px);
  box-shadow: var(--card-glow);
}

.form-group {
  margin-bottom: 20px;
}

input,
textarea {
  width: 100%;
  padding: 12px;
  background-color: var(--input-bg);
  border: 1px solid var(--input-border);
  color: var(--text-color);
  font-family: "Onest", sans-serif;
  transition: transform 0.25s cubic-bezier(0.23, 1, 0.32, 1), color 0.25s ease, background-color 0.25s ease,
    border-color 0.25s ease, box-shadow 0.25s ease, opacity 0.25s ease;
  border-radius: 4px;
}

input:focus,
textarea:focus {
  background-color: var(--dark-bg);
  border-color: var(--border-accent);
  outline: none;
  box-shadow: 0 0 10px var(--vusion-blue-glow);
}

textarea {
  height: 150px;
  resize: none;
}

.submit-btn {
  background-color: transparent;
  border: 1px solid #fff;
  color: #fff;
  padding: 12px 30px;
  font-size: 14px;
  text-transform: uppercase;
  letter-spacing: 2px;
  cursor: pointer;
  transition: transform 0.25s cubic-bezier(0.23, 1, 0.32, 1), color 0.25s ease, background-color 0.25s ease,
    border-color 0.25s ease, box-shadow 0.25s ease, opacity 0.25s ease;
  position: relative;
  overflow: hidden;
  z-index: 1;
  display: flex;
  align-items: center;
  margin: 0 auto;
}

.submit-btn:before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  transform: translateX(-100%);
  transition: transform 0.3s cubic-bezier(0.23, 1, 0.32, 1);
  background-color: var(--vusion-blue);
  transition: transform 0.25s cubic-bezier(0.23, 1, 0.32, 1), color 0.25s ease, background-color 0.25s ease,
    border-color 0.25s ease, box-shadow 0.25s ease, opacity 0.25s ease;
  z-index: -1;
}

.submit-btn:hover {
  color: #fff;
  border-color: var(--vusion-blue);
}

.submit-btn:hover:before {
  left: 0;
}

.submit-btn i {
  margin-right: 8px;
}

.alt-contact {
  margin-top: 30px;
  text-align: center;
  font-size: 0.9rem;
  color: #ccc;
}

#formMessage {
  display: none;
  margin-top: 20px;
  padding: 15px;
  background: var(--dark-bg);
  border-radius: 8px;
  transition: transform 0.25s cubic-bezier(0.23, 1, 0.32, 1), color 0.25s ease, background-color 0.25s ease,
    border-color 0.25s ease, box-shadow 0.25s ease, opacity 0.25s ease;
}

#formMessage.success {
  background: rgba(76, 175, 80, 0.15);
  border: 1px solid #4caf50;
  color: #4caf50;
}

#formMessage.error {
  background: rgba(244, 67, 54, 0.15);
  border: 1px solid #f44336;
  color: #f44336;
}

.alt-contact a {
  color: var(--vusion-blue);
  transition: transform 0.25s cubic-bezier(0.23, 1, 0.32, 1), color 0.25s ease, background-color 0.25s ease,
    border-color 0.25s ease, box-shadow 0.25s ease, opacity 0.25s ease;
}

.alt-contact a:hover {
  text-decoration: underline;
}

/* Footer */
footer {
  padding: 40px 0;
  background: none;
  text-align: left;
  border-top: 1px solid var(--border-light);
}
footer .container {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
}
footer p {
  margin: 0;
  font-size: 0.85rem;
  color: rgba(255, 255, 255, 0.5);
}

.back-to-top {
  flex: none;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background-color: rgba(25, 25, 25, 0.8);
  border: 1px solid rgba(255, 255, 255, 0.1);
  color: white;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 20px;
  cursor: pointer;
  transition: transform 0.25s cubic-bezier(0.23, 1, 0.32, 1), color 0.25s ease, background-color 0.25s ease,
    border-color 0.25s ease, box-shadow 0.25s ease, opacity 0.25s ease;
}

@media (hover: hover) {
  .back-to-top:hover {
    background-color: var(--vusion-blue);
    color: #000;
    transform: translateY(-3px);
  }
}

/* Navigation */
.main-nav {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  /* 1001, one above the grain (body::before, 1000) and the hero frame
     (.hero-media, 1000, which sits above the grain), so the nav still paints
     over the frame when the page scrolls under it. */
  z-index: 1001;
  box-shadow: none;
  border-bottom: 1px solid transparent;
  background-color: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  transition: background-color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease;
}

/* Homepage: a compact SOLID black header above the hero frame, never a
   transparent bar over it. At rest it sits on the hero's top padding, so the
   frame's lit ceiling starts below it, uncovered. (Beats .visible's 0.9 too,
   so the bar never turns see-through while the frame scrolls under it.) */
body:has(> .hero) .main-nav {
  background-color: var(--bg-color);
}

.main-nav.visible {
  background-color: rgba(0, 0, 0, 0.9);
  backdrop-filter: blur(10px);
  -webkit-backdrop-filter: blur(10px);
  box-shadow: 0 2px 20px rgba(0, 0, 0, 0.3);
  border-bottom: 1px solid var(--border-light);
}

/* Shares .container's measure so the nav logo, the hero headline and every
   section heading sit on ONE left axis. It used to be `padding: 15px 5%` — a
   viewport-relative edge that drifted away from the centred 1200px content box,
   so Roy's name appeared twice at two different left positions. */
/* Shares .container's measure, so the nav sits on the same left/right edges as
   every section. No logo: the hero states the name once, at full size — a
   wordmark 60px above it was the same name twice, at two sizes.
   flex-end (not space-between): with the logo gone, space-between would strand
   the links on the LEFT, since they'd be the only desktop child. */
.nav-container {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: 16px;
  width: 100%;
  max-width: var(--measure);
  margin: 0 auto;
  padding: 15px var(--gutter);
}

.nav-links {
  display: flex;
  gap: 5px;
}

.nav-links a {
  padding: 10px 15px;
}

.nav-links a {
  font-size: 0.9rem;
  text-transform: uppercase;
  letter-spacing: 1.5px;
  position: relative;
  padding: 8px 12px;
  margin: 0 5px;
  font-weight: 400;
  color: rgba(255, 255, 255, 0.8);
  transition: transform 0.25s cubic-bezier(0.23, 1, 0.32, 1), color 0.25s ease, background-color 0.25s ease,
    border-color 0.25s ease, box-shadow 0.25s ease, opacity 0.25s ease;
}

.nav-links a::after {
  content: "";
  position: absolute;
  width: calc(100% - 24px);
  height: 2px;
  bottom: 3px;
  left: 12px;
  background-color: var(--vusion-blue);
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.3s cubic-bezier(0.23, 1, 0.32, 1);
}

/* Split, so the observer-driven active underline survives the hover gate. */
.nav-links a.active::after {
  transform: scaleX(1);
}

@media (hover: hover) {
  .nav-links a:hover {
    color: #fff;
    transform: translateY(-2px);
    text-shadow: 0 0 10px rgba(194, 221, 229, 0.3);
  }

  .nav-links a:hover::after {
    transform: scaleX(1);
  }
}

.nav-links a.active {
  color: var(--vusion-blue-bright);
}

/* The primary nav control below 768px. It previously had a JS listener,
   cursor:pointer, and no hover, active, or focus styling whatsoever. */
.mobile-menu-btn {
  display: none;
  background: none;
  border: none;
  color: white;
  font-size: 24px;
  cursor: pointer;
  padding: 8px;
  margin: -8px; /* hit area without shifting layout */
  transition: color var(--dur-base) ease, transform var(--dur-press) var(--ease-out);
}

@media (hover: hover) {
  .mobile-menu-btn:hover {
    color: var(--vusion-blue);
  }
}

@media (max-width: 768px) {
  .mobile-menu-btn {
    display: block;
  }

  .nav-links {
    position: fixed;
    top: 60px;
    right: -100%;
    width: 80%;
    height: 100vh;
    background-color: rgba(0, 0, 0, 0.95);
    flex-direction: column;
    align-items: center;
    justify-content: flex-start;
    padding-top: 50px;
    transition: right 0.3s ease;
    z-index: 999;
  }

  .nav-links.open {
    right: 0;
  }

  .nav-links a {
    margin: 15px 0;
  }
}

/* Scroll Indicator */
.scroll-indicator {
  position: fixed;
  top: 0;
  left: 0;
  height: 3px;
  background-color: var(--vusion-blue);
  z-index: 1002; /* over the solid nav (1001) */
  width: 100%;
  transform: scaleX(0);
  transform-origin: left;
}

/* The wrapper owns interactive state; the child <i> owns the loop. Previously
   `drift` ran on this element, and a running animation outranks a non-animated
   declaration in the cascade — so :hover { opacity } never rendered at all. */
.scroll-down-arrow {
  position: absolute;
  bottom: 28px;
  left: 50%;
  z-index: 2;
  transform: translateX(-50%);
  padding: 12px 20px; /* hit area, without touching the glyph */
  color: #fff;
  font-size: 2rem;
  line-height: 1;
  opacity: 0.55;
  cursor: pointer;
  transition: opacity var(--dur-slow) ease, color var(--dur-slow) ease,
    transform var(--dur-press) var(--ease-out);
}

.scroll-down-arrow i {
  display: block;
  animation: drift var(--dur-ambient) ease-in-out infinite;
}

@media (hover: hover) {
  .scroll-down-arrow:hover {
    opacity: 1;
    color: var(--vusion-blue);
  }
}

.scroll-down-arrow:active {
  transform: translateX(-50%) scale(0.92);
}

/* Bobbing AND pulsing is two ideas fighting. It bobs. */
@keyframes drift {
  0%,
  100% {
    transform: translateY(0);
  }
  50% {
    transform: translateY(6px);
  }
}

/* ============================================================
   HERO — the one signature moment: a film title card.
   The frame dissolves up from black and the name resolves. Nothing slides.

   fill-mode is `backwards`, never forwards/both: every keyframe's implicit
   `to` IS the element's natural resting state, so only `from` is declared.
   If this never runs, the hero is simply there — no JS, no class gate, no
   stuck-hidden failure mode. (forwards would also permanently lock transform
   and silently kill every :active on these elements.)
   ============================================================ */
@media (prefers-reduced-motion: no-preference) {
  /* The ground is NOT animated. The frame used to dissolve up from black and
     settle from scale(1.03); both are gone (Roy, 2026-09-27: "make sure its
     not faded it should be 100% opacity", and a scale-in crops the frame's
     edges until it lands). The picture is at opacity 1 from its first paint;
     only the credits below it move. */

  /* The credits — one shared gesture, landing as a run. */
  html.js .hero .tagline,
  html.js .hero .social-icons,
  html.js .hero .cta-btn {
    animation: hero-rise var(--dur-entrance) var(--ease-out) backwards;
  }
  @keyframes hero-rise {
    from {
      opacity: 0;
      transform: translateY(6px);
    }
  }
  html.js .hero .tagline {
    animation-delay: calc(var(--stagger) * 2);
  }
  html.js .hero .social-icons {
    animation-delay: calc(var(--stagger) * 10);
  }
  html.js .hero .cta-btn {
    animation-delay: calc(var(--stagger) * 11);
  }

  /* The title. The h1 is already committed to tracked-wide uppercase, so the
     tracking IS the gesture: the name arrives loose and tightens to rest.
     calc(3px + .04em), not a literal — a fixed start value inverts across the
     clamp, +0.04em is always proportional. */
  html.js .hero h1 {
    animation: hero-title var(--dur-title) var(--ease-out) backwards;
    animation-delay: calc(var(--stagger) * 3);
  }
  @keyframes hero-title {
    from {
      opacity: 0;
      letter-spacing: calc(3px + 0.04em);
    }
  }

  /* The punctuation — the brand rule draws beneath the resolved name. */
  html.js .hero h1::after {
    animation: hero-rule var(--dur-rule) var(--ease-out) backwards;
    animation-delay: calc(var(--stagger) * 9);
  }
  @keyframes hero-rule {
    from {
      transform: scaleX(0);
    }
  }

  /* The invitation. */
  html.js .scroll-down-arrow {
    animation: hero-arrow var(--dur-entrance) linear backwards;
    animation-delay: calc(var(--stagger) * 15);
  }
  @keyframes hero-arrow {
    from {
      opacity: 0;
    }
  }
}

/* Focus States for Accessibility */
a:focus-visible,
button:focus-visible,
summary:focus-visible,
input:focus-visible,
textarea:focus-visible {
  outline: 2px solid var(--vusion-blue);
  outline-offset: 2px;
}

.skip-link {
  position: absolute;
  top: -40px;
  left: 0;
  background: var(--vusion-blue);
  color: white;
  padding: 8px 16px;
  z-index: 1003; /* over the progress line (1002) */
  transition: top 0.3s ease;
}

.skip-link:focus {
  top: 0;
}

/* Circuit board pattern overlay */
.circuit-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'%3E%3Cg fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.03'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' /%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
  pointer-events: none;
  z-index: -1;
  opacity: 0.02;
  opacity: 0.02;
}

/* ===== Audit additions (2026-07-11) ===== */

/* Press feedback — the UI listens.
   Split by base transform: elements already carrying one must COMPOSE, not
   clobber. .back-to-top was in the shared rule below, which dropped its
   centering translateY(-50%) and made it jump 20px down on every press. */
.cta-btn:active {
  transform: scale(0.97);
}

.back-to-top:active {
  transform: scale(0.97);
}

/* Never scale text — subpixel mush. Cancelling the hover lift IS the press. */
.nav-links a:active {
  transform: translateY(0);
}

.email-link:active,
.reel-more a:active,
.about-copy a:active,
.journey-more summary:active {
  opacity: 0.6;
}

.social-icons a:active {
  transform: translateY(-1px) scale(0.94);
}

.mobile-menu-btn:active {
  transform: scale(0.9);
}

/* Comfortable touch targets without changing visual size */
.social-icons a,
.back-to-top {
  min-width: 44px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
}

h1, h2, h3 {
  text-wrap: balance;
}

/* Hover gating is INVERTED, deliberately. There used to be a hand-maintained
   `@media (hover: none)` opt-out list here that neutralised `transform` on six
   selectors — three of which no longer existed, while four live hover-transforms
   were missing from it. An opt-out list fails silently on the very device that
   cannot report the problem. Movement-hovers now live inside
   `@media (hover: hover)` instead, so forgetting one is impossible. */

/* Reduced motion: keep content, remove movement */
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    animation-delay: 0ms !important;
    transition-duration: 0.01ms !important;
  }
  html {
    scroll-behavior: auto !important;
  }
}

/* ===== Merged from fix-blue.css (patch layer retired 2026-07-11) ===== */
/* Fix blue highlighting elements */

/* About section hover border fix */
.about-content:hover {
  transform: none;
  box-shadow: none;
  border-color: transparent;
}

.quote-banner:hover {
  background-color: var(--darker-bg);
  transform: scale(1.01);
  border-color: rgba(255, 255, 255, 0.25);
}

/* Journey skills */
.journey-card:hover .journey-skill {
  background-color: rgba(255, 255, 255, 0.15);
  transform: translateY(-2px);
  box-shadow: 0 4px 8px rgba(255, 255, 255, 0.1);
}

/* Journey period */
.journey-period {
  color: rgba(255, 255, 255, 0.7);
}

.journey-card:hover .journey-period {
  text-shadow: 0 0 10px rgba(255, 255, 255, 0.3);
}

/* Details toggle */
.details-toggle {
  color: rgba(255, 255, 255, 0.7);
}

/* Journey summary */
.journey-summary {
  color: rgba(255, 255, 255, 0.7);
}

/* Extended details bullet points */
.extended-details ul li::before {
  color: rgba(255, 255, 255, 0.7);
}

/* Removed typewriter animation in favor of animated line */

/* Fix card hover border and effects */
.journey-card:hover {
  transform: translateY(-10px);
  box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
  border-color: rgba(255, 255, 255, 0.25);
}

.skill-card:hover {
  transform: translateY(-10px);
  box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
  border-color: rgba(255, 255, 255, 0.25);
}

/* Fix any other blue elements */
.skill-keyword {
  color: rgba(255, 255, 255, 0.8);
  background-color: rgba(255, 255, 255, 0.1);
  border: 1px solid rgba(255, 255, 255, 0.15);
  text-shadow: none;
}

.skill-card:hover .skill-keyword {
  background-color: rgba(255, 255, 255, 0.15);
}

.skill-category {
  color: rgba(255, 255, 255, 0.8);
}


/* ===== Desktop recomposition (2026-07-11) ===== */

/* --- About: asymmetric two-column --- */
.about-grid {
  display: grid;
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: clamp(32px, 5vw, 72px);
  align-items: center;
  max-width: var(--measure);
  margin: 0 auto;
  text-align: left;
}
/* No border, no radius, no glow. The portrait is low-key black-on-black: its
   edges sample rgb(6,6,6)-(7,7,7) against the page's rgb(8,8,8), so it already
   dissolves. A frame here would draw a glowing rectangle around a region
   indistinguishable from the page — framing nothing, announcing the crop. */
.about-visual {
  margin: 0;
  aspect-ratio: 4 / 5;
}
/* <picture> is inline by default, which would leave the img's height:100% with
   no definite parent to resolve against. Make it a pass-through box. */
.about-visual picture,
.reel-facade picture {
  display: block;
  height: 100%;
}
.about-visual img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}
.about-copy p {
  text-align: left;
}
/* Aligned to the portrait (2026-09-27, audit A1). Centred, the copy was 314px
   of type floating beside a 502px portrait at 1440, with a 94px dead band above
   and below it. Now the column spans the portrait exactly: the opening line
   starts on the portrait's top edge and the client line lands on its bottom
   edge, so both edges of the section agree. Vertical rhythm only; the copy and
   the columns are unchanged. When the copy is taller than the portrait (820 to
   ~1000px wide) the row simply grows to it, as before. */
@media (min-width: 821px) {
  .about-grid {
    align-items: stretch;
  }
  .about-copy {
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    gap: 2rem;
  }
  .about-copy > p {
    margin-bottom: 0;
  }
  .about-copy .about-clients {
    margin-top: 0;
  }
  /* Optical top: Onest's line box sits above the cap height by half-leading
     plus the ascender gap, so the first line is pulled up to meet the edge. */
  .about-open {
    margin-top: -0.32em;
  }
}
/* The opening paragraph carries the section — no display lede above it. Sized
   a step up from body copy so it still leads, without becoming a slogan. */
.about-open {
  font-size: clamp(1.15rem, 0.9rem + 0.7vw, 1.4rem);
  line-height: 1.55;
  color: var(--text-color);
}
/* The only inline link in the body copy. It had no hover rule of its own and
   was relying entirely on the blanket a:hover{opacity:.9} that's now deleted. */
.about-copy a {
  color: var(--vusion-blue-bright);
  border-bottom: 1px solid var(--border-accent);
  transition: color var(--dur-base) ease, border-color var(--dur-base) ease;
}

@media (hover: hover) {
  .about-copy a:hover {
    color: #fff;
    border-bottom-color: var(--vusion-blue-bright);
  }
}
.about-clients {
  margin-top: 2rem;
  font-size: 0.95rem;
  color: rgba(255, 255, 255, 0.75);
  letter-spacing: 0.4px;
}
.about-clients span {
  display: block;
  font-size: 0.75rem;
  text-transform: uppercase;
  letter-spacing: 2px;
  color: var(--vusion-blue);
  margin-bottom: 6px;
}

/* --- Journey: editorial timeline rows --- */
.journey-rows {
  max-width: var(--measure);
  margin: 0 auto;
  text-align: left;
}
/* 200px + a 56px gap put 152px of void beside a 104px date. 8rem sizes to the
   longest string ("2022-Present") so both rows' headings still align — and
   baseline alignment does exactly what the old padding-top: 4px was
   hand-approximating, and stays correct if the type scale ever moves. */
.journey-row {
  display: grid;
  grid-template-columns: 8rem minmax(0, 1fr);
  gap: clamp(20px, 2.5vw, 36px);
  align-items: baseline;
  padding: clamp(32px, 4vw, 56px) 0;
  border-top: 1px solid var(--border-light);
}
.journey-row:last-child {
  border-bottom: 1px solid var(--border-light);
}
.journey-when {
  font-size: 1.05rem;
  letter-spacing: 1.5px;
  color: var(--vusion-blue-bright);
  white-space: nowrap;
}
.journey-what h3 {
  text-align: left;
  margin-bottom: 12px;
  font-size: 1.5rem;
}
.journey-what p {
  text-align: left;
  max-width: 62ch;
  margin-bottom: 1rem;
}
.journey-what .journey-skills {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  justify-content: flex-start;
  margin: 0 0 1rem;
}
.journey-more {
  margin-top: 0.5rem;
}
.journey-more summary {
  cursor: pointer;
  color: rgba(255, 255, 255, 0.7);
  font-size: 0.9rem;
  letter-spacing: 0.5px;
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  transition: color 0.25s ease;
}
.journey-more summary::-webkit-details-marker { display: none; }
.journey-more summary::after {
  content: "\f078";
  font-family: "Font Awesome 6 Free";
  font-weight: 900;
  font-size: 0.7rem;
  transition: transform 0.25s cubic-bezier(0.23, 1, 0.32, 1);
}
.journey-more[open] summary::after { transform: rotate(180deg); }
.journey-more summary:hover { color: var(--vusion-blue-bright); }
.journey-more ul {
  margin: 1rem 0 0;
  padding-left: 0;
}
.journey-more li {
  color: rgba(255, 255, 255, 0.75);
  font-size: 0.95rem;
}
.journey-more li::before {
  content: "\2014";
  color: var(--vusion-blue);
  margin-right: 10px;
  position: static;
}

/* --- Featured reel --- */
.reel-feature {
  max-width: var(--measure);
  margin: 0 auto clamp(48px, 6vw, 80px);
}
.reel-facade {
  position: relative;
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  padding: 0;
  border: 1px solid var(--border-light);
  border-radius: 8px;
  overflow: hidden;
  cursor: pointer;
  background: #000;
  transition: border-color 0.3s ease, box-shadow 0.3s ease;
}
/* The poster is a bright orange commercial still on a #080808 page — the
   loudest object on the site. `opacity` was the wrong variable: dimming orange
   just gives muddy orange. The shout is chroma, so saturate() attacks the
   actual offender, and the frame comes fully alive when you reach for it —
   the section's one reward. The scale() already forces a composited layer, so
   the filter animates on the compositor rather than repainting. */
.reel-facade img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  filter: saturate(0.62) brightness(0.88);
  /* was 500ms — one hover fired 300/300/500 and the image settled last */
  transition: filter var(--dur-slow) ease, transform var(--dur-slow) var(--ease-out);
}
@media (hover: hover) {
  .reel-facade:hover { border-color: var(--border-accent); }
  .reel-facade:hover img { filter: saturate(1) brightness(1); transform: scale(1.015); }
}
.reel-play {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 84px;
  height: 84px;
  border-radius: 50%;
  background: rgba(8, 8, 8, 0.65);
  border: 1px solid rgba(255, 255, 255, 0.4);
  backdrop-filter: blur(4px);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.6rem;
  color: #fff;
  transition: transform 0.3s cubic-bezier(0.23, 1, 0.32, 1), background-color 0.3s ease;
}
.reel-play i { margin-left: 5px; }
@media (hover: hover) {
  .reel-facade:hover .reel-play {
    transform: translate(-50%, -50%) scale(1.08);
    background: rgba(8, 8, 8, 0.85);
  }
}
.reel-facade:active .reel-play { transform: translate(-50%, -50%) scale(0.96); }
.reel-meta {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  padding: 40px 24px 18px;
  background: linear-gradient(transparent, rgba(8, 8, 8, 0.85));
  color: #fff;
  font-size: 1.05rem;
  text-align: left;
}
.reel-meta strong {
  display: block;
  font-size: 0.72rem;
  font-weight: 500;
  text-transform: uppercase;
  letter-spacing: 2px;
  color: var(--vusion-blue-bright);
  margin-bottom: 4px;
}
/* `.reel-feature iframe` lived here and is DELETED. It only ever matched the
   player.vimeo.com iframe that main.js:66-79 injected; with Vimeo gone there is
   no iframe on this site and the rule matched nothing. Its replacement is
   `/* TRANSITIONAL, delete in commit 5 with the Vimeo handler.
   Agent D removed this rule because the only thing that ever matched it was the
   iframe main.js injects. That injector is still live (main.js:66-79) and its
   replacement needs the featured master, which does not exist yet. Shipping the
   deletion first would leave the homepage reel unstyled: full width, no 16/9,
   no border. Restored until film.js lands. */
.reel-feature iframe {
  width: 100%;
  aspect-ratio: 16 / 9;
  border: 1px solid var(--border-light);
  border-radius: 8px;
  display: block;
}

.reel-feature .film-stage`, in the /work/ block at the end of this file:
   same box, same committed 8px radius, now around a self-hosted <video>.
   (PRODUCT.md listed this rule as a "do not delete blind" false positive
   because main.js injected it. That reason expires with the injector.) */
/* Left. This was the third axis inside one section (h2 left, .reel-meta left,
   this right). Now that the reel and the index share --measure, the left edge
   is a real spine and the caption belongs on it. */
.reel-more {
  text-align: left;
  margin-top: 14px;
}
.reel-more a {
  color: rgba(255, 255, 255, 0.7);
  font-size: 0.9rem;
  letter-spacing: 0.5px;
  border-bottom: 1px solid transparent;
  transition: color 0.25s ease, border-color 0.25s ease;
}
.reel-more a:hover { color: var(--vusion-blue-bright); border-bottom-color: var(--border-accent); }


/* --- Contact / Connect ---
   Hoisted out of an inline <style> in index.html: motion values living in the
   markup, outside the token layer, is how a token layer rots on day two. */
/* No panel: rgba(8,8,8,.95) over #080808 is a 0.4% delta — invisible, and its
   only visible product was a seam. The border does the separating; .section
   supplies the rhythm. --border-light, not the blue: this is structure. */
.contact-section {
  border-top: 1px solid var(--border-light);
}

/* position:relative is load-bearing. The ::after underline is absolutely
   positioned at width:100%, and `.email-wrapper` (its intended containing
   block) only ever existed in CSS, never in the markup — so the nearest
   positioned ancestor was section#contact, and hovering this link drew a blue
   rule clean across the whole section floor. */
.email-link {
  position: relative;
  font-size: 1.5rem;
  color: #fff;
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 6px 0;
  max-width: 100%;
  transition: color var(--dur-slow) ease; /* was `transition: all` */
}

.email-link i {
  color: var(--vusion-blue);
}

.email-link::after {
  content: "";
  position: absolute;
  width: 100%;
  height: 1px;
  bottom: 0;
  left: 0;
  background-color: var(--vusion-blue);
  transform: scaleX(0);
  transform-origin: bottom right;
  transition: transform var(--dur-slow) var(--ease-out);
}

.email-link:hover {
  color: var(--vusion-blue);
}

.email-link:hover::after,
.email-link:focus-visible::after {
  transform: scaleX(1);
  transform-origin: bottom left;
}

.connect-grid {
  max-width: var(--measure);
  margin: 0 auto;
  text-align: left;
}
/* The email is the section now — same weight class as the h2 above it. The
   icon must shrink or a 3rem envelope out-shouts the address. */
.connect-primary .email-link { font-size: clamp(1.9rem, 4vw, 3rem); }
.connect-primary .email-link i { font-size: 0.6em; }
.connect-loc {
  color: rgba(255, 255, 255, 0.6);
  font-size: 0.95rem;
  margin: 14px 0 24px;
  text-align: left;
}
.connect-social { justify-content: flex-start; margin-top: 0; }

/* Contact rhythm (2026-09-27, audit C1). One step between heading, email,
   location and icons, measured ink to ink, not box to box: the h2's descender
   space, the email's 1.5 line-height and the icons' 44px hit areas each added
   invisible space of their own, so the steps measured 87 / 33 / 38px at 1440.
   Now all three are `--connect-step` (measured 36.4 / 36.8 / 36.8 at 1440,
   24.7 / 25.4 / 25.8 at 390). The envelope sat 10.6px below the email's
   baseline; `align-items: baseline` puts its ink on the baseline (0px). */
#contact {
  --connect-step: clamp(1.5rem, 2.5vw, 2.25rem);
}
#contact h2 {
  /* Less the h2's own descender space below its (descender-free) ink. */
  margin-bottom: calc(var(--connect-step) - 0.35em);
}
.connect-primary {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
}
.connect-primary .email-link {
  align-items: baseline;
  line-height: 1.1;
  padding: 0 0 6px;
  /* Less the space Onest's ascent leaves above the ascenders. */
  margin-top: -0.2em;
}
.connect-primary .connect-loc {
  margin: calc(var(--connect-step) - 6px) 0 0;
  line-height: 1.3;
}
.connect-primary .connect-social {
  /* 44px hit areas around 24px glyphs: 10px of each is air, not a step. */
  margin-top: calc(var(--connect-step) - 10px);
}
/* --- Collapse to single column on small screens --- */
@media (max-width: 820px) {
  .about-grid,
  .connect-grid { grid-template-columns: 1fr; }
  .about-visual { max-width: 420px; margin: 0 auto; aspect-ratio: 4 / 5; }
  .journey-row { grid-template-columns: 1fr; gap: 10px; }
  .journey-when { padding-top: 0; }
  .reel-more { text-align: left; }
}


/* ============================================================
   /work/ : THE FILM STRIP                          (2026-08-20)

   Full-bleed frames, type on the axis. The declared exception to DESIGN.md 1
   ("one image per section", "two images on the whole page"); everything else
   stays binding and is honoured below: Flat Rule (no box-shadow), Line Rule
   (accent is a line or a state, never a fill or a glow), no scroll reveals,
   square corners, no new radii, one left axis, no uppercase tracked
   micro-copy.

   Three prohibitions, each of which broke an earlier draft:

   1. NEVER scope band geometry to bare `.film-facade`. That class is also on
      the homepage reel, where an explicit height beats `aspect-ratio`: at
      1440x900 the site's most important poster would silently grow 583 ->
      684px and crop from 16:9 to 1.51:1. Everything geometric here is scoped
      to `.film-band` or `.film-row`.
   2. NEVER `contain: layout paint` on `.film`, and never `overflow: hidden`
      on a box whose border box equals the facade's. `a:focus-visible` draws
      `outline: 2px` at `outline-offset: 2px`, entirely OUTSIDE that box;
      either would clip the only focus ring on eight full-bleed keyboard
      targets to nothing. Clipping happens on `.film-frame`, inside the
      facade, and `:focus-within` lifts the band so the ring paints over its
      neighbours.
   3. NEVER `width: 100vw` or `margin-inline: calc(50% - 50vw)`. Both include
      the scrollbar (~15px measured) and cause horizontal scroll on any
      desktop with a classic one. Full bleed here is "a plain block child of
      the page"; where a full-bleed fill needs contained text, the inset is a
      PERCENTAGE of the parent, which excludes the scrollbar by construction.
   ============================================================ */

/* Visually hidden, still in the accessibility tree. Did not exist until now:
   an earlier draft shipped `<span class="sr-only">Play film. </span>` with no
   rule behind it, so it rendered as visible body copy on every band. Build
   gate 4 now fails if any emitted class has no rule in this file. */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

/* ---- masthead ------------------------------------------------------- */

/* There is no hero on this page, so `.section`'s 4rem minimum top padding
   would tuck the h1 under the fixed nav on short viewports. */
.work-masthead {
  padding-top: clamp(7.5rem, 12vw, 11rem);
  padding-bottom: clamp(2.5rem, 5vw, 4rem);
}

.work-intro {
  max-width: 62ch;
  margin: clamp(1rem, 2vw, 1.5rem) 0 0;
  color: var(--text-color-secondary);
}

/* The client jump index. Title-case names on a middot line, the same idiom
   as `.journey-skill`: NOT uppercase tracked micro-copy. The page's label
   budget is spent on the homepage and this page adds nothing to it. */
.work-jump {
  margin-top: clamp(1.5rem, 3vw, 2.25rem);
}

.work-jump ul {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  row-gap: 6px;
  margin: 0;
  padding: 0;
  list-style: none;
}

.work-jump li {
  margin: 0;
  padding: 0;
  font-weight: 400;
}

.work-jump li::before {
  content: none;
}

.work-jump li + li::before {
  content: "\00b7";
  /* The global `ul li::before` (line 239) is `position: absolute; left: 0`, for the
     site's custom bullets. Inherited here it pinned the middot behind the client
     name -- invisible, and contributing zero width -- so on a flex row with no
     column-gap every client name ran into the next: "JawwyLusineHungerStation".
     Caught on the rendered page, not in review: the content, colour and margin all
     computed correctly, which is exactly why it read as fine in the source. */
  position: static;
  display: inline;
  margin: 0 0.6em;
  color: rgba(255, 255, 255, 0.3);
}

.work-jump a {
  font-size: 0.9rem;
  letter-spacing: 0.02em;
  color: rgba(255, 255, 255, 0.65);
  border-bottom: 1px solid transparent;
  transition: color var(--dur-press) ease, border-color var(--dur-press) ease;
}

/* Accent as a line and a state. Ungated `:focus-visible`, hover behind the
   gate, per DESIGN.md 6. Neither moves anything. */
.work-jump a:focus-visible {
  color: var(--vusion-blue-bright);
}

@media (hover: hover) {
  .work-jump a:hover {
    color: var(--vusion-blue-bright);
    border-bottom-color: var(--border-accent);
  }
}

.work-jump a:active {
  opacity: 0.6;
}

/* The 1px rule between the strip and the index. A direct child of <body>, so
   it carries the measure itself. `margin-inline`, not `padding-inline`: a
   border-top paints across the BORDER box, which padding does not inset. The
   percentage resolves against <body>'s content width (no scrollbar); at 1440
   it puts the left edge at 202px, exactly where `.container`'s text sits. */
.work-rule {
  border: 0;
  border-top: 1px solid var(--border-light);
  height: 0;
  margin-block: clamp(3rem, 7vw, 5.5rem);
  margin-inline: max(var(--gutter), calc((100% - var(--measure)) / 2 + var(--gutter)));
}

/* ---- the strip ------------------------------------------------------ */

/* A plain block child of <body>: naturally full width, with no 100vw and no
   negative margin, so it cannot produce the scrollbar-width horizontal
   overflow those cause. `flow-root` keeps child margins from escaping. */
.film-strip {
  display: flow-root;
  margin: 0;
}

/* Shared by bands and index rows. `position: relative` is load-bearing: it
   is the containing block for `.film-band .film-stage`. NOT `contain` of any
   kind: see note 2 in the header. */
.film {
  position: relative;
}

/* `#film-<slug>` sits on the <article>/<li>, which is not `section[id]`, so
   it misses the global scroll-margin and every jump-index link would land
   under the fixed nav. */
.film[id] {
  scroll-margin-top: 80px;
}

/* A focused band paints its outline 2px OUTSIDE its own border box, i.e.
   over its neighbours. Positioned siblings with `z-index: auto` paint in DOM
   order, so without this the ring's bottom 2px would be covered by the next
   band's frame. */
.film-band:focus-within {
  z-index: 2;
}

/* THE BAND HEIGHT.
   `min-height`, never `height`: `.hero` uses min-height for the same reason,
   and `save-the-children-education` is a hero band with the longest title in
   the enabled set. In a fixed-height band a three-line wrap is clipped
   instead of pushing.
   `56.25vw` only governs between aspect 1.333 and 1.351: the portrait media
   query at the end of this block owns everything below 4:3 and `76svh` wins
   above 1.351. It is a floor, not a design parameter. */
.film-band {
  /* THE SCRIM ALPHA. Bound to a property so the proof and the paint cannot
     drift apart. Compositing rgba(8,8,8,a) over pure white gives
     255(1-a) + 8a; at a = 0.70 the backdrop can never exceed rgb(82,82,82),
     which is 7.81:1 for #fff and 6.20:1 for rgba(255,255,255,.85). No sRGB
     colour composites brighter than white, so no frame can break it. That
     proof is for a FLAT fill and only holds where the fill is flat: see
     `.film-band .film-plate`. */
  --film-scrim: 0.7;
  /* The hover/focus poster zoom, as a property so reduced motion can zero it
     in one place rather than in four selectors. */
  --film-zoom: 1.015;
  display: flex;
  min-height: min(56.25vw, 76vh);
}

@supports (height: 1svh) {
  .film-band {
    min-height: min(56.25vw, 76svh);
  }
}

@media (prefers-reduced-motion: reduce) {
  .film-band {
    --film-zoom: 1;
  }
}

/* Shared by the band facade, the row facade and the homepage reel facade.
   Deliberately holds nothing geometric: `.reel-facade` (styles.css, featured
   reel) also carries this class, and anything sized here would override it
   by source order and crop the homepage poster. */
.film-facade {
  position: relative;
  display: block;
}

/* In flow, and stretched by the band's flex box, so a long title grows the
   band instead of overflowing it. */
.film-band > .film-facade {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: flex-end;
}

/* <picture> is inline by default, which would leave the img's height:100%
   resolving against nothing. Same fix as `.about-visual picture`.
   This is the ONLY clipping box in the band: it sits inside the facade, so
   the focus ring outside the facade is untouched. */
.film-frame {
  position: absolute;
  inset: 0;
  z-index: 0;
  display: block;
  overflow: hidden;
  background-color: #000;
}

/* The committed reel treatment, reused: held at reduced chroma on a #080808
   page, full colour when you reach for it. `opacity` is the wrong variable
   (dimming orange yields muddy orange); the shout is chroma. */
.film-frame img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  filter: saturate(0.62) brightness(0.88);
  transition: filter var(--dur-slow) ease, transform var(--dur-slow) var(--ease-out);
}

/* A circle is the committed `rounded.full` token and what `.reel-play`
   already uses; 8px stays exactly one thing on this site (the inset homepage
   reel), so no new radius enters here. No backdrop-filter, unlike
   `.reel-play`: eight blurred discs over eight full-bleed frames is real GPU
   cost for a 4px blur behind a disc that is already 65% opaque. The 1px 40%
   white ring is `.reel-play`'s committed value, not a new one (--border-light
   at 15% is invisible over a lit frame). No box-shadow (Flat Rule).
   Non-text contrast of the glyph on the disc over a white frame: 6.44:1. */
.film-play {
  --film-play-t: translate(-50%, -50%);
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 2;
  transform: var(--film-play-t);
  width: 84px;
  height: 84px;
  border-radius: 50%;
  background: rgba(8, 8, 8, 0.65);
  border: 1px solid rgba(255, 255, 255, 0.4);
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.6rem;
  color: #fff;
  transition: transform var(--dur-slow) var(--ease-out), background-color var(--dur-slow) ease;
}

/* Optical, not geometric: the triangle's visual centre of mass sits left of
   its bounding box centre. */
.film-play svg {
  display: block;
  margin-left: 0.07em;
}

/* THE PLATE, AND THE SCRIM.
   The plate paints its OWN flat rgba(8,8,8,.70) background. That is the
   whole correction: the flat region is the plate's border box, so it covers
   the ink box by construction rather than by an arithmetic that can drift.
   A gradient cannot do this job. A ramp reaches 0.70 at exactly one
   y-coordinate, and over a two-line plate it leaves the title sitting on
   a ~0.35 composite, which is 2.34:1: a flat WCAG AA failure. The gradient
   below is used ONLY to dissolve this flat region upward into the frame,
   and no ink ever enters it.

   At 1440 CSS px (title 23.04px, meta 12.96px: the 18.4/13.6 pair does not
   coexist at any viewport) the flat region is
     21.6 pad-top + 29.95 title line + 8 + 18.14 meta line + 36 pad-bottom
     = 113.7px,
   with the title's topmost ink ~25px below its top edge and the meta's
   descenders ~36px above its bottom. A three-line title grows it to ~173.5px
   and those margins are unchanged, because they are padding, not constants.

   Contrast is computed at brightness(1) (posters go full on hover, so the
   hover state is the worst case) and against pure white.

   FULL BLEED WITHOUT 100vw: `.container`'s max-width is dropped and its
   effect reproduced with a percentage inset, so the fill reaches both edges
   while the type stays on the one axis. At 1440 that is (1440-1100)/2 + 32 =
   202px, exactly where `.container` puts its text; below 1100px + gutters the
   max() falls back to the plain gutter, as `.container` does. The percentage
   resolves against the facade's content box, excluding the scrollbar. */
.film-band .film-plate {
  position: relative;
  z-index: 1;
  flex: 1 1 auto;
  width: 100%;
  max-width: none;
  margin: 0;
  padding-inline: max(var(--gutter), calc((100% - var(--measure)) / 2 + var(--gutter)));
  padding-block: clamp(1.25rem, 2.4vh, 2rem) clamp(1.5rem, 4vh, 2.75rem);
  background-color: rgba(8, 8, 8, var(--film-scrim));
}

/* The dissolve. Sits directly ON TOP of the flat region (`bottom: 100%`), so
   there is no seam and no gap where alpha could dip under 0.70 behind ink.
   Ends at 0 so the frame above is untouched. */
.film-band .film-plate::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;
  height: clamp(64px, 12vh, 140px);
  background: linear-gradient(
    to top,
    rgba(8, 8, 8, var(--film-scrim)) 0%,
    rgba(8, 8, 8, 0) 100%
  );
  pointer-events: none;
}

/* ---- film type, shared by bands and rows ---------------------------- */

/* The Title scale. `h3` globally carries margin-bottom: 20px and
   color: var(--light-accent); both are reset here on purpose. */
.film-title {
  margin: 0;
  font-size: clamp(1.15rem, 1.6vw, 1.45rem);
  font-weight: 500;
  line-height: 1.3;
  letter-spacing: -0.5px;
  color: var(--text-color);
  transition: color var(--dur-press) ease;
}

/* The Label SCALE, with none of the Label's uppercase or 3px tracking:
   client names are Title-case middot copy on this page. `p` globally carries
   margin-bottom: 1.5rem and font-weight 300.
   rgba(255,255,255,.85) is --light-accent, and it is the floor: at 0.5 the
   same text over the scrim measures 3.30:1 and fails. */
.film-meta {
  margin: 0.5rem 0 0;
  font-size: clamp(0.72rem, 0.9vw, 0.85rem);
  font-weight: 400;
  line-height: 1.4;
  letter-spacing: 0.02em;
  color: var(--light-accent);
}

.film-meta span + span::before {
  content: "\00b7";
  margin: 0 0.6em;
  color: rgba(255, 255, 255, 0.55);
}

.film-dur {
  font-variant-numeric: tabular-nums;
}

/* ---- band interaction ------------------------------------------------ */

/* Movement-hover inside the gate (DESIGN.md 6). The zoom is bound to
   --film-zoom, which reduced motion sets to 1. */
@media (hover: hover) {
  .film-band .film-facade:hover .film-frame img {
    filter: saturate(1) brightness(1);
    transform: scale(var(--film-zoom));
  }

  .film-band .film-facade:hover .film-play {
    transform: var(--film-play-t) scale(1.08);
    background: rgba(8, 8, 8, 0.85);
  }
}

/* The `:focus-visible` equivalent, OUTSIDE the gate, so a keyboard user gets
   the same reward as a mouse user. The ring itself comes from the global
   a:focus-visible rule and is deliberately not clipped by anything here. */
.film-band .film-facade:focus-visible .film-frame img {
  filter: saturate(1) brightness(1);
  transform: scale(var(--film-zoom));
}

.film-band .film-facade:focus-visible .film-play {
  transform: var(--film-play-t) scale(1.08);
  background: rgba(8, 8, 8, 0.85);
}

.film-band .film-facade:active .film-play {
  transform: var(--film-play-t) scale(0.96);
}

/* ---- the index ------------------------------------------------------- */

/* The index section. The strip above it already carries the page's images;
   this half is type only, on the same one left axis via `.container`. */
.work-index {
  padding-top: 0;
}

/* Rows separated by 1px rules. Not a grid, not cards: no border beyond the
   rule, no radius, no background, no image, no shadow. Rhymes with
   `.portfolio-index` on the homepage on purpose. */
.film-rows {
  list-style: none;
  max-width: var(--measure);
  margin: clamp(40px, 5vw, 64px) 0 0;
  padding: 0;
}

.film-row {
  border-top: 1px solid var(--border-light);
  margin: 0;
  padding: 0;
}

.film-row:last-child {
  border-bottom: 1px solid var(--border-light);
}

.film-row::before {
  content: none;
}

.film-row-link,
.film-row-inner {
  display: block;
  padding: clamp(24px, 3vw, 36px) 0;
}

/* Colour only, 150ms, no movement. An index is scanned tens of times per
   visit; a lift fails the frequency test and lies about the affordance. */
.film-row-link {
  transition: color var(--dur-press) ease;
}

.film-row-link:focus-visible .film-title {
  color: var(--vusion-blue-bright);
}

@media (hover: hover) {
  .film-row-link:hover .film-title {
    color: var(--vusion-blue-bright);
  }
}

.film-row-link:active {
  opacity: 0.6;
}

/* A film with no encode yet. A metadata row: nothing clickable, nothing that
   looks clickable, and quieter than a real row so the difference is legible
   without a badge. 8.48:1 on #080808. */
.film-pending .film-title {
  color: rgba(255, 255, 255, 0.65);
}

/* ---- the player mount ------------------------------------------------ */

/* The `[hidden]` guard is not optional: a bare `.film-stage { display: block }`
   beats the UA's `[hidden] { display: none }` on specificity and every stage
   on the page would render empty and open. */
.film-stage {
  display: block;
}

.film-stage[hidden] {
  display: none;
}

/* `contain`, never `cover`: a 16:9 film inside a 2.1:1 band must letterbox,
   not lose its edges. */
.film-stage video {
  display: block;
  width: 100%;
  height: 100%;
  background: #000;
  object-fit: contain;
}

/* In a band the stage covers the facade exactly, so the film plays where the
   frame was rather than pushing the page. */
.film-band .film-stage {
  position: absolute;
  inset: 0;
  z-index: 3;
  background: #000;
}

/* ===== The one moment: a frame coming to life =========================
   Everything else on this page is a colour state. This is the only real
   transition, so it gets the budget and nothing else asks for attention.

   The stage crossfades in only once the video can PAINT (film.js adds
   .film-lit on loadeddata/playing). Before that the poster stays lit
   underneath, which is what removes the old poster -> black -> film cut.

   scale starts at 1.012, never 0: nothing in the world appears from
   nothing, and on a full-bleed frame even 1.2% reads as the image
   settling rather than a slide. No shadow, no glow, no accent fill --
   the Flat Rule and the Line Rule both still bind here. */
.film-band .film-stage {
  opacity: 0;
  transform: scale(1.012);
  transform-origin: center;
  transition:
    opacity var(--dur-slow) var(--ease-out),
    transform var(--dur-slow) var(--ease-out);
}
.film-band.film-lit .film-stage {
  opacity: 1;
  transform: scale(1);
}

/* The poster dims and softens as the film takes over. The blur is the
   bridge: without it you see two distinct images overlapping for 300ms,
   which reads as a swap. 4px is enough to fuse them and cheap enough to
   stay smooth on a 1920 frame. Kept well under the 20px Safari cost. */
.film-band .film-facade {
  transition:
    opacity var(--dur-slow) var(--ease-out),
    filter var(--dur-slow) var(--ease-out);
}
.film-band.film-lit .film-facade {
  opacity: 0;
  filter: blur(4px);
}

/* Exit at ~65% of entry. The visitor has already decided to stop; the
   system should get out of the way rather than perform the reverse. */
.film-band:not(.film-lit) .film-stage {
  transition-duration: 195ms;
}
.film-band:not(.film-lit) .film-facade {
  transition-duration: 195ms;
}

/* The disc is feedback, not decoration, so it runs at feedback speed.
   The frame keeps the slower 300ms because it is a big tonal move; the
   disc under the cursor has to answer immediately. */
.film-play {
  transition:
    transform 180ms var(--ease-out),
    background-color 180ms ease,
    border-color 180ms ease;
}
.film-band .film-facade:active .film-play {
  transform: var(--film-play-t) scale(0.94);
  transition-duration: 120ms;
}

/* ===== Touch devices were never seeing the work in colour ==============
   The rest treatment (saturate .62 / brightness .88) was unconditional, but
   the full-colour state lived only inside `@media (hover: hover)`. On every
   phone and tablet that meant a permanently dimmed reel with no way to reach
   full colour -- on a director's portfolio, where most traffic is mobile.
   The dim is a HOVER AFFORDANCE: it only means anything if reaching for the
   frame can undo it. Where nothing can hover, the work shows as it is. */
@media (hover: none), (pointer: coarse) {
  .film-band .film-frame img,
  .film-row .film-frame img,
  .reel-facade img {
    filter: none;
  }
}

/* ===== Each frame arrives as a title card =============================
   Roy asked for the films to animate in on scroll. DESIGN.md bans scroll
   reveals by name, and it is right about why: the previous one gated
   visibility on a class, so headless renders and hidden tabs shipped a blank
   page. Two rules make this one safe.

   1. VISIBLE IS THE DEFAULT. Nothing here is hidden by a stylesheet. The
      hidden state needs `.film-arm`, which only film.js adds. No JS, dead
      JS, no IntersectionObserver -> every band is simply there.
   2. film.js arms ONLY bands that are below the fold when it runs, and
      unobserves each after it fires. Nothing already on screen flashes out
      and back in, and nothing can be left armed with no observer to release
      it.

   The gesture is the site's own, not a generic fade-and-rise: the hero
   dissolves from black on `linear` (a projector dissolve IS linear) and
   settles from scale(1.03) on --ease-cine. A film strip where each frame
   arrives that way is the title card repeating, which is the north star. */
.film-band.film-arm .film-frame {
  opacity: 0;
  transform: scale(1.03);
}
.film-band.film-arm .film-plate {
  opacity: 0;
  transform: translateY(6px);
}
.film-band.film-in .film-frame {
  opacity: 1;
  transform: scale(1);
  transition:
    opacity 700ms linear,
    transform 900ms var(--ease-cine);
}
.film-band.film-in .film-plate {
  opacity: 1;
  transform: translateY(0);
  transition:
    opacity var(--dur-entrance) var(--ease-out) 120ms,
    transform var(--dur-entrance) var(--ease-out) 120ms;
}

/* The index rows are a genuine list, so a sibling stagger is legitimate
   where a whole-section reveal would not be. 55ms apart, capped at 8 by
   film.js: ten items at 55ms is already 550ms of waiting. */
.film-row.film-arm .film-row-inner,
.film-row.film-arm .film-row-link {
  opacity: 0;
  transform: translateY(8px);
}
.film-row.film-in .film-row-inner,
.film-row.film-in .film-row-link {
  opacity: 1;
  transform: translateY(0);
  transition:
    opacity var(--dur-entrance) var(--ease-out) calc(var(--i, 0) * 55ms),
    transform var(--dur-entrance) var(--ease-out) calc(var(--i, 0) * 55ms);
}

/* Reduced motion: the reveal becomes a plain crossfade with no movement and
   no delay. Fewer and gentler, not zero. */
@media (prefers-reduced-motion: reduce) {
  .film-band.film-arm .film-frame,
  .film-band.film-arm .film-plate,
  .film-row.film-arm .film-row-inner,
  .film-row.film-arm .film-row-link {
    transform: none;
  }
  .film-band.film-in .film-frame,
  .film-band.film-in .film-plate,
  .film-row.film-in .film-row-inner,
  .film-row.film-in .film-row-link {
    transform: none;
    transition-delay: 0ms;
    transition-duration: 200ms;
  }
}

/* Reduced motion: keep the crossfade, drop the movement and the blur.
   Fewer and gentler, not zero -- the opacity change is what explains that
   one thing became another, so removing it would cost comprehension. */
@media (prefers-reduced-motion: reduce) {
  .film-band .film-stage {
    transform: none;
  }
  .film-band.film-lit .film-stage {
    transform: none;
  }
  .film-band.film-lit .film-facade {
    filter: none;
  }
}

.film-row .film-stage {
  aspect-ratio: 16 / 9;
  background: #000;
}

/* The homepage reel's player mount. Player v2 (2026-09-27) lays it OVER the
   facade instead of replacing it: same box, same committed 8px radius (which
   stays exactly one thing on this site), and the facade keeps its place in the
   layout, so on phones the words under the picture stay put while it plays.
   It crossfades in once its poster, the same decoded image, can paint. */
.reel-feature .film-stage {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 3;
  width: 100%;
  aspect-ratio: 16 / 9;
  border: 1px solid var(--border-light);
  border-radius: 8px;
  overflow: hidden;
  background: #000;
  opacity: 0;
  transition: opacity var(--dur-slow) var(--ease-out);
}
.reel-feature.film-lit .film-stage {
  opacity: 1;
}

/* ---- portrait-ish viewports ------------------------------------------ */

/* Keyed on ASPECT, not width, because the problem IS aspect: a 219px band
   with type over its bottom third is unreadable at any alpha. Reuses the
   hero's committed answer: TAKE THE COPY OFF THE IMAGE. The frame becomes a
   16:9 band, the plate sits below it, and the scrim is removed entirely.

   The plate is `transparent`, not an explicit flat #080808. The page ground
   here IS #080808 under the committed radial gradient, measuring 18.4:1 (at
   its lightest, #141414) to 20.0:1 for white. Painting an opaque #080808
   rectangle would lay a 12/255 hard-edged band across the strip at mid-page:
   a visible defect bought with 1.6:1 of headroom already 4x the requirement.

   The facade becomes a two-row grid so `.film-play` can share the frame's
   cell and stay centred on the IMAGE, not on image-plus-copy. It is a grid
   item here, not absolutely positioned, so there is no percentage-of-height
   trap. */
@media (max-aspect-ratio: 4 / 3) {
  .film-band {
    min-height: 0;
  }

  .film-band > .film-facade {
    display: grid;
    grid-template-columns: 1fr;
    align-items: stretch;
  }

  .film-band .film-frame {
    position: static;
    grid-area: 1 / 1;
    width: 100%;
    aspect-ratio: 16 / 9;
  }

  .film-play {
    --film-play-t: translate(0, 0);
    position: static;
    grid-area: 1 / 1;
    align-self: center;
    justify-self: center;
    width: 64px;
    height: 64px;
    font-size: 1.25rem;
  }

  .film-band .film-plate {
    grid-area: 2 / 1;
    background-color: transparent;
    padding-block: clamp(1rem, 3vw, 1.5rem) clamp(2rem, 7vw, 3rem);
  }

  .film-band .film-plate::before {
    content: none;
  }
}

/* ===== Self-hosted homepage reel + on-image film text (2026-09-27) =====
   Roy: the words live ON the frame and vanish on play; the longer note sits
   behind an "Info" disclosure. film.js hides the facade (and with it every
   word on the image) the moment the player mounts, and adds .film-playing to
   the facade's parent, which is what hides the Info control below. */
.reel-facade[hidden] {
  display: none;
}
.reel-facade picture {
  display: block;
  width: 100%;
  height: 100%;
}

/* Reel type on the image: title, client · year, one line. The scrim is taller
   than the old one-line caption's so the extra lines never sit on bare frame. */
.reel-feature {
  position: relative;
}
.reel-feature .reel-meta {
  padding: clamp(56px, 12%, 110px) clamp(18px, 3vw, 32px) clamp(16px, 2.4vw, 26px);
  background: linear-gradient(transparent, rgba(8, 8, 8, 0.55) 30%, rgba(8, 8, 8, 0.9));
}
.reel-title {
  display: block;
  font-size: clamp(1.15rem, 2.1vw, 1.6rem);
  font-weight: 500;
  line-height: 1.25;
  letter-spacing: -0.3px;
}
.reel-sub {
  display: block;
  margin-top: 4px;
  font-size: clamp(0.78rem, 1vw, 0.9rem);
  color: var(--light-accent);
}
.reel-logline {
  display: block;
  margin-top: 10px;
  max-width: 60ch;
  font-size: clamp(0.8rem, 1.05vw, 0.95rem);
  font-weight: 300;
  line-height: 1.5;
  color: var(--light-accent);
}
/* Phones: a ~200px frame cannot carry three lines of type. Same answer as the
   /work/ bands on tall screens: take the copy OFF the image and set it below,
   on the page ground. It still leaves with the facade on play. */
@media (max-width: 600px) {
  .reel-facade {
    aspect-ratio: auto;
    height: auto;
  }
  .reel-facade picture {
    aspect-ratio: 16 / 9;
    height: auto;
  }
  .reel-feature .reel-meta {
    display: block;
    position: static;
    padding: 14px 16px 16px;
    background: #080808;
  }
  .reel-play {
    top: calc((100vw - 2 * var(--gutter)) * 9 / 32);
    width: 64px;
    height: 64px;
    font-size: 1.25rem;
  }
}

/* Logline on a /work/ band, under client · year · duration. */
.film-logline {
  margin: 0.6rem 0 0;
  max-width: 60ch;
  font-size: clamp(0.8rem, 1vw, 0.95rem);
  font-weight: 300;
  line-height: 1.5;
  color: var(--light-accent);
}

/* ---- the Info disclosure ------------------------------------------------ */
/* Top-right of the frame. Outside the facade link, so opening it never plays. */
.film-info {
  position: absolute;
  top: clamp(12px, 2vw, 20px);
  right: clamp(12px, 2vw, 20px);
  z-index: 4;
  max-width: min(460px, calc(100% - 24px));
  text-align: left;
}
.film-playing > .film-info {
  display: none;
}
.film-info > summary {
  display: inline-block;
  float: right;
  list-style: none;
  cursor: pointer;
  padding: 6px 14px;
  font-size: 0.78rem;
  letter-spacing: 0.04em;
  color: #fff;
  background: rgba(8, 8, 8, 0.6);
  border: 1px solid rgba(255, 255, 255, 0.35);
  border-radius: 999px;
  transition: background-color 180ms ease, border-color 180ms ease;
}
.film-info > summary::-webkit-details-marker {
  display: none;
}
@media (hover: hover) {
  .film-info > summary:hover { background: rgba(8, 8, 8, 0.85); border-color: var(--border-accent); }
}
.film-info[open] > summary {
  background: rgba(8, 8, 8, 0.9);
  border-color: var(--border-accent);
}
.film-info-body {
  clear: both;
  margin-top: 8px;
  padding: 16px 18px;
  max-height: min(60vh, 420px);
  overflow-y: auto;
  background: #0c0c0c;
  border: 1px solid var(--border-light);
}
.film-info-body p {
  margin: 0 0 0.8rem;
  font-size: 0.88rem;
  font-weight: 300;
  line-height: 1.6;
  color: var(--light-accent);
}
.film-info-body .film-studio {
  margin: 1rem 0 0;
  font-size: 0.8rem;
  font-weight: 400;
  color: rgba(255, 255, 255, 0.7);
}
.film-studio a {
  color: var(--text-color);
  border-bottom: 1px solid var(--border-light);
  transition: color 0.25s ease, border-color 0.25s ease;
}
@media (hover: hover) {
  .film-studio a:hover { color: var(--vusion-blue-bright); border-bottom-color: var(--border-accent); }
}

/* ============================================================
   PLAYER v2                                           (2026-09-27)
   The site's own controls, shared by the homepage reel, the /work/ bands and
   the index rows (film.js builds the markup, one player at a time). Spec:
   AUDIT-DESIGN 2026-09-27 §3.3-3.4.

   Every rule still binds: Onest and white; the accent only as a 1px line or a
   state (the loading line, hover and focus); square corners (the two circles
   below reuse the committed `rounded.full` of `.reel-play`); no box-shadow, no
   glow. Only opacity and transform animate.
   ============================================================ */
.fp {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: #000;
  color: #fff;
  font-family: "Onest", sans-serif;
  -webkit-tap-highlight-color: transparent;
  outline: none;
}
.fp:focus-visible {
  outline: 2px solid var(--vusion-blue);
  outline-offset: -2px;
}
.fp.is-idle {
  cursor: none;
}
.fp video,
.fp .fp-cover,
.fp .fp-freeze {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: contain;
  background: #000;
}

/* The poster holds the frame until the first decoded video frame is presented
   (film.js adds .has-frame from requestVideoFrameCallback, or `playing` plus a
   frame). No black between the poster and the film. */
.fp-cover {
  z-index: 1;
  opacity: 1;
  transition: opacity 220ms var(--ease-out);
  pointer-events: none;
}
.fp.has-frame .fp-cover {
  opacity: 0;
}
.fp-freeze {
  z-index: 1;
  pointer-events: none;
}

/* Legibility under the controls, and the dim behind the pause/end copy. It
   rides with the controls, so an idle film is the film and nothing else. */
.fp-veil {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background:
    linear-gradient(to top, rgba(8, 8, 8, 0.72) 0, rgba(8, 8, 8, 0.3) 72px, transparent 128px);
  transition: opacity 300ms var(--ease-out);
}
.fp.is-paused .fp-veil,
.fp.is-ended .fp-veil {
  background:
    linear-gradient(to top, rgba(8, 8, 8, 0.9) 0, rgba(8, 8, 8, 0.55) 34%, rgba(8, 8, 8, 0.15) 70%);
}
.fp.is-ended .fp-veil {
  background-color: rgba(8, 8, 8, 0.45);
}

.fp-surface {
  position: absolute;
  inset: 0;
  z-index: 3;
  /* No double-tap zoom on the picture: a double tap there seeks. */
  touch-action: manipulation;
}

/* The paused disc: `.reel-play`'s committed values, feedback only. */
.fp-bigplay {
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 4;
  width: 72px;
  height: 72px;
  margin: -36px 0 0 -36px;
  border-radius: 50%;
  background: rgba(8, 8, 8, 0.65);
  border: 1px solid rgba(255, 255, 255, 0.4);
  display: flex;
  align-items: center;
  justify-content: center;
  pointer-events: none;
  opacity: 0;
  transform: scale(0.96);
  transition: opacity 180ms var(--ease-out), transform 180ms var(--ease-out);
}
.fp-bigplay svg {
  width: 26px;
  height: 26px;
  margin-left: 3px;
}
/* Shown whenever the film is paused and not loading, including when the
   browser refused play() before a single frame was decoded. */
.fp.is-paused:not(.is-loading) .fp-bigplay {
  opacity: 1;
  transform: scale(1);
}

/* Title, client · year and logline come back on pause (the facade's own copy;
   nothing new is written). Hidden while playing, as the facade's words were. */
.fp-overlay {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 56px;
  z-index: 4;
  padding: 0 clamp(16px, 3vw, 32px);
  pointer-events: none;
  text-align: left;
  opacity: 0;
  transform: translateY(4px);
  transition: opacity 220ms var(--ease-out), transform 220ms var(--ease-out);
}
.fp.is-paused .fp-overlay,
.fp.is-ended .fp-overlay {
  opacity: 1;
  transform: none;
}
.fp-title {
  display: block;
  font-size: clamp(1.15rem, 2.1vw, 1.6rem);
  font-weight: 500;
  line-height: 1.25;
  letter-spacing: -0.3px;
}
.fp-sub {
  display: block;
  margin-top: 4px;
  font-size: clamp(0.78rem, 1vw, 0.9rem);
  color: var(--light-accent);
}
.fp-logline {
  display: block;
  margin-top: 8px;
  max-width: 60ch;
  font-size: clamp(0.8rem, 1.05vw, 0.95rem);
  font-weight: 300;
  line-height: 1.5;
  color: var(--light-accent);
}

/* The end card. Replay, plus the onward path: "Next film" on /work/, "All
   films" on the homepage. Outlined, never filled (Line Rule). */
.fp-end {
  position: absolute;
  top: 50%;
  left: 50%;
  z-index: 5;
  display: flex;
  gap: 12px;
  transform: translate(-50%, -50%);
  opacity: 0;
  visibility: hidden;
  transition: opacity 220ms var(--ease-out), visibility 0s linear 220ms;
}
.fp.is-ended .fp-end {
  opacity: 1;
  visibility: visible;
  transition: opacity 220ms var(--ease-out), visibility 0s;
}
.fp-end-act {
  display: inline-flex;
  flex-direction: column;
  align-items: flex-start;
  justify-content: center;
  gap: 2px;
  min-height: 44px;
  padding: 10px 18px;
  background: rgba(8, 8, 8, 0.55);
  border: 1px solid rgba(255, 255, 255, 0.4);
  border-radius: 0;
  color: #fff;
  font: inherit;
  font-size: 0.9rem;
  letter-spacing: 0.04em;
  cursor: pointer;
  transition: color 150ms ease, border-color 150ms ease, transform 150ms var(--ease-out);
}
.fp-replay {
  flex-direction: row;
  align-items: center;
  gap: 8px;
}
.fp-end-act small {
  max-width: 22ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  font-size: 0.75rem;
  color: rgba(255, 255, 255, 0.7);
  letter-spacing: 0;
}
@media (hover: hover) {
  .fp-end-act:hover {
    color: var(--vusion-blue-bright);
    border-color: var(--vusion-blue);
  }
}
.fp-end-act:focus-visible {
  color: var(--vusion-blue-bright);
  border-color: var(--vusion-blue);
}
.fp-end-act:active {
  transform: scale(0.97);
}

/* ---- the control bar -------------------------------------------------- */
.fp-controls {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 6;
  display: flex;
  align-items: center;
  gap: 4px;
  height: 52px;
  padding: 0 clamp(6px, 1.2vw, 14px);
  opacity: 1;
  transition: opacity 180ms var(--ease-out);
}
/* Auto-hide: only while playing and the pointer has been still for 2.5 s
   (film.js). Paused, ended or keyboard-focused, the bar stays. */
.fp.is-playing.is-idle .fp-controls,
.fp.is-playing.is-idle .fp-veil {
  opacity: 0;
  transition-duration: 420ms;
}
.fp.is-playing.is-idle .fp-controls {
  pointer-events: none;
}

.fp-btn {
  flex: none;
  width: 40px;
  height: 40px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  background: none;
  border: 0;
  border-radius: 0;
  color: #fff;
  cursor: pointer;
  transition: color 150ms ease, transform 150ms var(--ease-out);
}
.fp-btn svg {
  display: block;
}
@media (hover: hover) {
  .fp-btn:hover {
    color: var(--vusion-blue-bright);
  }
}
.fp-btn:active {
  transform: scale(0.92);
}
.fp-btn:focus-visible,
.fp-track:focus-visible {
  outline: 2px solid var(--vusion-blue);
  outline-offset: -2px;
}

.fp-time {
  flex: none;
  min-width: 3.2em;
  font-size: 0.8rem;
  font-variant-numeric: tabular-nums;
  letter-spacing: 0.02em;
  color: rgba(255, 255, 255, 0.85);
  text-align: center;
  user-select: none;
  -webkit-user-select: none;
}

/* The line. A tall invisible hit area around a 2px rail; it thickens on
   hover, focus and scrub. Progress is white; the accent is the 1px loading
   line and the focus state. */
.fp-track {
  position: relative;
  flex: 1 1 auto;
  min-width: 60px;
  height: 32px;
  margin: 0 8px;
  cursor: pointer;
  touch-action: none;
}
.fp-rail {
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  height: 2px;
  margin-top: -1px;
  overflow: hidden;
  /* 0.4 white over a 0.5 dark underlay, cased by a 1px dark line: the rail
     keeps a >=3:1 boundary on pale and dark frames alike. */
  background: linear-gradient(rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.4)), rgba(8, 8, 8, 0.5);
  box-shadow: 0 0 0 1px rgba(8, 8, 8, 0.6);
  transform: scaleY(1);
  transition: transform 150ms var(--ease-out);
}
/* Thickens under the pointer, on keyboard focus and while scrubbing. The hover
   half is gated: on touch a tap leaves :hover stuck and the rail would stay fat. */
@media (hover: hover) {
  .fp-track:hover .fp-rail { transform: scaleY(2); }
  .fp-track:hover .fp-head { opacity: 1; }
}
.fp-track:focus-visible .fp-rail,
.fp.is-scrubbing .fp-rail {
  transform: scaleY(2);
}
.fp-buffer,
.fp-progress {
  position: absolute;
  inset: 0;
  transform: scaleX(0);
  transform-origin: left center;
}
.fp-buffer {
  background: rgba(255, 255, 255, 0.28);
}
.fp-progress {
  background: #fff;
}
.fp-head {
  position: absolute;
  left: -1px;
  top: 50%;
  width: 2px;
  height: 14px;
  margin-top: -7px;
  background: #fff;
  opacity: 0;
  transition: opacity 150ms ease;
  pointer-events: none;
}
.fp-track:focus-visible .fp-head,
.fp.is-scrubbing .fp-head {
  opacity: 1;
}
.fp-tip {
  position: absolute;
  left: 0;
  bottom: 100%;
  padding: 3px 7px;
  background: #080808;
  border: 1px solid var(--border-light);
  font-size: 0.75rem;
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  pointer-events: none;
  opacity: 0;
  transition: opacity 120ms ease;
}
.fp.is-tip .fp-tip {
  opacity: 1;
}

/* Waiting / stalled: a 1px accent line travelling the rail. */
.fp-loading {
  position: absolute;
  left: 0;
  top: 50%;
  width: 28%;
  height: 1px;
  margin-top: -0.5px;
  background: var(--vusion-blue);
  opacity: 0;
  transform: translateX(-100%);
}
.fp.is-loading .fp-loading {
  opacity: 1;
  animation: fp-loading 1.1s linear infinite;
}
@keyframes fp-loading {
  from { transform: translateX(-100%); }
  to { transform: translateX(360%); }
}
/* Reduced motion: the line stops travelling and breathes in place instead.
   Fewer and gentler, not zero: it is the only sign the film is loading. The
   !important is needed to outrank the global reduced-motion reset. */
@media (prefers-reduced-motion: reduce) {
  .fp.is-loading .fp-loading {
    width: 100%;
    transform: none;
    animation: fp-breathe 1.6s ease-in-out infinite alternate !important;
  }
  @keyframes fp-breathe {
    from { opacity: 0.25; }
    to { opacity: 1; }
  }
}

/* Double-tap seek acknowledgement (touch). */
.fp-ack {
  position: absolute;
  top: 50%;
  z-index: 4;
  width: 64px;
  height: 64px;
  margin-top: -32px;
  border-radius: 50%;
  background: rgba(8, 8, 8, 0.55);
  border: 1px solid rgba(255, 255, 255, 0.35);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  font-size: 0.72rem;
  font-variant-numeric: tabular-nums;
  pointer-events: none;
  opacity: 0;
  transform: scale(0.94);
  transition: opacity 400ms ease, transform 400ms var(--ease-out);
}
.fp-ack b {
  font-weight: 500;
}
.fp-ack--back { left: 16.66%; margin-left: -32px; }
.fp-ack--fwd { left: 83.33%; margin-left: -32px; }
.fp-ack.is-on {
  opacity: 1;
  transform: scale(1);
  transition-duration: 120ms;
}

/* Fullscreen: the player fills the screen; nothing else changes. */
.fp:fullscreen { width: 100%; height: 100%; }
.fp:-webkit-full-screen { width: 100%; height: 100%; }

/* The mounted film's facade is out of the tab order and the click path. */
.film-playing > .film-facade {
  pointer-events: none;
}

/* ---- phones ---------------------------------------------------------- */
@media (max-width: 600px) {
  .fp-controls {
    height: 48px;
    gap: 0;
    padding: 0 2px;
  }
  .fp-btn {
    width: 44px;
    height: 44px;
  }
  .fp-track {
    height: 44px;
    margin: 0 6px;
  }
  .fp-time {
    min-width: 2.8em;
    font-size: 0.72rem;
  }
  /* The picture is ~200px tall: the words stay below it (the facade's own,
     still in the layout), so the pause overlay does not repeat them. */
  .fp-overlay {
    display: none;
  }
  .fp-bigplay {
    width: 56px;
    height: 56px;
    margin: -28px 0 0 -28px;
  }
  .fp-end {
    gap: 8px;
  }
  .fp-end-act {
    padding: 8px 12px;
    font-size: 0.82rem;
  }
  .fp-end-act small {
    display: none;
  }

  /* Info expands inline under the reel's words and stays available while the
     film plays. */
  .reel-feature .film-info {
    position: static;
    max-width: none;
    margin-top: 10px;
  }
  .reel-feature .film-info > summary {
    float: none;
  }
  .reel-feature.film-playing > .film-info {
    display: block;
  }
}

/* Portrait-ish screens on /work/: the band's plate already sits below the
   frame (see the portrait block above), so the stage covers only the frame and
   the title stays readable under the picture while the film plays. */
@media (max-aspect-ratio: 4 / 3) {
  .film-band .film-stage {
    inset: 0 0 auto 0;
    height: auto;
    aspect-ratio: 16 / 9;
  }
  .film-band.film-lit .film-facade {
    opacity: 1;
    filter: none;
  }
}
