/* =========================================================
   JUAN CESTAC — PORTFOLIO
   Palette (light, soft neutrals):
     --white  #FFFFFF  base background
     --blush  #ECD9D1  warm tint / glows / gradients
     --greige #C5BFB3  card surfaces / gradients
     --grey   #D6D6D6  hairlines, dividers
     --taupe  #B9A89C  accent fills, borders, icons, decoration
     --clay   #85695A  accent used AS TEXT (deepened taupe — the raw
              swatch reads ~2.3:1 on white, below the 3:1 floor for
              large text, so anything readable uses this instead)
     --ink    #2A211C  primary text (not one of the 5 supplied swatches —
              added because the palette has no color dark enough to read
              as body copy on white; everything else stays true to it)
   ========================================================= */

@font-face {
  font-family: "Switzer";
  src: url("../assets/fonts/switzer/Switzer-Regular.woff2") format("woff2"),
       url("../assets/fonts/switzer/Switzer-Regular.woff") format("woff");
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Switzer";
  src: url("../assets/fonts/switzer/Switzer-Medium.woff2") format("woff2"),
       url("../assets/fonts/switzer/Switzer-Medium.woff") format("woff");
  font-weight: 500;
  font-style: normal;
  font-display: swap;
}
@font-face {
  font-family: "Switzer";
  src: url("../assets/fonts/switzer/Switzer-Extrabold.woff2") format("woff2"),
       url("../assets/fonts/switzer/Switzer-Extrabold.woff") format("woff");
  font-weight: 800;
  font-style: normal;
  font-display: swap;
}

:root {
  --white: #FFFFFF;
  --blush: #ECD9D1;
  --greige: #C5BFB3;
  --grey: #D6D6D6;
  --taupe: #B9A89C;
  --clay: #85695A;
  --ink: #2A211C;

  --white-rgb: 255, 255, 255;
  --blush-rgb: 236, 217, 209;
  --greige-rgb: 197, 191, 179;
  --taupe-rgb: 185, 168, 156;
  --ink-rgb: 42, 33, 28;

  /* Tints of the same 5 swatches, used as full-section backgrounds so each
     part of the page reads as a different color while scrolling. Mixed
     strong enough to actually read as color, but every one still clears
     WCAG AA (4.5:1) for --text-muted with real margin (checked against the
     darkest, taupe, which is the tightest case at ~4.8:1). */
  --tint-blush: #F5EAE6;   /* work      — ~55% blush  */
  --tint-grey: #EDEDED;    /* about     — ~45% grey   */
  --tint-greige: #E5E2DD;  /* services  — ~45% greige */
  --tint-taupe: #E7E1DC;   /* process   — ~35% taupe (kept lowest %: taupe is the most saturated swatch) */

  /* One per work category, reusing the same 4 section tints above (each
     already checked against --text-muted, ~4.8:1 worst case) plus one new
     blend for the 5th. All verified ≥4.7:1 against --text-muted and
     ≥12:1 against --ink. */
  --tint-personal: #F0EBE7; /* ~20% blush + ~20% greige */

  --bg: var(--white);
  --bg-card: rgba(var(--greige-rgb), 0.35);
  --bg-card-hover: rgba(var(--greige-rgb), 0.55);
  --text: var(--ink);
  --text-muted: #6b5f56;
  --accent: var(--taupe);
  --accent-text: var(--clay);
  --line: rgba(var(--ink-rgb), 0.12);

  --font-display: "Switzer", system-ui, -apple-system, sans-serif;
  --font-body: "Switzer", system-ui, -apple-system, sans-serif;

  --wrap: 1240px;
  --ease: cubic-bezier(0.16, 1, 0.3, 1);
  --header-h: 88px;
}

/* ---------- Reset ---------- */
* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
  background: var(--bg);
  color: var(--text);
  font-family: var(--font-body);
  font-weight: 400;
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}
/* native cursor is hidden only once js/main.js confirms the custom cursor is
   actually running (fine pointer, motion not reduced) — never hide it
   unconditionally, or a reduced-motion/edge-case user loses both */
html.custom-cursor-active body { cursor: none; }
img, svg { display: block; max-width: 100%; }
a { color: inherit; text-decoration: none; }
button { font: inherit; color: inherit; background: none; border: none; cursor: pointer; }
ol, ul { list-style: none; }
::selection { background: var(--accent); color: var(--ink); }

.wrap {
  max-width: var(--wrap);
  margin: 0 auto;
  padding: 0 clamp(20px, 5vw, 48px);
}

@property --angle {
  syntax: "<angle>";
  inherits: false;
  initial-value: 0deg;
}

.eyebrow, .hero__eyebrow {
  display: inline-block;
  font-size: 0.8rem;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  font-weight: 500;
  margin-bottom: 14px;
  background: linear-gradient(90deg, var(--accent-text) 0%, var(--blush) 45%, var(--accent-text) 90%);
  background-size: 220% auto;
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
  animation: shine 5s linear infinite;
}
@keyframes shine {
  to { background-position: -220% center; }
}

/* section anchors should not hide under the fixed header */
section[id] { scroll-margin-top: var(--header-h); }

/* when real photos/videos replace the color placeholders, keep them responsive */
.work-card__media img, .work-card__media video,
.about__frame img, .about__frame video,
.work-item__media img, .work-item__media video {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
}

/* ---------- Reveal-on-scroll ---------- */
.reveal {
  opacity: 0;
  transform: translateY(28px);
  transition: opacity 0.9s var(--ease), transform 0.9s var(--ease);
}
.reveal.in-view { opacity: 1; transform: translateY(0); }
.reveal:nth-child(2) { transition-delay: 0.06s; }
.reveal:nth-child(3) { transition-delay: 0.12s; }
.reveal:nth-child(4) { transition-delay: 0.18s; }
.reveal:nth-child(5) { transition-delay: 0.24s; }
.reveal:nth-child(6) { transition-delay: 0.3s; }

/* variant: slide in from the side instead of rising (services + process) */
.services-grid .reveal { transform: translateX(-30px); }
.services-grid .reveal:nth-child(even) { transform: translateX(30px); }
.services-grid .reveal.in-view { transform: translateX(0); }
.process-item.reveal { transform: translateX(-24px); }
.process-item.reveal.in-view { transform: translateX(0); }

