/* 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 — hero 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: 1400ms;
  --dur-settle: 2400ms;
  --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 — full-bleed cinematic frame, copy anchored bottom-left.
   The shot is a centred, symmetric composition (vanishing point, car and
   reflections all on the central axis), so centred text collides with it both
   compositionally and on contrast: dead-centre samples luma 156 (2.4:1 white).
   The bottom-left plate samples luma 36 (11.6:1) — the only region that is
   dark, empty and not the subject. */
.hero {
  position: relative;
  min-height: 100vh;
  display: flex;
  align-items: flex-end;
  overflow: hidden; /* required: the settle animation scales past the edges */
}

/* Mobile browser chrome: prefer the small viewport unit where supported. */
@supports (min-height: 100svh) {
  .hero {
    min-height: 100svh;
  }
}

.hero-media {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
}

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

.hero-media img {
  width: 100%;
  height: 100%;
  display: block;
  object-fit: cover;
  object-position: center center;
}

/* Structural, never animated. Three targeted layers rather than one full-frame
   veil — a flat scrim buys legibility by killing the warm sky, which is the
   reason the frame is here at all. Each layer darkens only where it must:
     1. nav strip — the top 70px samples luma 164 (white nav = 2.2:1, fails)
     2. left-weighted veil — the copy sits on bright mist (luma 146 = 2.7:1
        unscrimmed). A radial can't solve this: the copy ends at x=37% and the
        car starts at 39%, leaving no room to fall off, so an ellipse strong
        enough for the text also swallowed the headlights. A horizontal ramp
        buys the left third and eases to a light 0.3 veil over the car, which
        keeps it the brightest object in the frame.
     3. bottom dissolve — the road's dark and --bg-color are the same value, so
        the seam into the page vanishes rather than banding */
.hero-media::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background:
    linear-gradient(to bottom, rgba(8, 8, 8, 0.8) 0%, rgba(8, 8, 8, 0.24) 11%, transparent 19%),
    linear-gradient(
      to right,
      rgba(8, 8, 8, 0.74) 0%,
      rgba(8, 8, 8, 0.52) 34%,
      rgba(8, 8, 8, 0.3) 58%,
      rgba(8, 8, 8, 0.34) 100%
    ),
    linear-gradient(to top, #080808 0%, rgba(8, 8, 8, 0.82) 9%, transparent 38%);
}

.hero .container {
  position: relative;
  z-index: 1;
}

/* Narrow on purpose. The car sits at x 631-875 at 16:9 and its headlights are
   the brightest pixels in the frame — no scrim darkens them without killing the
   shot. So the copy simply never reaches them: capping the column wraps the
   name to ROY / CHUCRI and keeps every line left of the subject. The text owns
   the left third, the car owns the right. */
.hero-content {
  max-width: clamp(17rem, 30vw, 29rem);
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  text-align: left;
  padding-bottom: clamp(3rem, 7vh, 4.25rem);
}

.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;
}

/* Portrait-ish viewports: keyed on ASPECT, not width — the problem is aspect.
   A 16:9 frame filling a 100vh portrait viewport shows ~26% of its width at
   3.5x upscale and the shot is destroyed. Instead the image becomes a band at
   the top, dissolving into the page; the copy sits below it on solid black. */
@media (max-aspect-ratio: 4 / 3) {
  .hero {
    justify-content: center;
    padding-top: 56svh;
  }

  .hero-media {
    bottom: auto;
    height: 62svh;
  }

  .hero-media img {
    object-position: center 42%;
  }

  /* No horizontal scrim: the copy is no longer over the image. Just the nav
     scrim at the top and a long dissolve into the page at the bottom. */
  .hero-media::after {
    background: linear-gradient(
      to bottom,
      rgba(8, 8, 8, 0.72) 0%,
      transparent 26%,
      transparent 46%,
      #080808 96%
    );
  }

  .hero-content {
    max-width: 100%;
    padding-bottom: clamp(3.5rem, 9vh, 5rem);
  }
}

.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%;
  z-index: 1000;
  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;
}

.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: 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. Scale goes 1.03 -> 1: settling TO rest is the only fill-safe
     direction. linear on a dissolve because a projector dissolve is linear. */
  html.js .hero-media img {
    animation: hero-dissolve var(--dur-dissolve) linear backwards,
      hero-settle var(--dur-settle) var(--ease-cine) backwards;
  }
  @keyframes hero-dissolve {
    from {
      opacity: 0;
    }
  }
  @keyframes hero-settle {
    from {
      transform: scale(1.03);
    }
  }

  /* 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: 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;
}
/* 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; }
/* --- 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, replacing the deleted
   `.reel-feature iframe`. Same box and the same committed 8px radius, which
   stays exactly one thing on this site. */
.reel-feature .film-stage {
  width: 100%;
  aspect-ratio: 16 / 9;
  border: 1px solid var(--border-light);
  border-radius: 8px;
  overflow: hidden;
  background: #000;
}

/* ---- 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;
  }
}