/* section title grows an underline once its section has scrolled into view */
.section-title {
  position: relative;
  display: inline-block;
}
.section-title::after {
  content: "";
  position: absolute;
  left: 0; bottom: -12px;
  height: 3px;
  width: 0;
  background: var(--accent);
  transition: width 1s var(--ease) 0.25s;
}
.section-head.in-view .section-title::after { width: 64px; }

/* BlurText: each word starts blurred/offset/transparent, then settles in,
   staggered per word via an inline transition-delay set in JS */
.blur-text__unit {
  display: inline-block;
  opacity: 0;
  filter: blur(12px);
  transform: translateY(-24px);
  transition: opacity 0.6s var(--ease), filter 0.6s var(--ease), transform 0.6s var(--ease);
}
.blur-text__unit--bottom { transform: translateY(24px); }
.blur-text__unit.in-view {
  opacity: 1;
  filter: blur(0);
  transform: translateY(0);
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .reveal, .cursor, .marquee__track, .work-card, .btn, .service-card,
  .process-item, .process-item::before,
  .glare-hover, .eyebrow, .hero__eyebrow, .btn--ghost::before, .spark,
  .section-title::after, .about__frame,
  .preloader, .preloader__photo, .preloader__eyebrow, .preloader__title,
  .preloader__rule, .preloader__foot, .preloader__bar-fill { transition: none !important; animation: none !important; }
  .reveal { opacity: 1; transform: none; }
  .about__frame { clip-path: none; }
  .hero__particles { display: none; }
  .preloader__photo { transform: none; filter: none; }
  .preloader__eyebrow, .preloader__title { transform: none; }
  .preloader__rule { width: 64px; }
  .preloader__foot { opacity: 1; }
  .preloader__bar-fill { transform: scaleX(1); }
  /* no transition above means these would otherwise still hard-cut on
     hover; pin both states to the sweep's resting position instead */
  .glare-hover, .glare-hover:hover { background-position: -100% -100%; }
}

/* ---------- Noise overlay ---------- */
.noise {
  position: fixed;
  inset: 0;
  z-index: 3;
  pointer-events: none;
  opacity: 0.035;
  /* no mix-blend-mode: a full-viewport blended layer forces the browser to
     re-composite the whole screen against everything under it (video,
     particles, parallax) on every frame. At 3.5% opacity plain alpha
     blending looks the same. */
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='120' height='120'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
}

/* ---------- Preloader / welcome splash ----------
   Cinematic intro: the photo racks focus from a blurred, desaturated
   zoom into a sharp, graded still (a nod to the color-grading service
   below) while "Bienvenidos" slides out of a mask like a title card,
   timed against a counted progress readout at the foot. */
.preloader {
  position: fixed;
  inset: 0;
  z-index: 200;
  background: var(--ink); /* shows briefly if the photo is still loading */
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  transition: transform 1.1s var(--ease), opacity 0.9s var(--ease);
}
.preloader.is-done {
  transform: translateY(-100%) scale(1.04);
  opacity: 0;
  pointer-events: none;
}
.preloader__photo {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* nudged off the raw 50/50 default to center on his face rather than
     the photo's geometric middle (he sits a touch right-of-center, with
     a lot of jacket filling the bottom half) */
  object-position: 54% 46%;
  transform: scale(1.16);
  filter: grayscale(1) blur(14px);
  animation: preloader-kenburns 2.6s var(--ease) 0.1s forwards;
}
@keyframes preloader-kenburns {
  to { transform: scale(1); filter: grayscale(0) blur(0); }
}
.preloader__scrim {
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(180deg, rgba(var(--ink-rgb), 0.32), rgba(var(--ink-rgb), 0.7) 80%);
}
.preloader__welcome {
  position: relative;
  z-index: 2;
  text-align: center;
  padding: 0 24px;
}
.preloader__mask { overflow: hidden; }
.preloader__eyebrow {
  font-size: 0.8rem;
  letter-spacing: 0.28em;
  text-transform: uppercase;
  color: var(--blush);
  margin-bottom: 14px;
  transform: translateY(100%);
  animation: preloader-line-in 0.9s var(--ease) 0.25s forwards;
}
.preloader__title {
  font-family: var(--font-display);
  font-weight: 800;
  text-transform: uppercase;
  font-size: clamp(2.6rem, 9vw, 6rem);
  color: var(--white);
  letter-spacing: 0.01em;
  transform: translateY(115%);
  animation: preloader-line-in 1s var(--ease) 0.45s forwards;
}
@keyframes preloader-line-in {
  to { transform: translateY(0); }
}
.preloader__rule {
  width: 0;
  height: 1px;
  margin: 22px auto 0;
  background: rgba(var(--white-rgb), 0.5);
  animation: preloader-rule-in 0.8s var(--ease) 1.3s forwards;
}
@keyframes preloader-rule-in {
  to { width: 64px; }
}
.preloader__foot {
  position: absolute;
  left: 0;
  right: 0;
  bottom: clamp(24px, 5vh, 48px);
  z-index: 2;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 16px;
  opacity: 0;
  animation: preloader-fade-in 0.6s var(--ease) 0.5s forwards;
}
@keyframes preloader-fade-in {
  to { opacity: 1; }
}
.preloader__count {
  font-family: var(--font-display);
  font-variant-numeric: tabular-nums;
  font-size: 0.8rem;
  letter-spacing: 0.1em;
  color: var(--blush);
}
.preloader__bar {
  width: clamp(120px, 20vw, 220px);
  height: 2px;
  background: rgba(var(--white-rgb), 0.2);
  overflow: hidden;
}
.preloader__bar-fill {
  width: 100%;
  height: 100%;
  background: var(--white);
  transform: scaleX(0);
  transform-origin: left;
}

/* ---------- Custom cursor ---------- */
.cursor {
  position: fixed;
  top: 0; left: 0;
  width: 0; height: 0;
  z-index: 150;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.3s ease;
  /* moved by JS via transform (compositor only) — never left/top, which
     would re-run layout + repaint the drop-shadow on every mouse move */
  will-change: transform;
}
.cursor.is-active { opacity: 1; }
.cursor__dot { display: none; } /* unused now — the arrow below is the whole cursor */
.cursor__ring {
  position: fixed;
  top: 0; left: 0;
  width: 22px; height: 22px;
  pointer-events: none;
  background-color: var(--accent-text);
  /* Classic arrow-pointer silhouette (tip + notch + tail), drawn once as an
     SVG path and used as a mask so it can be filled with any palette color —
     reads as "a normal cursor" instead of an abstract shape. */
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M2 2 L2 16.5 L6 13 L8.5 18.5 L11 17.3 L8.5 12 L15 12 Z'/%3E%3C/svg%3E");
  mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M2 2 L2 16.5 L6 13 L8.5 18.5 L11 17.3 L8.5 12 L15 12 Z'/%3E%3C/svg%3E");
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-size: contain;
  mask-size: contain;
  /* hotspot at the tip (near the mask's top-left), like a real pointer,
     instead of centering the box on the mouse coordinate */
  transform: translate(-2px, -2px);
  transform-origin: 2px 2px;
  filter: drop-shadow(0 1px 1px rgba(var(--ink-rgb), 0.3));
  transition: transform 0.15s var(--ease), background-color 0.25s var(--ease);
}
.cursor.is-hovering .cursor__ring,
.cursor.is-viewfinder .cursor__ring {
  transform: translate(-2px, -2px) scale(1.35);
}
@media (hover: none), (pointer: coarse) {
  .cursor { display: none; }
}

/* ---------- Click spark ---------- */
.spark {
  position: fixed;
  top: 0; left: 0;
  width: 4px; height: 4px;
  border-radius: 50%;
  background: var(--accent);
  transform: translate(-50%, -50%);
  animation: spark-burst 0.6s ease-out forwards;
  z-index: 160;
  pointer-events: none;
}
@keyframes spark-burst {
  to {
    transform: translate(calc(-50% + var(--tx)), calc(-50% + var(--ty))) scale(0);
    opacity: 0;
  }
}

/* ---------- Scroll progress ---------- */
.progress-bar {
  position: fixed;
  top: 0; left: 0;
  height: 3px;
  width: 100%;
  background: linear-gradient(90deg, var(--greige), var(--taupe), var(--clay));
  z-index: 190;
  /* scaled, not resized: animating width re-runs layout on every scroll frame */
  transform: scaleX(0);
  transform-origin: left;
  will-change: transform;
}

/* ---------- Header ---------- */
.site-header {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 100;
  height: var(--header-h);
  display: flex;
  align-items: center;
  background: linear-gradient(to bottom, rgba(var(--white-rgb), 0.85), transparent);
  transition: background 0.4s ease, box-shadow 0.4s ease, height 0.4s ease;
}
.site-header.is-scrolled {
  background: rgba(var(--white-rgb), 0.85);
  backdrop-filter: blur(10px);
  box-shadow: 0 1px 0 var(--line);
  height: 74px;
}
.header__inner {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.brand { display: flex; flex-direction: column; gap: 2px; }
.brand__name {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 1.15rem;
  letter-spacing: 0.02em;
}
.brand__tag {
  font-size: 0.68rem;
  color: var(--text-muted);
  letter-spacing: 0.06em;
  text-transform: uppercase;
}
.nav { display: flex; align-items: center; gap: clamp(20px, 3vw, 40px); }
.nav__link {
  position: relative;
  font-size: 0.92rem;
  color: var(--text);
  padding: 4px 0;
}
.nav__link::after {
  content: "";
  position: absolute;
  left: 0; bottom: -2px;
  width: 100%; height: 1px;
  background: var(--accent-text);
  transform: scaleX(0);
  transform-origin: right;
  transition: transform 0.35s var(--ease);
}
.nav__link:hover::after { transform: scaleX(1); transform-origin: left; }
.nav__link--cta {
  border: 1px solid var(--taupe);
  border-radius: 999px;
  padding: 9px 20px;
  transition: background 0.3s var(--ease), border-color 0.3s var(--ease), color 0.3s var(--ease);
}
.nav__link--cta::after { display: none; }
.nav__link--cta:hover { background: var(--accent); border-color: var(--accent); color: var(--ink); }

.nav-toggle {
  display: none;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 5px;
  width: 44px;
  height: 44px;
  z-index: 101;
}
.nav-toggle span {
  width: 28px;
  height: 1.5px;
  background: var(--ink);
  transition: transform 0.3s var(--ease), opacity 0.3s var(--ease);
}
.nav-toggle[aria-expanded="true"] span:nth-child(1) { transform: translateY(6.5px) rotate(45deg); }
.nav-toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] span:nth-child(3) { transform: translateY(-6.5px) rotate(-45deg); }

/* ---------- Hero ---------- */
.hero {
  position: relative;
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  padding-top: var(--header-h);
  overflow: hidden;
  background: var(--white);
}
.hero__bg {
  position: absolute;
  inset: -10% 0 0 0;
  z-index: 0;
  will-change: transform; /* parallax target — keep it on its own layer */
}
.hero__particles {
  position: absolute;
  inset: 0;
  z-index: 1;
  width: 100%;
  height: 100%;
}
.hero__grainient {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  /* WebGL canvas draws its own colors; this is only a fallback in case it
     never initializes (unsupported browser, context creation failure).
     Currently never even started — see the comment by initGrainient()'s
     call site in main.js — so in practice this plain fill is all that
     ever shows here, permanently hidden under the video below anyway. */
  background: var(--tint-blush);
}
.hero__video {
  position: absolute;
  inset: 0;
  z-index: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  filter: saturate(0.9);
}
.hero__video-scrim {
  position: absolute;
  inset: 0;
  z-index: 0;
  /* washes the footage into the site's own pastel palette so it reads as
     part of the design rather than a jarring literal video pop-in. Lighter
     up top (eyebrow + title, which also get their own legibility halo
     below) and heavier toward the foot, where the smaller --text-muted
     description needs more help than a halo alone can give it. */
  background: linear-gradient(
    180deg,
    rgba(var(--white-rgb), 0.22) 0%,
    rgba(var(--white-rgb), 0.42) 45%,
    rgba(var(--white-rgb), 0.62) 100%
  );
}
.hero__inner { position: relative; z-index: 2; padding-top: 40px; padding-bottom: 60px; will-change: transform, opacity; }
/* the shared .eyebrow rule's shine gradient reads fine on the light
   section tints it's normally used on; only the hero copy sits over
   video and needs the extra halo, so it's scoped here rather than
   added to .eyebrow itself */
.hero__eyebrow {
  filter: drop-shadow(0 2px 14px rgba(var(--white-rgb), 0.75));
  /* over the video the shared shine's near-white blush stop vanished —
     shimmer through ink instead so the line stays readable */
  background-image: linear-gradient(90deg, var(--accent-text) 0%, var(--ink) 45%, var(--accent-text) 90%);
}
.hero__title {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(3rem, 10vw, 8rem);
  line-height: 0.98;
  letter-spacing: -0.01em;
  text-transform: uppercase;
  margin: 10px 0 40px;
  /* soft light halo so the dark type stays legible over the hero video
     regardless of what's happening in the footage right behind it —
     applied here (not on .line, which clips via overflow:hidden for the
     reveal animation) so the blur isn't cut off at each line's edge */
  filter: drop-shadow(0 4px 26px rgba(var(--white-rgb), 0.55));
}
.hero__title .line {
  display: block;
  overflow: hidden;
  color: var(--ink);
}
.hero__title .line > span {
  display: inline-block;
  transform: translateY(105%);
  animation: line-up 1s var(--ease) forwards;
  animation-delay: var(--d, 0s);
}
.hero__title em {
  font-style: normal;
  color: var(--accent-text);
}
@keyframes line-up {
  to { transform: translateY(0); }
}
.hero__foot {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 40px;
  flex-wrap: wrap;
  max-width: 900px;
}
.hero__desc {
  max-width: 420px;
  color: var(--text-muted);
  font-size: 1.02rem;
  /* --text-muted is a lighter brown — needs more halo than the ink title
     to hold its own contrast over the video */
  filter: drop-shadow(0 2px 14px rgba(var(--white-rgb), 0.75));
}

.btn {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 15px 26px;
  border-radius: 999px;
  font-size: 0.95rem;
  font-weight: 500;
  white-space: nowrap;
}
.btn--ghost {
  position: relative;
  border: 1px solid var(--taupe);
  color: var(--ink);
  transition: background 0.35s var(--ease), border-color 0.35s var(--ease), color 0.35s var(--ease), gap 0.35s var(--ease);
}
.btn--ghost:hover { background: var(--accent); border-color: var(--accent); color: var(--ink); gap: 16px; }
.btn--ghost::before {
  content: "";
  position: absolute;
  inset: -8px;
  z-index: -1;
  border-radius: inherit;
  background: conic-gradient(from var(--angle, 0deg), transparent, var(--clay), transparent 30%);
  filter: blur(12px);
  opacity: 0;
  transition: opacity 0.4s var(--ease);
}
.btn--ghost:hover::before { opacity: 0.5; animation: rotate-border 2.4s linear infinite; }
@keyframes rotate-border { to { --angle: 360deg; } }

/* ---------- Marquee ---------- */
.marquee {
  border-top: 1px solid var(--line);
  border-bottom: 1px solid var(--line);
  overflow: hidden;
  padding: 20px 0;
  background: rgba(var(--greige-rgb), 0.2);
}
.marquee__track {
  display: flex;
  align-items: center;
  gap: 20px;
  width: max-content;
  white-space: nowrap;
  animation: marquee 26s linear infinite;
  font-family: var(--font-display);
  font-weight: 800;
  text-transform: uppercase;
  font-size: clamp(1.2rem, 3vw, 1.8rem);
  color: var(--text-muted);
}
.marquee__track span:not(.dot) { padding: 0 4px; }
.marquee .dot { color: var(--accent-text); font-size: 0.9rem; }
@keyframes marquee { from { transform: translateX(0); } to { transform: translateX(-50%); } }
.marquee:hover .marquee__track { animation-play-state: paused; }

/* ---------- Section shared ---------- */
section { padding: clamp(80px, 12vw, 160px) 0; position: relative; }
.section-head { max-width: 640px; margin-bottom: clamp(40px, 6vw, 72px); }
.section-title {
  font-family: var(--font-display);
  font-weight: 800;
  text-transform: uppercase;
  font-size: clamp(2rem, 5vw, 3.4rem);
  line-height: 1.05;
  color: var(--ink);
}
.section-title span { color: var(--accent-text); }
.section-sub { color: var(--text-muted); margin-top: 16px; font-size: 1rem; max-width: 46ch; }
.section-sub code {
  font-family: monospace;
  background: rgba(var(--ink-rgb), 0.08);
  padding: 1px 6px;
  border-radius: 4px;
  font-size: 0.85em;
}

/* ---------- Work category <-> detail swap ---------- */
.view-pane { transition: opacity 0.35s var(--ease); }
.view-pane.is-hidden { opacity: 0; pointer-events: none; }

.work-back {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin-bottom: clamp(28px, 4vw, 40px);
  padding: 0;
  font-family: var(--font-display);
  font-weight: 700;
  text-transform: uppercase;
  font-size: 0.78rem;
  letter-spacing: 0.07em;
  color: var(--text-muted);
  background: none;
  border: none;
  cursor: pointer;
  transition: color 0.3s var(--ease);
}
.work-back svg { transition: transform 0.3s var(--ease); }
.work-back:hover { color: var(--ink); }
.work-back:hover svg { transform: translateX(-4px); }

.work-detail__title {
  font-family: var(--font-display);
  font-weight: 800;
  text-transform: uppercase;
  font-size: clamp(1.8rem, 4.5vw, 3rem);
  line-height: 1.05;
  color: var(--ink);
  text-align: center;
  margin-bottom: clamp(32px, 5vw, 56px);
}

/* .work normally gets its vertical padding from the generic `section` rule
   below, but #workCategoriesView carries its own instead (so it can sit
   flush against .work-detail, which needs a different padding scheme —
   see below). */
.work { padding: 0; }
#workCategoriesView { padding: clamp(80px, 12vw, 160px) 0; }

/* A category is its own full-screen "sector": fixed over the whole
   viewport so scrolling never reaches the rest of the page while it's
   open (main.js also sets body { overflow: hidden } for the same
   reason) — the only way back to the rest of the site is #workBack.
   Its own background color (set per-category in JS) covers the full
   viewport this way too, with no seam from .work's gradient. */
.work-detail {
  position: fixed;
  inset: 0;
  z-index: 50; /* above normal content, below .site-header (100) */
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
  padding: calc(var(--header-h) + clamp(24px, 4vw, 48px)) 0 clamp(48px, 8vw, 96px);
}

/* each section eases from the previous section's color into its own over
   the first 220px, so the shift reads as a gradient while you scroll
   rather than a hard cut at the boundary line */
.work { background: linear-gradient(to bottom, var(--white), var(--tint-blush) 220px, var(--tint-blush) 100%); }
.about { background: linear-gradient(to bottom, var(--tint-blush), var(--tint-grey) 220px, var(--tint-grey) 100%); }
.services { background: linear-gradient(to bottom, var(--tint-grey), var(--tint-greige) 220px, var(--tint-greige) 100%); }
.process { background: linear-gradient(to bottom, var(--tint-greige), var(--tint-taupe) 220px, var(--tint-taupe) 100%); }
.contact {
  /* tighter than the shared `section` bottom padding — the form+social
     row is the last thing in this section, no divider/next-block below
     it anymore, so the full 160px read as empty space before the footer */
  padding-bottom: clamp(40px, 6vw, 72px);
  /* plain fallback for browsers without color-mix() — still a much longer,
     softer ramp than the rest of the site's section seams */
  background: linear-gradient(to bottom, var(--tint-taupe), var(--ink) 640px, var(--ink) 100%);
  background: linear-gradient(
    to bottom,
    var(--tint-taupe) 0,
    color-mix(in srgb, var(--tint-taupe) 70%, var(--ink)) 160px,
    color-mix(in srgb, var(--tint-taupe) 35%, var(--ink)) 340px,
    color-mix(in srgb, var(--tint-taupe) 12%, var(--ink)) 520px,
    var(--ink) 640px
  );
}

/* ---------- Work grid ---------- */
.work-grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 24px;
}
/* the category covers: one row of four on desktop (2x2 / 1 column below,
   via the shared .work-grid breakpoints) instead of 3 + a lone orphan */
@media (min-width: 961px) {
  #workGrid { grid-template-columns: repeat(4, 1fr); }
}

.work-card[hidden] { display: none; }

/* visible keyboard focus everywhere (mouse clicks don't trigger it) */
:focus-visible {
  outline: 2px solid var(--clay);
  outline-offset: 3px;
  border-radius: 6px;
}
.contact :focus-visible { outline-color: var(--blush); }
.work-card {
  --rx: 0deg;
  --ry: 0deg;
  --mx: 50%;
  --my: 50%;
  display: block;
  width: 100%;
  text-align: left;
  border-radius: 18px;
  overflow: hidden;
  background: var(--bg-card);
  border: 1px solid var(--line);
  position: relative;
  transform: perspective(900px) rotateX(0deg) rotateY(0deg) translateY(0);
  transition: transform 0.5s var(--ease), background 0.5s var(--ease), border-color 0.5s var(--ease), opacity 0.35s var(--ease);
}
.work-card:hover { background: var(--bg-card-hover); border-color: var(--taupe); }
/* two classes to out-specificity .reveal.in-view's opacity:1 once a
   card has already scrolled into view */
.work-card.work-card--hide { opacity: 0; pointer-events: none; }
.work-card.is-tilting {
  transition: transform 0.15s ease-out, background 0.5s var(--ease), border-color 0.5s var(--ease);
  transform: perspective(900px) rotateX(var(--ry)) rotateY(var(--rx)) translateY(-6px);
}
.work-card::after {
  content: "";
  position: absolute; inset: 0;
  z-index: 2;
  border-radius: inherit;
  background: radial-gradient(circle at var(--mx) var(--my), rgba(var(--white-rgb), 0.5), transparent 55%);
  opacity: 0;
  transition: opacity 0.4s var(--ease);
  pointer-events: none;
}
.work-card.is-tilting::after { opacity: 1; }
.work-card__media {
  aspect-ratio: 4 / 5;
  display: flex;
  align-items: center;
  justify-content: center;
  color: var(--ink);
  position: relative;
  overflow: hidden;
}
.work-card__media::after {
  content: "";
  position: absolute; inset: 0;
  background: linear-gradient(160deg, rgba(var(--ink-rgb), 0) 40%, rgba(var(--ink-rgb), 0.16) 100%);
}
.work-card__icon {
  position: relative;
  z-index: 1;
  opacity: 0.8;
  transition: transform 0.5s var(--ease);
}
.work-card:hover .work-card__icon { transform: scale(1.15); }
.work-card__media--a { background: linear-gradient(135deg, var(--blush), var(--taupe)); }
.work-card__media--b { background: linear-gradient(135deg, var(--white), var(--greige)); }
.work-card__media--c { background: linear-gradient(135deg, var(--greige), var(--taupe)); }
.work-card__media--d { background: linear-gradient(135deg, var(--blush), var(--white)); }
.work-card__media--e { background: linear-gradient(135deg, var(--taupe), var(--grey)); }
.work-card__media--f { background: linear-gradient(135deg, var(--greige), var(--blush)); }
/* opt-in modifier (set from JS via createWorkCard's `orientation` option)
   for placeholders that should read as a landscape shot instead of the
   site's default portrait 4:5 — e.g. Mola's 2 horizontal pieces */
.work-card__media--landscape { aspect-ratio: 3 / 2; }
/* real photo/video in a card: fills the frame behind the icon and the
   shading ::after, instead of joining the flex row next to the icon */
.work-card__media > img,
.work-card__media > video { position: absolute; inset: 0; }

html.force-reduced-motion .hero__particles,
html.force-reduced-motion .hero__grainient,
html.force-reduced-motion .cursor { display: none !important; }
/* Hover motion on work thumbnails (wired up in main.js, "Work
   thumbnails: motion on hover"): an ultra slow-mo clip that fades in
   over the still, and for photo-only cards a slow cinematic push-in. */
.work-card__media > .work-card__slowmo {
  opacity: 0;
  transition: opacity 0.6s var(--ease);
}
.work-card__media.is-moving > .work-card__slowmo { opacity: 1; }
.work-card--photo-motion .work-card__media > img {
  transform: scale(1.001);
  transition: transform 1.2s var(--ease), filter 1.2s var(--ease);
}
.work-card--photo-motion:hover .work-card__media > img {
  transform: scale(1.09) translate(-1.2%, -1.6%);
  filter: brightness(1.05) saturate(1.06);
  transition: transform 7s cubic-bezier(0.25, 0.1, 0.25, 1), filter 1.2s var(--ease);
}
@media (prefers-reduced-motion: reduce) {
  .work-card--photo-motion:hover .work-card__media > img { transform: none; filter: none; }
}

.work-card__meta { padding: 20px 22px 24px; }
.work-card__tag {
  font-size: 0.72rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent-text);
}
.work-card__title {
  font-family: var(--font-display);
  font-weight: 800;
  text-transform: uppercase;
  font-size: 1.15rem;
  margin-top: 8px;
  letter-spacing: 0.01em;
}

/* ---------- About ---------- */
.about__inner {
  display: grid;
  grid-template-columns: 0.85fr 1.15fr;
  gap: clamp(30px, 6vw, 80px);
  align-items: center;
}
.about__frame {
  aspect-ratio: 3 / 4;
  border-radius: 20px;
  background: linear-gradient(160deg, var(--greige), var(--taupe) 70%);
  border: 1px solid var(--line);
  display: flex;
  align-items: flex-end;
  padding: 20px;
  position: relative;
  overflow: hidden;
  /* wipes open once .about__media (its observed ancestor) scrolls into view.
     The clip lives here, one level below the observed .reveal element,
     because an element that starts fully clip-path-hidden never registers
     as "intersecting" for IntersectionObserver — it'd never trigger itself. */
  clip-path: inset(0 0 100% 0);
  transition: clip-path 1.1s var(--ease) 0.15s;
}
.about__media.in-view .about__frame {
  clip-path: inset(0 0 0% 0);
}
.about__frame::before {
  content: "";
  position: absolute; inset: 0;
  background: radial-gradient(circle at 30% 20%, rgba(var(--white-rgb), 0.4), transparent 55%);
}
.about__frame-label {
  font-family: var(--font-display);
  font-weight: 800;
  letter-spacing: 0.06em;
  color: var(--ink);
  position: relative;
}
/* Real photo inside the About frame: fills the whole frame (ignoring its
   padding), framed a bit high so the face stays in view; the label sits
   on top in white with a soft shadow so it reads over the dark photo. */
.about__frame--photo > img {
  position: absolute;
  inset: 0;
  object-position: 50% 25%;
}
.about__frame--photo::before {
  z-index: 1;
  background: linear-gradient(to top, rgba(0, 0, 0, 0.45), transparent 40%);
}
.about__frame--photo .about__frame-label {
  z-index: 2;
  color: #fff;
  text-shadow: 0 1px 8px rgba(0, 0, 0, 0.4);
}
.about__title {
  font-family: var(--font-display);
  font-weight: 800;
  text-transform: uppercase;
  font-size: clamp(1.6rem, 3.4vw, 2.4rem);
  line-height: 1.15;
  margin-bottom: 22px;
}
.about__text { color: var(--text-muted); max-width: 54ch; margin-bottom: 44px; }
.stats { display: flex; gap: clamp(28px, 5vw, 56px); flex-wrap: wrap; }
.stat { display: flex; flex-direction: column; }
.stat__num, .stat__plus {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(2.2rem, 4vw, 3rem);
  color: var(--accent-text);
}
.stat__plus { display: inline; }
.stat__label { font-size: 0.82rem; color: var(--text-muted); margin-top: 6px; }

/* ---------- Services ---------- */
.services-grid {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: 18px;
  overflow: hidden;
}
.service-card {
  background-color: var(--white);
  padding: clamp(28px, 3vw, 40px) clamp(20px, 2.4vw, 30px);
}
.service-card:hover { background-color: var(--bg-card); }
.service-card__num { font-size: 0.8rem; color: var(--text-muted); letter-spacing: 0.08em; }
.service-card__title {
  font-family: var(--font-display);
  font-weight: 800;
  text-transform: uppercase;
  font-size: 1.15rem;
  margin: 18px 0 12px;
}
.service-card__text { color: var(--text-muted); font-size: 0.92rem; }

/* ---------- Glare Hover ----------
   Vanilla-CSS port of reactbits.dev's GlareHover (originally a React
   component) onto the services cards above — a diagonal light sweep
   that slides across the card on hover. Pure CSS: a two-stop diagonal
   gradient sized larger than the card and slid across it via a
   background-position transition. No JS needed — the original's only
   other prop is `playOnce` (default false), and "sweep in on hover,
   sweep back out on mouse-leave" is exactly what a plain :hover
   transition already gives for free.
   Color is a warm taupe tint rather than the original demo's white —
   this card's own background is already white, so a white sweep would
   barely register; taupe reads as a visible warm highlight instead. */
.glare-hover {
  background-image: linear-gradient(
    -30deg,
    transparent 60%,
    rgba(var(--white-rgb), 0.7) 70%,
    transparent 100%
  );
  background-size: 300% 300%;
  background-repeat: no-repeat;
  background-position: -100% -100%;
}
.service-card.glare-hover {
  transition: background-color 0.4s var(--ease), background-position 800ms ease;
}
.process-item.glare-hover {
  transition: background-color 0.4s var(--ease), padding-left 0.4s var(--ease), background-position 800ms ease;
}
.glare-hover:hover { background-position: 100% 100%; }

/* ---------- Process ---------- */
.process-list { display: flex; flex-direction: column; }
.process-item {
  display: grid;
  grid-template-columns: 90px 1fr;
  gap: 24px;
  padding: 30px 0;
  border-top: 1px solid var(--line);
  align-items: baseline;
  position: relative;
  overflow: hidden;
  /* the row itself nudges its content right and picks up a faint card
     tint on hover — .glare-hover (below) layers the diagonal sweep and
     its own background-position transition on top of this */
  transition: background-color 0.4s var(--ease), padding-left 0.4s var(--ease);
}
.process-item:hover { background-color: var(--bg-card); padding-left: 14px; }
/* a second, accent-colored rule that draws in left-to-right over the
   row's top hairline on hover — same "underline reveal" idea as
   .section-title::after, reused here for a consistent, restrained
   signature instead of a flashier one-off effect */
.process-item::before {
  content: "";
  position: absolute;
  top: 0; left: 0;
  width: 0; height: 2px;
  background: var(--accent-text);
  transition: width 0.5s var(--ease);
}
.process-item:hover::before { width: 100%; }
.process-list li:last-child { border-bottom: 1px solid var(--line); }
.process-item__num {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: 1.4rem;
  color: var(--accent-text);
  transition: color 0.4s var(--ease);
}
.process-item:hover .process-item__num { color: var(--ink); }
.process-item h3 {
  font-family: var(--font-display);
  font-weight: 800;
  text-transform: uppercase;
  font-size: 1.2rem;
  margin-bottom: 8px;
  transition: color 0.4s var(--ease);
}
.process-item:hover h3 { color: var(--accent-text); }
.process-item p { color: var(--text-muted); max-width: 56ch; }

/* ---------- Contact (dark closing section) ----------
   The only place on the site that inverts to the dark end of the palette
   (--ink) instead of a light tint — a deliberate bold close, echoing the
   welcome splash's dark scrim so the site opens and closes on the same note. */
.contact__heading {
  font-family: var(--font-display);
  font-weight: 800;
  text-transform: uppercase;
  font-size: clamp(2.2rem, 6.4vw, 4.6rem);
  line-height: 1.08;
  margin-bottom: clamp(48px, 8vw, 88px);
}
.contact__line {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  row-gap: 0.15em;
  color: var(--white);
}
.contact__line--muted { color: var(--grey); }

.rotating-pill {
  /* just a clipping window for the slide animation now — no pill
     background/shape, the rotating word carries the color itself */
  display: inline-flex;
  align-items: center;
  overflow: hidden;
  height: 1.2em;
  margin-left: 0.22em;
}
.rotating-pill__word {
  display: inline-block;
  line-height: 1;
  /* same tone as the "trabajo" accent in "Cómo trabajo" (.section-title
     span) — greige was too close in luminance to this section's
     background and disappeared */
  color: var(--accent-text);
}

/* ---------- Contact form ----------
   No backend on this static site — see the comment in main.js — so this
   builds a mailto: link instead of actually transmitting anything. It
   still saves the visitor from typing the address and subject by hand. */
.contact__body {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: clamp(32px, 6vw, 64px);
  flex-wrap: wrap;
}
.contact__form { max-width: 640px; flex: 1 1 480px; }
.contact__form-row {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 24px;
}
.contact__field {
  display: flex;
  flex-direction: column;
  margin-bottom: 22px;
}
.contact__field label {
  font-size: 0.78rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--taupe);
  margin-bottom: 10px;
}
.contact__field input,
.contact__field textarea {
  font-family: var(--font-body);
  font-size: 1rem;
  color: var(--white);
  background: transparent;
  border: none;
  border-bottom: 1px solid rgba(var(--white-rgb), 0.3);
  padding: 8px 0;
  resize: vertical;
  transition: border-color 0.3s var(--ease);
}
.contact__field input::placeholder,
.contact__field textarea::placeholder { color: rgba(var(--white-rgb), 0.4); }
.contact__field input:focus,
.contact__field textarea:focus {
  outline: none;
  border-color: var(--blush);
}
.contact__form-foot {
  display: flex;
  align-items: center;
  gap: 24px;
  flex-wrap: wrap;
  margin-top: 12px;
}
.contact__form-submit {
  background: var(--white);
  color: var(--ink);
  transition: background 0.3s var(--ease), gap 0.3s var(--ease);
}
.contact__form-submit:hover { background: var(--blush); gap: 16px; }
.contact__form-note {
  font-size: 0.82rem;
  color: rgba(var(--white-rgb), 0.5);
  transition: color 0.3s var(--ease);
}
.contact__form-note.is-success { color: rgba(var(--white-rgb), 0.95); }
.contact__form-note.is-error { color: var(--blush); }
.contact__form-submit[disabled] { opacity: 0.6; cursor: progress; }
/* spam trap — kept out of sight and out of the tab order */
.contact__hp { position: absolute; left: -9999px; width: 1px; height: 1px; opacity: 0; }

.contact__social { display: flex; gap: 12px; }
.contact__social--side { flex-direction: column; }
.social-btn {
  width: 44px; height: 44px;
  border-radius: 50%;
  background: var(--white);
  color: var(--ink);
  display: flex;
  align-items: center;
  justify-content: center;
  transition: background 0.3s var(--ease), transform 0.3s var(--ease);
}
.social-btn:hover { background: var(--blush); transform: translateY(-3px); }

/* ---------- Footer ---------- */
.site-footer {
  background: var(--ink);
  padding-top: 36px;
  overflow: hidden;
}
.footer__inner {
  /* 3 column tracks even though there are only 2 items: the empty 3rd
     (1fr, same width as the 1st) balances the 1st so the copyright in
     the middle column sits truly centered, not just centered in the
     leftover space after the link. */
  display: grid;
  grid-template-columns: 1fr auto 1fr;
  align-items: center;
  gap: 20px;
  font-size: 0.85rem;
  color: var(--grey);
}
.footer__inner > :first-child { justify-self: start; }
.footer__inner a { color: var(--grey); transition: color 0.3s var(--ease); }
.footer__inner a:hover { color: var(--white); }
.footer__copy { color: var(--taupe); }

/* ---------- Item detail (level 4: full-size view of one image) ----------
   Deepest of four nested .work-detail-style full-screen panes:
     50  #workDetail       level 2 — Toma grid, inside a category
     54  #workImageDetail  level 3 — individual images, inside a Toma
     56  #workItemDetail   level 4 — one image, full size
   Each stacks above its parent (they're reached from inside it) and all
   share the same background tint as #workDetail (set from JS) so the
   whole chain reads as zooming into one category, not leaving it. */
#workImageDetail { z-index: 54; }
#workItemDetail { z-index: 56; }

.work-item__media {
  /* .work-card__media + a --a.."--f" variant class is applied here from
     JS (same gradient the clicked card used) — this just overrides the
     small-card sizing for a large, centered display. */
  max-width: 640px;
  margin: 0 auto clamp(28px, 5vw, 48px);
  border-radius: 24px;
  border: 1px solid var(--line);
}
.work-item__media .work-card__icon { width: 56px; height: 56px; }
/* a real vertical video: its own 9:16 frame, uncropped, sized so the
   whole thing (controls included) fits on screen */
.work-item__media--video {
  aspect-ratio: 9 / 16;
  width: min(100%, 420px, calc(78vh * 9 / 16));
  background: var(--ink);
}
.work-item__media--video::after { display: none; } /* no shading over the controls */
.work-item__media--video.work-item__media--wide {
  aspect-ratio: 16 / 9;
  width: min(100%, 960px, calc(78vh * 16 / 9));
}
.work-item__media--video video { object-fit: contain; }
/* a real photo: the frame takes the image's own proportions (portrait or
   landscape), capped so the whole photo fits on screen */
.work-item__media--photo {
  display: block;
  aspect-ratio: auto;
  width: fit-content;
  max-width: 100%;
}
.work-item__media--photo::after { display: none; }
/* opens the fullscreen viewer — the photo's equivalent of a video's own fullscreen control */
.work-item__fs {
  position: absolute;
  right: 14px;
  bottom: 14px;
  display: grid;
  place-items: center;
  width: 40px;
  height: 40px;
  border-radius: 50%;
  background: rgba(var(--ink-rgb), 0.55);
  color: var(--white);
  transition: background 0.25s var(--ease), transform 0.25s var(--ease);
}
.work-item__fs:hover { background: rgba(var(--ink-rgb), 0.8); transform: scale(1.08); }

/* viewport-based caps, not %: a % max-width here would be relative to a
   fit-content parent that is itself sized by this image — a cycle that
   collapses the image to 0px */
.work-item__media--photo img {
  position: static; /* undo the card rule's absolute fill — here the image sizes the frame */
  width: auto;
  height: auto;
  /* 100vw minus .wrap's side padding on both sides, minus the scrollbar + frame border */
  max-width: min(960px, calc(100vw - 2 * clamp(20px, 5vw, 48px) - 24px));
  max-height: 78vh;
  object-fit: contain;
}

/* ---------- Fullscreen viewer (double-click a card in level 3) ----------
   Black, edge to edge, one file per screen, snapping to each in turn —
   scroll-snap-stop: always means even a hard flick lands on the very next
   file. The custom cursor sits below this layer, so the native one comes
   back while it's open. */
.viewer {
  position: fixed;
  inset: 0;
  z-index: 300; /* above the header and the custom cursor */
  background: #000;
  cursor: auto;
}
.viewer[hidden] { display: none; }
html.viewer-open .cursor { display: none; }
.viewer__track {
  height: 100%;
  overflow-y: auto;
  scroll-snap-type: y mandatory;
  overscroll-behavior: contain;
  scrollbar-width: none;
}
.viewer__track::-webkit-scrollbar { display: none; }
.viewer__slide {
  position: relative;
  height: 100vh;
  height: 100dvh;
  display: flex;
  align-items: center;
  justify-content: center;
  scroll-snap-align: start;
  scroll-snap-stop: always;
}
.viewer__slide video,
.viewer__slide img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain; /* the whole file, uncropped, whatever its orientation */
}
.viewer__slide video { cursor: pointer; touch-action: manipulation; } /* lets a double-tap through instead of zooming */
.viewer__count {
  position: absolute;
  left: 24px;
  bottom: 20px;
  font-size: 0.75rem;
  letter-spacing: 0.12em;
  color: rgba(255, 255, 255, 0.7);
  font-variant-numeric: tabular-nums;
  pointer-events: none;
}
.viewer__btn {
  position: absolute;
  z-index: 1;
  padding: 10px 16px;
  border: 1px solid rgba(255, 255, 255, 0.25);
  border-radius: 999px;
  background: rgba(255, 255, 255, 0.12);
  color: #fff;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 0.75rem;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  cursor: pointer;
}
.viewer__btn:hover { background: rgba(255, 255, 255, 0.22); }
.viewer__btn[hidden] { display: none; }
.viewer__close { top: 20px; right: 20px; padding: 8px 14px; font-size: 1rem; }
.viewer__sound { right: 20px; bottom: 16px; }

.work-item__info { max-width: 60ch; margin: 0 auto; text-align: center; }
.work-item__tag {
  font-size: 0.78rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--accent-text);
}
.work-item__title {
  font-family: var(--font-display);
  font-weight: 800;
  text-transform: uppercase;
  font-size: clamp(1.6rem, 4vw, 2.4rem);
  margin: 12px 0 16px;
  color: var(--ink);
}
.work-item__desc { color: var(--text-muted); }

/* ---------- Responsive ----------
   Tiers: >960 desktop · 601-960 tablet (off-canvas nav from here down,
   to give the horizontal nav room to breathe) · <=600 phone. */
@media (max-width: 960px) {
  .nav {
    position: fixed;
    top: 0; right: 0;
    height: 100vh;
    width: min(78vw, 340px);
    background: var(--white);
    border-left: 1px solid var(--line);
    display: flex;
    flex-direction: column;
    justify-content: center;
    align-items: flex-start;
    gap: 28px;
    padding: 0 clamp(28px, 8vw, 48px);
    transform: translateX(100%);
    transition: transform 0.5s var(--ease);
  }
  .nav.is-open { transform: translateX(0); }
  .nav__link { font-size: 1.2rem; }
  .nav-toggle { display: flex; }

  .work-grid { grid-template-columns: repeat(2, 1fr); }
  .services-grid { grid-template-columns: repeat(2, 1fr); }
  .about__inner { grid-template-columns: 1fr; }
  .about__frame { max-width: 340px; margin: 0 auto; }
}

@media (max-width: 600px) {
  .work-grid { grid-template-columns: 1fr; }
  .services-grid { grid-template-columns: 1fr; }
  .process-item { grid-template-columns: 44px 1fr; gap: 16px; }
  .hero__foot { flex-direction: column; align-items: flex-start; gap: 24px; }
  .hero__title { font-size: clamp(2.5rem, 12vw, 8rem); }
  .contact__form-row { grid-template-columns: 1fr; gap: 0; }
  .contact__social--side { flex-direction: row; justify-content: center; }
  .footer__inner { grid-template-columns: 1fr; justify-items: center; gap: 12px; }
  .footer__inner > :first-child { justify-self: center; }
  .brand__tag { display: none; }
  .btn { padding: 14px 20px; font-size: 0.9rem; }
}

/* short viewports — landscape phones, small browser windows, split-screen —
   where min-height:100vh on the hero would otherwise force the title/CTA
   below the fold before the page even settles */
@media (max-height: 500px) {
  .hero { min-height: auto; padding-top: calc(var(--header-h) + 16px); }
  .hero__inner { padding-top: 8px; padding-bottom: 24px; }
  .hero__title { font-size: clamp(1.8rem, 6vw, 4rem); margin: 6px 0 20px; }
  .hero__foot { gap: 16px; }
  .preloader__title { font-size: clamp(2rem, 7vw, 4rem); }
}
