/* ==========================================================================
   Makarand Lanjewar - Portfolio
   Design language: a video editor's timeline. Color grading (orange/teal),
   timecodes instead of decorative numbering, level-meter skill bars.
   ========================================================================== */

/* Fonts are pulled with <link> tags in each page's <head>, NOT with @import.
   A stylesheet containing @import is not applied until that import resolves,
   so a slow Google Fonts round trip held back every rule on the site - long
   enough to paint the page unstyled first. <link> fetches in parallel. */

:root{
  --bg: #12100e;
  --surface: #1b1815;
  --surface-2: #221e1a;
  --line: #332d27;
  --text: #f4efe6;
  --muted: #9c9187;
  --teal: #2fbfa6;
  --teal-dim: #1d6f61;
  --orange: #ff8a4c;
  --orange-dim: #a85a30;

  --display: 'Bricolage Grotesque', system-ui, sans-serif;
  --body: 'Inter', system-ui, sans-serif;
  --mono: 'Space Mono', monospace;

  --radius: 10px;
  --wrap: 1120px;

  /* Rainbow CTA spectrum (see .ihb-rainbow). Kept as five separate hues so the
     sweep can be regraded to the site's teal/orange pair without touching the
     gradient stops themselves. */
  --color-1: 0 100% 63%;
  --color-2: 270 100% 63%;
  --color-3: 210 100% 63%;
  --color-4: 195 100% 63%;
  --color-5: 90 100% 63%;
  --rainbow-speed: 3s;
}

*{ box-sizing: border-box; }

/* scroll-padding-top keeps the fixed 44px scrubber from covering the top of
   whatever we just jumped to. Without it every segment click, every #hash link
   and every browser scroll-restore buried the section's timecode label behind
   the bar. 60px = the bar plus a little air. */
html{ scroll-behavior: smooth; scroll-padding-top: 60px; }

@media (prefers-reduced-motion: reduce){
  html{ scroll-behavior: auto; }
  *{ animation-duration: 0.001ms !important; animation-iteration-count: 1 !important; transition-duration: 0.001ms !important; }
}

body{
  margin: 0;
  background: var(--bg);
  color: var(--text);
  font-family: var(--body);
  line-height: 1.55;
  -webkit-font-smoothing: antialiased;
}

a{ color: inherit; text-decoration: none; }
img{ max-width: 100%; display: block; }

.wrap{
  max-width: var(--wrap);
  margin: 0 auto;
  padding: 0 28px;
}

::selection{ background: var(--orange); color: #1a1310; }

:focus-visible{
  outline: 2px solid var(--teal);
  outline-offset: 3px;
}

/* ---------- Timeline scrubber (top, fixed) ---------- */

.scrubber{
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 100;
  height: 44px;
  background: rgba(18,16,14,0.86);
  backdrop-filter: blur(8px);
  border-bottom: 1px solid var(--line);
  display: flex;
  align-items: stretch;
}

.scrubber-track{
  flex: 1;
  display: flex;
  position: relative;
}

.scrubber-seg{
  flex: 1;
  position: relative;
  border: none;
  border-right: 1px solid var(--line);
  background: transparent;
  padding: 0;
  margin: 0;
  font: inherit;
  text-align: left;
  color: inherit;
  cursor: pointer;
  overflow: hidden;
  -webkit-tap-highlight-color: transparent;
}

.scrubber-seg:focus-visible{
  outline: none;
  box-shadow: inset 0 0 0 2px var(--teal);
}

.scrubber-seg::before{
  content: '';
  position: absolute;
  inset: 0;
  background: var(--seg-color, var(--teal));
  opacity: 0.16;
  transition: opacity .25s ease;
}

.scrubber-seg.active::before{ opacity: 0.42; }
.scrubber-seg:hover::before{ opacity: 0.55; }

.scrubber-seg span{
  position: absolute;
  left: 8px;
  bottom: 4px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: 0.03em;
  /* Was --muted, which reads at 6.2:1 against the bar and was hard to pick out
     at 10px. --text takes that to 16.6:1. */
  color: var(--text);
  white-space: nowrap;
  transition: color .2s ease, text-shadow .2s ease;
}

/* Bolder on hover, and on the segment you are actually in.

   Safe inside the segment's overflow: hidden because Space Mono is monospaced
   and the page already loads a real 700, so the label's advance width does not
   change. A proportional face here would widen the text and clip it - the
   mobile short labels have already overflowed a 42px segment once. */
.scrubber-seg:hover span,
.scrubber-seg.active span{
  font-weight: 700;
  text-shadow: 0 0 12px color-mix(in srgb, var(--seg-color, var(--teal)) 55%, transparent);
}

.scrubber-seg .seg-short{ display: none; }

.scrubber-code{
  width: 118px;
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  font-family: var(--mono);
  font-size: 12px;
  color: var(--teal);
  border-left: 1px solid var(--line);
  letter-spacing: 0.04em;
}

.playhead{
  position: absolute;
  top: 0; bottom: 0;
  width: 2px;
  background: var(--orange);
  box-shadow: 0 0 8px var(--orange);
  pointer-events: none;
  transition: left .1s linear;
}

body{ padding-top: 44px; }

/* ---------- Nav (within hero) ---------- */

.nav{
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 22px 0;
}

.nav-mark{
  font-family: var(--mono);
  font-size: 13px;
  color: var(--muted);
}

.nav-links{
  display: flex;
  gap: 28px;
  font-size: 14px;
  color: var(--muted);
}

.nav-links a:hover{ color: var(--text); }

/* ---------- Section scaffolding ---------- */

section{ padding: 96px 0; border-bottom: 1px solid var(--line); }

.eyebrow{
  font-family: var(--mono);
  font-size: 12px;
  color: var(--teal);
  letter-spacing: 0.08em;
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 18px;
}

.eyebrow::before{
  content: '';
  width: 7px; height: 7px;
  border-radius: 50%;
  background: var(--orange);
}

h1, h2, h3{
  font-family: var(--display);
  font-weight: 700;
  line-height: 1.05;
  margin: 0 0 0.4em;
  letter-spacing: -0.01em;
}

h2{ font-size: clamp(28px, 4vw, 42px); }

.section-head{
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  gap: 24px;
  margin-bottom: 48px;
  flex-wrap: wrap;
}

.section-head p{ color: var(--muted); max-width: 46ch; margin: 0; }

/* ---------- Line-wipe headline ---------- */
/* The element still carries .reveal, purely to get picked up by the same
   IntersectionObserver as everything else. These two rules cancel .reveal's
   own fade and lift, because the wipe below is the entrance - running both
   reads as the headline moving twice. */
.line-wipe.reveal{ opacity: 1; transform: none; }

.wipe-line{
  display: block;
  overflow: hidden;
  /* Descenders in the display face sit below the text box and would be
     clipped by the overflow. The padding gives them room and the equal
     negative margin keeps the line spacing where it was. */
  padding-bottom: 0.12em;
  margin-bottom: -0.12em;
}

.wipe-line > span{
  display: block;
  transform: translateY(105%);
  transition: transform .78s var(--ramp);
}

/* Second line trails the first, so the two read as separate beats. */
.wipe-line:nth-child(2) > span{ transition-delay: .11s; }

.line-wipe.is-in .wipe-line > span{ transform: translateY(0); }

p{ color: var(--text); }
.muted{ color: var(--muted); }

/* ---------- Hero ---------- */

.hero{
  padding-top: 64px;
  border-bottom: 1px solid var(--line);
  position: relative;
}

.hero-grid{
  display: grid;
  grid-template-columns: 1.3fr 0.9fr;
  gap: 48px;
  align-items: center;
  padding: 40px 0 72px;
}

.hero-copy{
  padding: 56px 0 80px;
  max-width: 860px;
}

/* ---------- Hero floating tool marks ---------- */

.hero-icons{
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none; /* decorative - must never eat a click on the CTAs */
  z-index: 0;

  /* Held until the hero has its final height. inset:0 means this box tracks the
     hero, and mid-layout the hero is ~18px tall - which collapses every
     percentage-positioned mark onto a single point. Revealing before then makes
     all fourteen appear stacked in one spot and then fly outward to their real
     places, which reads as a spiral bursting from the middle. JS adds .is-ready
     once the geometry has settled.

     A plain opacity gate rather than an animation gate, so it also covers
     reduced motion, where there is no entrance to hold back. If JS never runs
     the marks stay hidden, which is the right failure for decoration. */
  opacity: 0;
}

.hero-icons.is-ready{ opacity: 1; }

/* Keep the copy above the drifting marks. */
.hero .wrap{ position: relative; z-index: 1; }

/* Outer element carries the cursor-repel offset (set from JS); the inner one
   carries the idle float. Splitting them means the two transforms compose
   instead of overwriting each other. */
.hero-icon{
  position: absolute;
  will-change: transform;
}

/* The Adobe marks are self-contained badges (dark plate, bright letters,
   transparent corners) so they need no background of our own.

   Size is per-icon rather than a fixed square, because the set is no longer
   all square badges: Descript is a 4.1:1 wordmark and Figma is 0.68:1. Boxed
   uniformly, those two would render at a fraction of the visual weight of a
   square. Sizes are matched on AREA instead (h = sqrt(A/ratio), A = 62^2),
   which is the same rule the client marquee uses - see setupLogoSizing.

   Marks render at full opacity. They were held at 0.5-0.74 with a slight
   per-icon variation so the field read as depth rather than wallpaper, but on
   a background this dark that just made them look faded. Depth now comes from
   the size differences, the drift and the drop shadow. */
.hero-icon-inner{
  display: block;
  width: var(--icon-w, 62px);
  height: var(--icon-h, 62px);
  filter: drop-shadow(0 16px 26px rgba(0,0,0,0.75));
  opacity: var(--icon-op, 1);

  /* Only the idle drift lives here. The one-shot spiral entrance is built in
     setupHeroRise, because its path depends on where each mark sits relative
     to the hero's centre - which is not knowable until layout. The two coexist
     because the entrance animates the INDIVIDUAL translate/rotate/scale
     properties while heroFloat animates the transform shorthand, and the
     browser composes those rather than letting one win.

     Held until the spiral has landed, so nothing wobbles mid-flight. Pausing
     rather than delaying preserves each mark's negative --float-delay, so they
     resume at desynced points in the loop instead of drifting in lockstep. */
  animation: heroFloat var(--float-dur, 12s) ease-in-out var(--float-delay, 0s) infinite;
  animation-play-state: paused;
}

.hero-icons.is-in .hero-icon-inner{ animation-play-state: running; }

/* DaVinci Resolve ships a single-colour #233a51 mark. Composited on --bg that
   is 1.6:1 - a dark smudge, not a logo. Reversed to white, which is the
   treatment the brand publishes for dark surfaces. drop-shadow has to be
   restated: filter is one property, so the base rule's value is replaced. */
.hero-icon-invert{
  filter: brightness(0) invert(1) drop-shadow(0 16px 26px rgba(0,0,0,0.75));
}

.hero-icon-inner img{
  width: 100%;
  height: 100%;
  display: block;
  object-fit: contain;
}

@keyframes heroFloat{
  0%, 100%{ transform: translate(0, 0) rotate(0deg); }
  25%     { transform: translate(7px, -10px) rotate(4deg); }
  50%     { transform: translate(0, -4px) rotate(0deg); }
  75%     { transform: translate(-7px, 8px) rotate(-4deg); }
}

/* Below this the copy grows into the space the marks occupy. */
@media (max-width: 1100px){
  .hero-icons{ display: none; }
}

@media (prefers-reduced-motion: reduce){
  .hero-icon-inner{ animation: none; }
}

.hero h1{
  font-size: clamp(48px, 8vw, 92px);
}

/* ---------- The name cuts in letter by letter ---------- */

/* Each letter arrives with its colour channels pulled apart and snaps them
   back into register - the misalignment you get off a bad tape transfer, in
   the site's own two grade colours. Nine letters on a 50ms stagger, so the
   word assembles left to right in about half a second.

   .reveal is cancelled the same way .line-wipe cancels it: the element keeps
   the class purely to be picked up by the shared IntersectionObserver, and the
   letters own the entrance instead of fading in behind it.

   nowrap because nine inline-blocks are nine legal break points, and without
   it a narrow viewport can split the name mid-word. */
.ltr-cut{ white-space: nowrap; }
.ltr-cut.reveal{ opacity: 1; transform: none; }

.ltr-cut .ltr{
  display: inline-block;
  opacity: 0;
  transition: text-shadow .16s ease, transform .16s var(--ramp-soft);
}

/* backwards, not both. `both` would leave the 100% frame's text-shadow: none
   locked on as an animation value, which outranks any normal declaration and
   would kill the hover split below for good. backwards holds the 0% frame
   during the delay and then hands control back to these base styles. */
.ltr-cut.is-in .ltr{
  opacity: 1;
  animation: ltrCut .52s var(--ramp) backwards;
  animation-delay: calc(var(--i) * .05s);
}

/* Point at a single letter and its channels drift apart again. */
.ltr-cut .ltr:hover{
  text-shadow: -.02em 0 var(--teal), .02em 0 var(--orange);
  transform: translateY(-.02em);
}

@keyframes ltrCut{
  0%{
    opacity: 0;
    transform: translateY(.16em) scaleY(.88);
    filter: blur(3px);
    text-shadow: -.05em 0 var(--teal), .05em 0 var(--orange);
  }
  55%{
    opacity: 1;
    filter: blur(0);
    text-shadow: -.022em 0 var(--teal), .022em 0 var(--orange);
  }
  100%{
    opacity: 1;
    transform: none;
    filter: blur(0);
    text-shadow: none;
  }
}

@media (prefers-reduced-motion: reduce){
  .ltr-cut .ltr{ animation: none; opacity: 1; }
}

.hero-role{
  font-family: var(--mono);
  font-size: 13px;
  color: var(--orange);
  letter-spacing: 0.04em;
  margin-bottom: 20px;
}

.hero-lede{
  font-size: 19px;
  color: var(--text);
  max-width: 52ch;
  margin: 22px 0 30px;
}

.hero-sub{ color: var(--muted); max-width: 52ch; margin: 0 0 30px; }

/* Open-to-work status. No container - just the live dot and the label.
   The dot carries LinkedIn's availability green; the label sits in the
   site's off-white body colour. */
.status-pill{
  --status: #00ff40;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--mono);
  font-size: 12px;
  color: var(--text);
  padding: 0;
  margin-bottom: 30px;
}

.status-pill .dot{
  width: 7px; height: 7px; border-radius: 50%;
  background: var(--status);
  box-shadow: 0 0 6px var(--status);
  animation: pulse 2s ease-in-out infinite;
}

@keyframes pulse{ 0%,100%{ opacity: 1; } 50%{ opacity: 0.35; } }

.btn{
  display: inline-flex;
  align-items: center;
  gap: 10px;
  background: var(--orange);
  color: #1a1310;
  font-weight: 600;
  padding: 13px 24px;
  border-radius: 999px;
  border: none;
  cursor: pointer;
  font-size: 15px;
  transition: transform .18s ease, background .18s ease;
}

.btn:hover{ transform: translateY(-2px); background: #ffa06c; }

.btn-ghost{
  background: transparent;
  color: var(--text);
  border: 1px solid var(--line);
}

.btn-ghost:hover{ border-color: var(--teal); background: transparent; transform: translateY(-2px); }

/* ---------- Interactive hover button ----------
   Resting: outlined pill with a small accent dot. On hover the dot expands to
   flood the pill while the label slides out and a label+arrow slides in. */

.ihb{
  --ihb-accent: var(--orange);
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  min-width: 168px;
  /* Symmetric padding keeps the label centred in the pill; it's wide enough
     that the label's left edge always clears the dot sitting at 20px. */
  padding: 14px 44px;
  border: 1px solid var(--line);
  border-radius: 999px;
  background: transparent;
  color: var(--text);
  font-family: var(--body);
  font-size: 15px;
  font-weight: 600;
  line-height: 1.2;
  cursor: pointer;
  text-align: center;
  transition: border-color .3s ease;
}

.ihb-teal{ --ihb-accent: var(--teal); }

.ihb:hover{ border-color: var(--ihb-accent); }

/* Resting label - slides right and fades as the flood arrives. Sits at
   translateX(0) so it stays optically centred in the pill; the original's
   4px nudge existed only because its dot sat behind the text. */
.ihb-label{
  position: relative;
  z-index: 2;
  display: inline-block;
  transform: translateX(0);
  transition: transform .3s ease, opacity .3s ease;
}

.ihb:hover .ihb-label,
.ihb:focus-visible .ihb-label{
  transform: translateX(48px);
  opacity: 0;
}

/* Incoming label + arrow, parked off to the right until hover. */
.ihb-hover{
  position: absolute;
  inset: 0;
  z-index: 3;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  color: #1a1310;
  transform: translateX(48px);
  opacity: 0;
  transition: transform .3s ease, opacity .3s ease;
}

.ihb:hover .ihb-hover,
.ihb:focus-visible .ihb-hover{
  transform: translateX(0);
  opacity: 1;
}

.ihb-hover svg{ width: 17px; height: 17px; flex-shrink: 0; }

/* Back variant - a backwards action should read backwards, so the label
   exits left and the arrow-first layer arrives from the left. */
.ihb-back .ihb-hover{ transform: translateX(-48px); }

.ihb-back:hover .ihb-label,
.ihb-back:focus-visible .ihb-label{ transform: translateX(-48px); }

.ihb-back:hover .ihb-hover,
.ihb-back:focus-visible .ihb-hover{ transform: translateX(0); }

/* The dot that becomes the fill. Sits inside the left padding, vertically
   centred via a top offset rather than a transform, so the hover scale()
   has nothing to fight. */
.ihb-dot{
  position: absolute;
  z-index: 1;
  left: 20px;
  top: calc(50% - 4px);
  width: 8px;
  height: 8px;
  border-radius: 8px;
  background: var(--ihb-accent);
  transition: all .3s ease;
}

.ihb:hover .ihb-dot,
.ihb:focus-visible .ihb-dot{
  left: 0;
  top: 0;
  width: 100%;
  height: 100%;
  border-radius: 999px;
  transform: scale(1.8);
}

@media (prefers-reduced-motion: reduce){
  .ihb-label, .ihb-hover, .ihb-dot{ transition: none; }
}

/* ---------- Rainbow CTA variant ----------
   For the one "do the thing" link per project page (Watch now, YouTube
   channel). Everything else on the site is deliberately quiet, so the primary
   action needs to shout; this is the only place the full spectrum appears.

   Three layers share ONE animated background-position, which is what keeps the
   border sweep and the bottom fade locked together:
     1. flat fill, clipped to padding-box  -> the button face
     2. vertical fade, clipped to border-box -> lets the rainbow through at the
        bottom edge only, so the border reads brightest where the glow sits
     3. the rainbow itself, clipped to border-box -> visible through the
        transparent border and through layer 2's fade

   Layers 1 and 2 are vertical gradients, so sliding them sideways is a no-op.
   Only the rainbow actually appears to move. */

@keyframes rainbow{
  0%{ background-position: 0%; }
  100%{ background-position: 200%; }
}

/* The halo sits below the button and must not be clipped, but .ihb needs
   overflow:hidden to contain the hover flood - so the glow lives on a shell
   around the button rather than on the button itself. */
.ihb-shell{
  position: relative;
  display: inline-flex;
}

.ihb-shell::before{
  content: '';
  position: absolute;
  z-index: 0;
  left: 50%;
  bottom: -18%;
  transform: translateX(-50%);
  width: 62%;
  height: 22%;
  background: linear-gradient(90deg,
    hsl(var(--color-1)), hsl(var(--color-5)), hsl(var(--color-3)),
    hsl(var(--color-4)), hsl(var(--color-2)));
  background-size: 200%;
  filter: blur(13px);
  animation: rainbow var(--rainbow-speed) infinite linear;
  pointer-events: none;
}

.ihb-shell > .ihb{ z-index: 1; }

.ihb-rainbow{
  border: 2px solid transparent;
  background:
    linear-gradient(var(--bg), var(--bg)),
    linear-gradient(var(--bg) 45%, rgba(18,16,14,0.55) 78%, rgba(18,16,14,0)),
    linear-gradient(90deg,
      hsl(var(--color-1)), hsl(var(--color-5)), hsl(var(--color-3)),
      hsl(var(--color-4)), hsl(var(--color-2)));
  background-size: 200%;
  background-clip: padding-box, border-box, border-box;
  background-origin: border-box;
  animation: rainbow var(--rainbow-speed) infinite linear;
}

/* The base rule tints the border on hover, which would paint over the sweep. */
.ihb-rainbow,
.ihb-rainbow:hover{ border-color: transparent; }

/* Halo tightens and brightens on hover, so the button still responds even
   though the flood already covers the face. */
.ihb-shell:hover::before,
.ihb-shell:focus-within::before{
  bottom: -24%;
  width: 74%;
  filter: blur(16px);
}

.ihb-shell::before{ transition: bottom .3s ease, width .3s ease, filter .3s ease; }

@media (prefers-reduced-motion: reduce){
  /* The global reduce rule collapses the duration, which parks the sweep at an
     arbitrary frame. Pin it to a known one instead and drop the halo pulse. */
  .ihb-rainbow, .ihb-shell::before{
    animation: none;
    background-position: 50%;
  }
  .ihb-shell::before{ transition: none; }
}

.hero-frame{
  position: relative;
  aspect-ratio: 4/5;
  border-radius: var(--radius);
  background: linear-gradient(155deg, #23433d 0%, #1b1815 55%, #4a2c1a 130%);
  border: 1px solid var(--line);
  display: flex;
  align-items: flex-end;
  overflow: hidden;
}

.hero-frame .frame-tag{
  font-family: var(--mono);
  font-size: 11px;
  color: rgba(244,239,230,0.7);
  padding: 14px;
  display: flex;
  justify-content: space-between;
  width: 100%;
}

.hero-frame .rec{
  position: absolute;
  top: 14px; left: 14px;
  display: flex;
  align-items: center;
  gap: 6px;
  font-family: var(--mono);
  font-size: 11px;
  color: #ff5c5c;
}

.hero-frame .rec .dot{
  width: 7px; height: 7px; border-radius: 50%; background: #ff5c5c;
  animation: pulse 1.4s ease-in-out infinite;
}

/* ---------- Stats ---------- */

.stat-grid{
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(190px, 1fr));
  gap: 14px;
  margin: 40px 0 56px;
}

.stat{
  display: flex;
  flex-direction: column;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 18px 18px 16px;
}

/* Two-class selector so this beats .reveal's transition shorthand, which is
   declared later in the file and would otherwise drop border-color entirely.
   Covers the entrance (opacity/transform) and the hover in one declaration. */
.stat-grid .stat{
  transition:
    opacity .5s steps(5, end),
    transform .38s var(--ramp-soft),
    border-color .25s ease;
}

.stat-grid .stat:hover{
  border-color: var(--stat-accent, var(--teal));
  transform: translateY(-3px);
}

/* Alternate the accent so the row reads in the site's teal/orange grade. */
.stat:nth-child(even){ --stat-accent: var(--orange); }

.stat-top{
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 22px;
}

.stat-icon{
  display: inline-flex;
  color: var(--stat-accent, var(--teal));
}

.stat-icon svg{ width: 20px; height: 20px; display: block; }

.stat-tag{
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--stat-accent, var(--teal));
  background: color-mix(in srgb, var(--stat-accent, var(--teal)) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--stat-accent, var(--teal)) 30%, transparent);
  padding: 3px 9px;
  border-radius: 999px;
  line-height: 1.5;
}

.stat-label{
  font-size: 13px;
  color: var(--muted);
  line-height: 1.45;
}

.stat .stat-num{
  display: block;
  font-family: var(--display);
  font-size: clamp(28px, 3.4vw, 38px);
  line-height: 1.1;
  color: var(--text);
  margin: 4px 0 14px;
  font-variant-numeric: tabular-nums;
}

.stat-meta{
  margin-top: auto;
  padding-top: 12px;
  border-top: 1px solid var(--line);
  font-family: var(--mono);
  font-size: 11px;
  color: var(--muted);
  line-height: 1.5;
}

/* Depth on hover. The tilt alone rotates a flat plate; lifting the contents off
   the card face on their own Z gives it parallax, so the card reads as having
   thickness rather than as a picture of a tilted card.

   These translateZ values do nothing at rest, and that is deliberate: with no
   perspective in play they are not projected at all. setupTilt writes
   perspective(900px) into the card's transform on pointermove, which is the
   moment they start to matter.

   Kept modest on purpose. Z moves an element closer, so it magnifies it -
   42px against a 900px perspective is 1.05x, and the mono text at the bottom
   sits shallower still, because that is the type that goes soft first. */
.stat{ transform-style: preserve-3d; }

.stat-top{ transform: translateZ(30px); }
.stat .stat-num{ transform: translateZ(42px); }
.stat-label{ transform: translateZ(20px); }
.stat-meta{ transform: translateZ(10px); }

/* A cast shadow sells the lift more than the rotation does. */
.stat-grid .stat{ box-shadow: 0 0 0 rgba(0,0,0,0); transition-property: opacity, transform, border-color, box-shadow; }
.stat-grid .stat:hover{ box-shadow: 0 28px 52px -26px rgba(0,0,0,0.9); }

/* Flat, in every sense. setupTilt already returns early here, so nothing writes
   a perspective and the Z offsets are inert anyway - this just says so. */
@media (prefers-reduced-motion: reduce){
  .stat, .do-item{ transform-style: flat; }
  .stat-top, .stat .stat-num, .stat-label, .stat-meta,
  .do-item .tc, .do-item p{ transform: none; }
}

/* Entrance stagger across the row. */
.stat-grid .stat:nth-child(1){ transition-delay: 0s; }
.stat-grid .stat:nth-child(2){ transition-delay: .07s; }
.stat-grid .stat:nth-child(3){ transition-delay: .14s; }
.stat-grid .stat:nth-child(4){ transition-delay: .21s; }
.stat-grid .stat:nth-child(5){ transition-delay: .28s; }

/* Hover shouldn't inherit the entrance delay - it makes the card feel laggy.
   Same specificity as the nth-child rules above, so source order wins. */
.stat-grid .stat:hover{ transition-delay: 0s; }

.about-body{ max-width: 68ch; font-size: 16.5px; color: var(--text); }

/* Marquee viewport. Two identical tracks sit side by side; each slides left
   by its own full width + gap, so as one clears the frame the other has
   taken its place - a seamless loop with no JS. Edges fade out via mask. */
.logos{
  --logo-gap: 12px;
  --marquee-duration: 42s;
  display: flex;
  gap: var(--logo-gap);
  margin-top: 28px;
  overflow: hidden;
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
          mask-image: linear-gradient(90deg, transparent, #000 6%, #000 94%, transparent);
}

.logos-track{
  display: flex;
  flex: 0 0 auto;
  gap: var(--logo-gap);
  will-change: transform;
}

/* Scrubbable: grab the strip and drag it either way. The drift and the drag
   share one JS transform loop - a CSS animation can't be dragged mid-flight
   without fighting the compositor. */
.logos{
  cursor: grab;
  user-select: none;
  -webkit-user-select: none;
  touch-action: pan-y; /* horizontal is ours, vertical still scrolls the page */
}

.logos.is-dragging{ cursor: grabbing; }
.logos img{ -webkit-user-drag: none; user-drag: none; }

/* Bare logos, no plate - the chip is just a spacing box. */
.logo-chip{
  flex: 0 0 auto;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  white-space: nowrap;
  font-family: var(--mono);
  font-size: 12.5px;
  color: var(--muted);
  padding: 0 8px;
}

/* Fallback height only - JS replaces this per logo with an optically matched
   size, because normalising by height alone makes squarer marks (Google Cloud)
   read far smaller than wide wordmarks (AMD) at the same pixel height. */
.logo-chip img{
  height: 34px;
  width: auto;
  max-width: 170px;
  object-fit: contain;
  opacity: 0.9;
  transition: opacity .25s ease;
}

/* A few supplied logos are solid black/navy wordmarks that composite to 0%
   visible ink on this background. White is those brands' own reversed
   treatment for dark surfaces, so lift just those - everything else keeps
   its real colours. Swap in an official reversed asset to drop this. */
.logo-chip.invert img{ filter: brightness(0) invert(1); }

.logo-chip:hover img{ opacity: 1; }

/* No auto-motion: fall back to a normal horizontal scroll the user drives. */
@media (prefers-reduced-motion: reduce){
  .logos{ overflow-x: auto; }
  .logos-track{ animation: none; }
  .logos-track[aria-hidden="true"]{ display: none; }
}

/* ---------- What I do ---------- */

.do-grid{
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 14px;
}

.do-item{
  --do-accent: var(--teal);
  position: relative;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 26px 24px 28px;
  min-height: 150px;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  /* Same depth treatment as the stat cards in About: the children ride their
     own Z so the card has thickness rather than being a rotating plate.

     This is why there is no overflow: hidden here any more. Any overflow other
     than visible forces transform-style back to flat, which would silently
     kill the parallax below while the tilt still worked - the confusing kind
     of broken. The bar it used to clip is inset instead, see .do-bar. */
  transform-style: preserve-3d;
}

/* Inert until setupTilt writes perspective() on pointermove. */
.do-item .tc{ transform: translateZ(34px); }
.do-item p{ transform: translateZ(20px); }

/* Alternating grade - the accent drives number, border and scrub bar. */
.do-item:nth-child(even){ --do-accent: var(--orange); }

/* Two-class selector to beat .reveal's later transition shorthand. */
.do-grid .do-item{
  transition:
    opacity .5s steps(5, end),
    transform .38s var(--ramp-soft),
    border-color .25s ease,
    box-shadow .25s ease;
}

/* The cast shadow does more of the lifting than the rotation does. */
.do-grid .do-item:hover{
  border-color: var(--do-accent);
  transform: translateY(-4px);
  box-shadow: 0 26px 46px -24px rgba(0,0,0,0.9);
  transition-delay: 0s;
}

.do-item .tc{
  font-family: var(--mono);
  font-size: 20px;
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--do-accent);
  transition: opacity .25s ease;
  opacity: .75;
}

.do-grid .do-item:hover .tc{ opacity: 1; }

.do-item p{ font-size: 16px; font-weight: 500; margin: 18px 0 0; }

/* Scrub bar along the bottom edge - fills on hover, like a clip playing.
   Inset by the card's own 14px radius: that is exactly where the bottom edge
   stops being straight, so the bar now ends where the corner starts curving
   instead of poking out past it. It used to rely on overflow: hidden for that,
   which the card can no longer afford (see .do-item). */
.do-bar{
  position: absolute;
  left: 14px; right: 14px; bottom: 0;
  height: 2px;
  border-radius: 2px;
  background: var(--do-accent);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform .45s var(--ramp-soft);
}

.do-grid .do-item:hover .do-bar{ transform: scaleX(1); }

/* Entrance stagger, row by row. */
.do-grid .do-item:nth-child(1){ transition-delay: 0s; }
.do-grid .do-item:nth-child(2){ transition-delay: .06s; }
.do-grid .do-item:nth-child(3){ transition-delay: .12s; }
.do-grid .do-item:nth-child(4){ transition-delay: .18s; }
.do-grid .do-item:nth-child(5){ transition-delay: .24s; }
.do-grid .do-item:nth-child(6){ transition-delay: .30s; }

/* ---------- Experience timeline ---------- */

.timeline{ position: relative; }

.tl-row{
  --i: 0;
  position: relative;
  display: grid;
  grid-template-columns: 160px 1fr;
  gap: 32px;
  padding: 28px 0;
}

/* Two-class selector: .reveal's `transition` shorthand is declared later in
   the file and resets transition-delay to 0, killing the stagger. */
.timeline .tl-row{ transition-delay: calc(var(--i) * .08s); }

/* Separator drawn as a wipe rather than a static border, so each clip in the
   timeline "cuts in" as it arrives. */
.tl-row::before{
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0;
  height: 1px;
  background: var(--line);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform .8s var(--ramp-soft) calc(var(--i) * .08s);
}

.tl-row:first-child::before{ display: none; }
.tl-row.is-in::before{ transform: scaleX(1); }

/* Per-row hover marker: a short playhead segment that draws down the row's own
   left edge, top to bottom, while the row is hovered. One row at a time, which
   is the point - it marks where you are rather than how far you have read.
   Replaced briefly by a scroll-filled rail down the whole section; that sat at
   this same x and drew on top of this, so only one of the two can exist. */
.tl-row::after{
  content: '';
  position: absolute;
  left: -16px; top: 0;
  width: 2px; height: 100%;
  border-radius: 2px;
  background: linear-gradient(180deg, var(--teal), var(--orange));
  transform: scaleY(0);
  transform-origin: top;
  transition: transform .34s var(--ramp-soft);
}

/* Hover must not inherit the rows' entrance stagger, or the lower rows feel
   laggy to point at. */
.tl-row:hover::after{
  transform: scaleY(1);
  transition-delay: 0s;
}
.tl-row:hover .tl-when{ color: var(--orange); }
.tl-when{ transition: color .25s ease; }

.tl-row:nth-child(2){ --i: 1; }
.tl-row:nth-child(3){ --i: 2; }
.tl-row:nth-child(4){ --i: 3; }

.tl-when{
  font-family: var(--mono);
  font-size: 13px;
  color: var(--teal);
}

.tl-role{ font-size: 13px; color: var(--muted); margin-top: 6px; }

.tl-body h3{ font-size: 20px; margin-bottom: 8px; }
.tl-body p{ color: var(--muted); max-width: 62ch; font-size: 15px; }
.tl-body .earn{ color: var(--orange); font-family: var(--mono); font-size: 13px; margin-top: 8px; display: block; }

/* ---------- Tools (level meters) ---------- */

.tool-grid{
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 20px 40px;
}

.tool-row{
  --i: 0;
  display: grid;
  /* Widened from 170px to seat the mark alongside the name. */
  grid-template-columns: 208px 1fr 90px;
  align-items: center;
  gap: 16px;
}

/* Same shorthand-reset issue as .tl-row above. */
.tool-grid .tool-row{ transition-delay: calc(var(--i) * .05s); }

.tool-name{
  font-size: 14.5px;
  display: flex;
  align-items: center;
  gap: 10px;
}

.tool-logo{
  flex: 0 0 auto;
  width: 26px; height: 26px;
  display: grid;
  place-items: center;
  border-radius: 7px;
  background: var(--surface-2);
  border: 1px solid var(--line);
  transition: border-color .25s ease, transform .25s var(--ramp-soft);
}

/* contain, not cover: these marks have very different aspect ratios and
   cropping would cut into the logo itself. */
.tool-logo img{
  width: 16px; height: 16px;
  object-fit: contain;
  display: block;
}

.tool-row:hover .tool-logo{
  border-color: var(--teal);
  transform: translateY(-2px);
}

/* Meters ride up from zero when the section arrives, then overshoot and fall
   back - the way a level meter jumps past and settles. A keyframe rather than
   a transition, because a transition cannot travel past its own end value.
   `both` holds frame 0 through the delay, which is the job the old resting
   scaleX(0) used to do. */
.tool-row .meter i{
  transform: scaleX(0);
  transform-origin: left center;
}

.tool-row.is-in .meter i{
  animation: meterPeak .95s var(--ramp-soft) calc(var(--i) * .05s + .14s) both;
}

/* Peaks at 1.06 of each bar's own width. The widest here is 92%, so the
   overshoot tops out near 98% and never runs off the end of the channel. */
@keyframes meterPeak{
  0%   { transform: scaleX(0); }
  58%  { transform: scaleX(1.06); }
  78%  { transform: scaleX(0.975); }
  100% { transform: scaleX(1); }
}

/* Signal running along the level while its row is pointed at.

   This has to live on a pseudo-element. .meter i's own animation slot is
   already taken by meterPeak, and that animation's `both` fill is the only
   thing holding the bar open - the base rule underneath it is still
   scaleX(0). Adding a second animation to .meter i on hover would replace
   meterPeak, drop the fill, and collapse every meter you point at. */
.meter i::after{
  content: '';
  position: absolute;
  inset: 0;
  background: linear-gradient(90deg,
    transparent, rgba(255,255,255,0.45) 50%, transparent);
  transform: translateX(-100%);
  opacity: 0;
}

.tool-row:hover .meter i::after{
  opacity: 1;
  animation: meterSheen 1.15s var(--ramp-soft) infinite;
}

@keyframes meterSheen{
  from{ transform: translateX(-100%); }
  to{ transform: translateX(100%); }
}

.tool-row:nth-child(2){ --i: 1; }
.tool-row:nth-child(3){ --i: 2; }
.tool-row:nth-child(4){ --i: 3; }
.tool-row:nth-child(5){ --i: 4; }
.tool-row:nth-child(6){ --i: 5; }
.tool-row:nth-child(7){ --i: 6; }
.tool-row:nth-child(8){ --i: 7; }

.meter{
  height: 8px;
  background: var(--surface-2);
  border-radius: 4px;
  overflow: hidden;
  border: 1px solid var(--line);
}

/* overflow keeps the sheen below inside the filled part of the bar. Without it
   the highlight would sweep on across the empty channel, which reads as the
   track lighting up rather than as signal running through the level. */
.meter i{
  display: block;
  height: 100%;
  position: relative;
  overflow: hidden;
  background: linear-gradient(90deg, var(--teal), var(--orange));
  border-radius: 4px;
}

.tool-level{ font-family: var(--mono); font-size: 11px; color: var(--muted); text-align: right; }

/* Below 1000px the two-column tool grid leaves each row roughly 380px, and a
   208px name + 90px level + gaps eats all but a sliver of it - the meter, the
   one thing the row exists to show, ends up ~36px wide. So the row reflows:
   name and level share the top line, the meter gets the full width beneath.
   (At 170px this was already tight; the mark alongside the name is what made
   it worth fixing rather than shaving pixels off the label column.) */
@media (max-width: 1000px){
  .tool-row{
    grid-template-columns: 1fr auto;
    grid-template-areas:
      "name  level"
      "meter meter";
    gap: 9px 12px;
  }
  .tool-name{ grid-area: name; }
  .tool-row .meter{ grid-area: meter; }
  .tool-level{ grid-area: level; }
}

/* ---------- Work grid ---------- */

/* Masonry columns: cards keep their true height, no ragged grid rows. */
.work-grid{
  columns: 2;
  column-gap: 24px;
}

.work-card{
  position: relative;
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  display: block;
  width: 100%;
  break-inside: avoid;
  -webkit-column-break-inside: avoid;
  margin-bottom: 24px;
  transition: border-color .2s ease, transform .2s ease;
}

.work-card:hover{ border-color: var(--teal-dim); transform: translateY(-3px); }

/* Alternate glow color per card, echoing the scrubber's teal/orange pairing. */
.work-card:nth-of-type(even){ --glow: var(--orange); }

/* Spotlight: mouse position tracked locally per card (--mx/--my set by JS
   on pointermove), revealed on hover. Ambient wash behind the content,
   plus a bright ring masked to just the 1px border. */
.work-card::before,
.work-card::after{
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  opacity: 0;
  transition: opacity .25s ease;
  pointer-events: none;
}

.work-card:hover::before,
.work-card:hover::after{ opacity: 1; }

.work-card::after{
  z-index: 0;
  background: radial-gradient(
    280px 280px at var(--mx, 50%) var(--my, 40%),
    color-mix(in srgb, var(--glow, var(--teal)) 22%, transparent), transparent 70%
  );
}

.work-card::before{
  z-index: 3;
  padding: 1px;
  background: radial-gradient(
    160px 160px at var(--mx, 50%) var(--my, 40%),
    color-mix(in srgb, var(--glow, var(--teal)) 95%, transparent), transparent 70%
  );
  -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite: xor;
  mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  mask-composite: exclude;
}

/* Thumb height comes from the video's own dimensions - no forced ratio. */

/* Pointer over a card becomes a timeline playhead.
   The site already runs on this motif - the fixed scrubber at the top of every
   page carries a playhead and the section labels are timecodes - so the marker
   reads as part of the design rather than as a stock "cut" icon.

   The mark is drawn twice: a heavy --bg outline underneath, then the orange on
   top. Thumbnails here run from a near-white Zoom UI to a black gaming frame,
   and a single-colour cursor vanishes into one end or the other.

   Hotspot is 16 3: dead centre on the playhead's own line, at the top of the
   head, so the line sits exactly where you are pointing.

   32x32 with a keyword fallback. Anything larger is refused outright by some
   browsers, and a custom cursor that fails to load with no fallback leaves the
   pointer invisible. */
.work-card{
  cursor:
    url("data:image/svg+xml,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20width%3D%2732%27%20height%3D%2732%27%20viewBox%3D%270%200%2032%2032%27%3E%3Cg%20stroke%3D%27%2312100e%27%20stroke-width%3D%274%27%20stroke-linejoin%3D%27round%27%20stroke-linecap%3D%27round%27%20fill%3D%27%2312100e%27%3E%3Cpath%20d%3D%27M10.5%203%20H21.5%20V10.5%20L16%2015.5%20L10.5%2010.5%20Z%27%2F%3E%3Cpath%20d%3D%27M16%2015.5%20V29.5%27%20fill%3D%27none%27%2F%3E%3C%2Fg%3E%3Cpath%20d%3D%27M10.5%203%20H21.5%20V10.5%20L16%2015.5%20L10.5%2010.5%20Z%27%20fill%3D%27%23ff8a4c%27%2F%3E%3Cpath%20d%3D%27M16%2015.5%20V29.5%27%20stroke%3D%27%23ff8a4c%27%20stroke-width%3D%273.2%27%20stroke-linecap%3D%27round%27%20fill%3D%27none%27%2F%3E%3C%2Fsvg%3E") 16 3,
    pointer;
}

/* Underline that draws in under the project title on hover. Origin follows the
   direction of travel: it grows from the left on the way in and retracts to the
   right on the way out, rather than snapping back through its own middle. */
.work-info h3{
  position: relative;
  display: inline-block;
}

.work-info h3::after{
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: -4px;
  height: 2px;
  border-radius: 2px;
  background: linear-gradient(90deg, var(--teal), var(--orange));
  transform: scaleX(0);
  transform-origin: right;
  transition: transform .34s var(--ramp-soft);
}

.work-card:hover .work-info h3::after,
.work-card:focus-visible .work-info h3::after{
  transform: scaleX(1);
  transform-origin: left;
}

@media (prefers-reduced-motion: reduce){
  .work-info h3::after{ transition: none; }
}

.work-thumb{
  position: relative;
  overflow: hidden;
  background: #0d0b0a;
  line-height: 0;
}

.work-thumb .dur{
  position: absolute;
  top: 16px; right: 16px;
  z-index: 2;
  line-height: 1.4;
  font-family: var(--mono);
  font-size: 11px;
  background: rgba(18,16,14,0.7);
  color: var(--text);
  padding: 4px 9px;
  border-radius: 999px;
  border: 1px solid var(--line);
}

.work-thumb .yr{
  position: absolute;
  top: 20px; left: 16px;
  z-index: 2;
  line-height: 1.4;
  font-family: var(--mono);
  font-size: 11px;
  color: rgba(244,239,230,0.85);
}

.work-info{ padding: 20px 22px 24px; }
.work-info .cat{ font-family: var(--mono); font-size: 11px; color: var(--orange); }
.work-info h3{ font-size: 20px; margin: 8px 0 8px; }
.work-info p{ font-size: 14px; color: var(--muted); }

.more-note{
  margin-top: 24px;
  font-size: 14px;
  color: var(--muted);
  border-top: 1px solid var(--line);
  padding-top: 20px;
}

/* ---------- Contact / footer ---------- */

.contact{ text-align: left; position: relative; }
.contact h2{ font-size: clamp(32px, 6vw, 56px); max-width: 14ch; }

/* ---------- Eyes ----------
   A pair that follows the cursor, parked in the empty right half of the
   contact block. The headline's ink runs to about 490px of a 1120px wrap, so
   this fills real dead space rather than crowding anything.

   The sclera is a warm off-white rather than the pure #fff of the original or
   the near-black first attempt here. Flat white is the brightest possible
   object on a --bg page and reads as a hole punched in it; near-black reads as
   a camera lens, not an eye. The gradient does the work instead: bright at the
   top left where the catchlight is, shading to a muted rim, so it has volume
   and its average is closer to the body text than to paper.

   Absolutely positioned so nothing above it in the contact markup has to
   move. Hidden below 900px, where the headline needs the full width. */

/* The block is still centred on the contact copy's midline; it is now a column
   so the caption can sit under the pair. The 34px that used to separate the
   two eyes moves to .eyes-pair with them - leaving it here would space the
   eyes from their own caption instead. */
.contact-eyes{
  position: absolute;
  top: 50%;
  right: 0;
  transform: translateY(-50%);
  display: flex;
  flex-direction: column;
  align-items: center;
  /* Wide on purpose. At 22px a two-line caption sitting that close under two
     eyes stops reading as a caption and starts reading as a mouth. */
  gap: 48px;
  pointer-events: none;   /* decorative - never intercept the email link */
}

.eyes-pair{
  display: flex;
  align-items: center;
  gap: 34px;
}

/* Clicking an eye makes the pair blink (setupEyes). Only the eye itself takes
   pointer events back - the block keeps pointer-events: none, so the caption
   and the empty space around the pair stay click-through and nothing here can
   ever come between a visitor and the email link. No cursor change on purpose:
   this is a flourish, not a control, and it is aria-hidden and unfocusable, so
   advertising it as a button would promise more than it delivers. */
.eye{ pointer-events: auto; }

/* Deliberately quiet: this is a footnote to a joke, not a second headline.
   Mono and --muted put it at the same weight as the timecode labels. */
.eyes-note{
  font-family: var(--mono);
  font-size: 11.5px;
  line-height: 1.55;
  letter-spacing: 0.02em;
  color: var(--muted);
  text-align: center;
}

.eye{
  --eye-w: 108px;
  --eye-h: 92px;
  position: relative;
  width: var(--eye-w);
  height: var(--eye-h);
  border-radius: 50%;
  overflow: hidden;
  background: radial-gradient(circle at 36% 28%,
    #fdfaf4 0%, #f0e9dc 46%, #d8cdbc 78%, #b6aa98 100%);
  box-shadow:
    inset 0 4px 10px rgba(58,44,30,0.26),
    inset 0 -3px 8px rgba(58,44,30,0.16),
    0 12px 32px rgba(0,0,0,0.55);
  /* Warm dark rim, not var(--line): on a light ball a cool grey outline reads
     as a cartoon stroke. This just seats it against the page. */
  border: 1px solid #3a3128;
  transform-origin: center;
  transition: transform .1s ease-in-out;
}

/* Blink. scaleY only, so the lids appear to meet in the middle. */
.eye.is-blinking{ transform: scaleY(0.06); }

.eye-iris{
  position: absolute;
  left: 50%;
  top: 50%;
  width: 48px;
  height: 48px;
  margin: -24px 0 0 -24px;
  border-radius: 50%;
  /* JS writes --ex/--ey; keeping the centring in margin rather than in the
     transform leaves translate free for the tracking offset. */
  translate: var(--ex, 0px) var(--ey, 0px);
  background: radial-gradient(circle at 38% 34%,
    #6fe0cb 0%, var(--teal) 42%, var(--teal-dim) 78%, #16443c 100%);
  /* The outer ring is a real limbal ring now that the sclera is light - it is
     what stops the iris bleeding into the white at its edge. */
  box-shadow: inset 0 2px 7px rgba(0,0,0,0.4), 0 0 0 1.5px rgba(20,42,38,0.55);
}

/* Radial fibres. One element with a repeating conic gradient rather than 24
   divs, which is what the React original used. */
.eye-fibres{
  position: absolute;
  inset: 0;
  border-radius: 50%;
  opacity: 0.5;
  background: repeating-conic-gradient(from 0deg,
    rgba(255,255,255,0.18) 0deg 2deg, transparent 2deg 15deg);
  mask: radial-gradient(circle, transparent 26%, #000 34%, #000 78%, transparent 92%);
  -webkit-mask: radial-gradient(circle, transparent 26%, #000 34%, #000 78%, transparent 92%);
}

/* Orange limbal ring - the one place the site's second grade colour appears
   here, so the eye is not flatly teal. */
.eye-ring{
  position: absolute;
  inset: 3px;
  border-radius: 50%;
  border: 2px solid color-mix(in srgb, var(--orange) 45%, transparent);
  opacity: 0.55;
}

.eye-pupil{
  position: absolute;
  left: 50%;
  top: 50%;
  width: 23px;
  height: 23px;
  margin: -11.5px 0 0 -11.5px;
  border-radius: 50%;
  background: #0a0908;
  /* No transition here on purpose. The dilation is eased in the rAF loop, so
     a CSS transition on top would be a second, competing smoother. */
  scale: var(--dilate, 1);
}

/* Catchlight, top left, matching where the sclera gradient puts its light. */
.eye-glint{
  position: absolute;
  left: 26%;
  top: 20%;
  width: 13px;
  height: 13px;
  border-radius: 50%;
  background: radial-gradient(circle, rgba(255,255,255,0.95), rgba(255,255,255,0.45));
  filter: blur(0.4px);
}

.eye-glint-sm{
  position: absolute;
  left: 62%;
  top: 63%;
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: rgba(255,255,255,0.55);
}

/* Lid shadows, so the ball reads as set into a socket. */
.eye-lid{
  position: absolute;
  left: 0; right: 0;
  pointer-events: none;
}
/* Softer and warmer than the dark-sclera version needed. At the old 0.5 black
   these sat on the off-white as two grey bands rather than as shading. */
.eye-lid-top{
  top: 0;
  height: 34%;
  background: linear-gradient(to bottom, rgba(48,36,24,0.26), transparent);
  border-radius: 50% 50% 0 0;
}
.eye-lid-bottom{
  bottom: 0;
  height: 20%;
  background: linear-gradient(to top, rgba(48,36,24,0.14), transparent);
  border-radius: 0 0 50% 50%;
}

@media (max-width: 1080px){
  .eye{ --eye-w: 88px; --eye-h: 75px; }
  /* Follows the eyes, not the block: this is the gap between the two eyes. */
  .eyes-pair{ gap: 26px; }
  .contact-eyes{ gap: 36px; }
  .eyes-note{ font-size: 11px; }
}

/* The headline needs the full width from here down. */
@media (max-width: 900px){
  .contact-eyes{ display: none; }
}

@media (prefers-reduced-motion: reduce){
  .eye{ transition: none; }
  .eye.is-blinking{ transform: none; }
}
.contact-email{
  position: relative;
  font-family: var(--mono);
  font-size: 18px;
  color: var(--teal);
  display: inline-block;
  margin: 18px 0 30px;
  padding-bottom: 4px;
}

/* Underline draws itself in when the section arrives. */
.contact-email::after{
  content: '';
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 1px;
  background: var(--teal-dim);
  transform: scaleX(0);
  transform-origin: left center;
  transition: transform .7s var(--ramp-soft) .1s, background-color .25s ease;
}

.contact-email.is-in::after{ transform: scaleX(1); }
.contact-email:hover::after{ background: var(--teal); }

/* Blinking block caret - steps(1) so it snaps on/off like the frame-stutter
   reveals rather than fading. */
.caret{
  display: inline-block;
  width: 9px;
  height: 17px;
  background: var(--teal);
  margin-left: 7px;
  vertical-align: -2px;
  animation: caretBlink 1.1s steps(1) infinite;
}

@keyframes caretBlink{
  0%, 50%{ opacity: 1; }
  50.01%, 100%{ opacity: 0; }
}

@media (prefers-reduced-motion: reduce){
  .caret{ animation: none; opacity: 1; }
  .contact-email::after{ transform: scaleX(1); }
  .tl-row::before{ transform: scaleX(1); }
  .tool-row .meter i{ transform: scaleX(1); }
  /* The global animation-duration override only parks a looping animation on
     an arbitrary frame, which would leave a bright band frozen mid-bar. */
  .tool-row:hover .meter i::after{ animation: none; opacity: 0; }
  /* The row marker is hover-only, so there is nothing to reveal up front -
     it just needs to arrive without animating. */
  .tl-row:hover::after{ transition: none; }
  /* The wipe is the headline's only entrance - without motion it must just
     be there, or the copy never arrives at all. */
  .wipe-line > span{ transform: none; }
}

.contact-links{ display: flex; align-items: center; font-size: 14px; color: var(--muted); }

/* Logo reveal - the badge pops up from behind the label on hover (blurred
   and tiny to full focus, with a small random tilt per hover), and the
   other links dim to keep attention on the one you're pointing at. */
.social-link{
  position: relative;
  display: inline-flex;
  align-items: center;
  padding: 6px 12px;
  transition: opacity .2s ease;
}

.contact-links:has(.social-link:hover) .social-link:not(:hover){
  opacity: .45;
}

.social-icon{
  position: absolute;
  left: 50%;
  bottom: 100%;
  width: 32px;
  height: 32px;
  color: var(--teal);
  transform: translate(-50%, 16px) rotate(var(--tilt, 0deg)) scale(0.6);
  opacity: 0;
  filter: blur(3px);
  pointer-events: none;
  transition: transform .25s var(--ramp-soft), opacity .25s ease, filter .25s ease;
}

.social-icon svg,
.social-icon img{ display: block; width: 100%; height: 100%; object-fit: contain; }

.social-label{
  position: relative;
  display: inline-block;
  transition: color .2s ease;
}

.social-link:hover .social-icon,
.social-link:focus-visible .social-icon{
  opacity: 1;
  filter: blur(0);
  transform: translate(-50%, -4px) rotate(var(--tilt, 0deg)) scale(1);
}

.social-link:hover .social-label,
.social-link:focus-visible .social-label{
  color: var(--text);
}

.social-link.is-bouncing .social-icon{
  animation: iconBounce .3s ease;
}

@keyframes iconBounce{
  0%, 100%{ transform: translate(-50%, -4px) rotate(var(--tilt, 0deg)) scale(1); }
  50%{ transform: translate(-50%, -4px) rotate(var(--tilt, 0deg)) scale(1.3); }
}

footer{ padding: 36px 0 60px; }
.foot-row{
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 12px;
  font-family: var(--mono);
  font-size: 12px;
  color: var(--muted);
}

/* ---------- Project (case study) pages ---------- */

.project-hero{ padding: 64px 0 48px; border-bottom: 1px solid var(--line); }
/* Row of pill actions at the top of a case study. */
.project-actions{
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin-bottom: 30px;
}

/* Primary CTA, sitting between the lede and the meta grid. */
.project-cta{
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  margin-top: 26px;
}
.project-hero h1{ font-size: clamp(36px, 6vw, 64px); max-width: 18ch; }
.project-lede{ font-size: 18px; color: var(--muted); max-width: 60ch; margin-top: 16px; }

/* The hairline dividers are the 1px gap letting --line show through from
   behind. That is why the panel has no overflow: hidden any more - a cell that
   tilts has to be allowed to leave the panel's box, and overflow would both
   clip it and force transform-style back to flat, killing the depth below.
   The rounded outer corners come off the corner cells instead, which draws the
   same shape without clipping anything. */
.meta-grid{
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  margin-top: 36px;
}

.meta-cell{
  position: relative;
  background: var(--surface);
  padding: 18px 20px;
  transform-style: preserve-3d;
  transition: box-shadow .25s ease, background .25s ease;
}

/* One pixel tighter than the panel, because it sits inside the panel's border. */
.meta-cell:first-child{
  border-top-left-radius: calc(var(--radius) - 1px);
  border-bottom-left-radius: calc(var(--radius) - 1px);
}
.meta-cell:last-child{
  border-top-right-radius: calc(var(--radius) - 1px);
  border-bottom-right-radius: calc(var(--radius) - 1px);
}

/* Has to lift above its neighbours while tilted, or a rotated corner slides
   underneath the cell beside it. */
.meta-cell:hover{
  z-index: 2;
  background: var(--surface-2);
  box-shadow: 0 22px 40px -20px rgba(0,0,0,0.9);
}

.meta-cell span{ font-family: var(--mono); font-size: 11px; color: var(--orange); display: block; margin-bottom: 6px; }
.meta-cell b{ font-size: 14.5px; font-weight: 500; display: block; }

/* Inert until setupTilt writes perspective() on pointermove. The label sits
   deeper than the value, so the two separate as the cell turns. */
.meta-cell span{ transform: translateZ(16px); }
.meta-cell b{ transform: translateZ(30px); }

@media (prefers-reduced-motion: reduce){
  .meta-cell{ transform-style: flat; }
  .meta-cell span, .meta-cell b{ transform: none; }
}

.project-body{ padding: 56px 0; border-bottom: 1px solid var(--line); }
.project-body h3{ font-size: 22px; margin-bottom: 14px; }
.project-body > .block{ max-width: 74ch; margin-bottom: 44px; }
.project-body > .block:last-child{ margin-bottom: 0; }
.project-body p{ color: var(--muted); font-size: 16px; }

.skills-list{ display: flex; flex-wrap: wrap; gap: 10px; margin-top: 8px; }
.skills-list span{
  font-family: var(--mono);
  font-size: 12px;
  color: var(--text);
  border: 1px solid var(--line);
  padding: 7px 13px;
  border-radius: 999px;
}

/* ---------- Silent clip player (no native controls, no download UI) ---------- */

/* Orientation-aware grid. A flat column count can't serve these projects:
   16:9 clips shrink to postage stamps at 3-up, while 9:16 clips become
   900px monsters at 2-up. So the cell width follows the clip's own shape -
   wide clips take a double cell (2 per row), tall clips a single (3 per row).
   JS tags .is-wide from the file's real dimensions, so dropping in any new
   clip lays out correctly with no markup change. */
/* Masonry packing. A plain grid makes every row as tall as its tallest clip,
   which leaves dead space under the short ones. Instead the rows are a fine
   4px unit and each clip spans however many units its own height needs (set
   from JS), so columns pack tightly with no vertical gaps. */
/* Masonry packing.

   Mixing widths in one grid can't be made gap-free: 3-up and 2-up items
   (span 2 and span 3 of six) leave a 1-column slot nothing can fill, and
   widening to span 4 blows squares up to 700px. So every clip is one column
   wide and the *column count* adapts to the page instead - JS sets
   --clip-cols to 2 when the page is mostly landscape, 3 otherwise.

   Height is handled by spanning a fine 4px row unit, so short clips never
   wait on the tallest item in their row. */
.clip-grid{
  display: grid;
  grid-template-columns: repeat(var(--clip-cols, 3), 1fr);
  grid-auto-rows: 4px;
  align-items: start;    /* clips keep their natural height - without this the
                            row span would feed back into the height */
  column-gap: 18px;
  row-gap: 0;            /* vertical spacing is baked into the row span */
  margin-top: 28px;
}

.clip{
  grid-column: span 1;
  position: relative;
  border-radius: var(--radius);
  overflow: hidden;
  border: 1px solid var(--line);
  background: #0d0b0a;
  line-height: 0;
}

/* A drop shadow alone cannot carry this. --bg is #12100e, so a black shadow
   has almost nothing left to darken and the gap between two stacked clips
   stays flat black - measured by eye at 6x and it was invisible. What reads as
   lift on a near-black page is a LIT EDGE: a hairline along the top, as though
   the card were catching light from above. The shadow stays because it does
   show against the lighter stills and it opens up properly on hover.

   Drawn as a pseudo-element rather than an inset shadow on .clip itself: an
   inset shadow paints under the element's own content, and these clips are
   full-bleed media, so it would never be seen. */
.clip::after{
  content: '';
  position: absolute;
  inset: 0;
  border-radius: inherit;
  pointer-events: none;   /* the pause toggle underneath stays clickable */
  box-shadow:
    inset 0 1px 0 rgba(244, 239, 230, 0.16),
    inset 0 0 0 1px rgba(244, 239, 230, 0.03);
  transition: box-shadow .25s ease;
}

.clip-grid .clip.is-in:hover::after{
  box-shadow:
    inset 0 1px 0 rgba(244, 239, 230, 0.28),
    inset 0 0 0 1px rgba(47, 191, 166, 0.16);
}

/* Two-class selector on purpose. .reveal declares a transition SHORTHAND later
   in this file, which drops every property it does not name and resets the
   delay - the trap already documented for .tl-row and .tool-row. That is why
   the entrance transitions are restated here, and .grade's filter transition
   with them: a .clip carries .grade and .reveal at once, so its filter
   transition was being silently clobbered and the grade was snapping. */
/* No cast shadow on the clips at all. The lit top edge on .clip::after and the
   border carry the separation; the coloured spill that used to sit here read as
   a glow around the artwork rather than as depth. */
.clip-grid .clip{
  transition:
    opacity .5s steps(5, end),
    transform .62s var(--ramp),
    border-color .25s ease,
    filter .1s linear;
}

/* .is-in required, or a clip still sitting 26px low from its entrance jumps
   when the pointer crosses it. Hover re-times transform: the .62s entrance
   curve is right for arriving and sluggish for a lift. */
/* Hover is now just the lift and the teal border - a touch device never gets
   this state, and nothing essential is hidden behind it. */
.clip-grid .clip.is-in:hover{
  transform: translateY(-4px);
  border-color: var(--teal);
  transition:
    transform .35s var(--ramp-soft),
    border-color .25s ease,
    filter .1s linear;
}

@media (prefers-reduced-motion: reduce){
  .clip-grid .clip.is-in:hover{ transform: none; }
}



/* height: auto means the browser uses the file's own dimensions -
   swap in any clip, portrait or landscape, and it lays out correctly.
   Stills (print work) sit in the same grid as video clips. */
.clip video,
.clip img{
  width: 100%;
  height: auto;
  display: block;
  pointer-events: none; /* removes any chance of a click surfacing a context/PiP affordance */
}

.clip .clip-tag{
  position: absolute;
  left: 10px; bottom: 10px;
  font-family: var(--mono);
  font-size: 10.5px;
  line-height: 1.4;
  color: var(--text);
  background: rgba(18,16,14,0.72);
  border: 1px solid var(--line);
  padding: 6px 10px;
  border-radius: 999px;
  letter-spacing: 0.03em;
}

.work-thumb video,
.work-thumb img{
  width: 100%;
  height: auto;
  display: block;
  pointer-events: none;
}

.clip-toggle{
  position: absolute;
  z-index: 3;
  right: 10px; bottom: 10px;
  width: 40px; height: 40px;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: rgba(18,16,14,0.72);
  cursor: pointer;
  padding: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 3px;
  transition: border-color .18s ease;
}

.clip-toggle:hover{ border-color: var(--teal); }

.clip-toggle::before{
  content: '';
  width: 0; height: 0;
  border-style: solid;
  border-width: 6px 0 6px 10px;
  border-color: transparent transparent transparent var(--text);
  margin-left: 2px;
}

.clip-toggle[data-state="playing"]::before,
.clip-toggle[data-state="playing"]::after{
  content: '';
  width: 3px; height: 12px;
  border-style: none;
  background: var(--text);
  margin-left: 0;
}

.work-thumb .thumb-scrim{
  position: absolute;
  inset: 0;
  z-index: 1;
  background: linear-gradient(180deg, rgba(18,16,14,0.35) 0%, rgba(18,16,14,0) 32%);
}

/* ==========================================================================
   SCROLL MOTION SYSTEM
   Three effects, each borrowed from an actual editing-room technique:
   1. Log → graded    - footage grades itself as it enters frame
   2. Frame stutter   - text lands in discrete steps, not a smooth fade
   3. J-cut stagger   - one layer leads the other in, like offset audio
   ========================================================================== */

/* Speed-ramp easing: slow out, fast through the middle, slow in.
   Mirrors the velocity curve of a ramped clip. */
:root{
  --ramp: cubic-bezier(0.72, 0, 0.16, 1);
  --ramp-soft: cubic-bezier(0.4, 0, 0.2, 1);
}

/* ---------- 2. Log → graded ---------- */
/* Starts flat and desaturated like ungraded log footage, then grades
   to full contrast and colour as it moves into frame. */

.grade{
  filter: saturate(0.12) contrast(0.78) brightness(1.12);
  transition: filter .1s linear;
}

.grade.is-graded{
  filter: saturate(1) contrast(1) brightness(1);
}

/* ---------- 3. Frame-stutter reveal ---------- */
/* steps() instead of a smooth curve: the element arrives in discrete
   jumps, the way footage looks when you scrub it at low frame rate. */

.reveal{
  opacity: 0;
  transform: translateY(26px);
  transition:
    opacity .5s steps(5, end),
    transform .62s var(--ramp);
}

.reveal.is-in{
  opacity: 1;
  transform: translateY(0);
}

/* ---------- 4. J-cut stagger ---------- */
/* In a J-cut the audio leads the picture. Here the visual layer leads
   the text by a beat, so sections don't arrive as one flat block. */

.jcut-lead{ transition-delay: 0s; }
.jcut-trail{ transition-delay: .16s; }
.jcut-trail-2{ transition-delay: .28s; }

/* Section-boundary flash frame - the single bright frame you sometimes
   catch on a hard cut. */
.cut-flash{
  position: relative;
}

.cut-flash::after{
  content: '';
  position: absolute;
  inset: 0;
  background: var(--text);
  opacity: 0;
  pointer-events: none;
  z-index: 5;
}

.cut-flash.is-cutting::after{
  animation: flashframe .26s steps(3, end);
}

@keyframes flashframe{
  0%   { opacity: 0.16; }
  100% { opacity: 0; }
}

/* Stat counters tick up like a timecode readout */
.stat b{ font-variant-numeric: tabular-nums; }

@media (prefers-reduced-motion: reduce){
  .reveal{ opacity: 1; transform: none; }
  .grade{ filter: none; }
  .cut-flash.is-cutting::after{ animation: none; }
}

/* ---------- Responsive ---------- */

@media (max-width: 1000px){
  /* Tablet: two columns regardless of orientation mix. */
  .clip-grid{ --clip-cols: 2; }
}

@media (max-width: 860px){
  .hero-grid{ grid-template-columns: 1fr; }
  /* stat-grid is auto-fit minmax now - it reflows on its own, and forcing
     2 columns here would squeeze the cards below their readable width. */
  .do-grid{ grid-template-columns: 1fr; }
  .tool-grid{ grid-template-columns: 1fr; }
  .work-grid{ columns: 1; }
  .clip-grid{ --clip-cols: 1; }
  /* Two columns means the four cells are a 2x2, so each one owns exactly one
     outer corner. The first/last-child rules above are for the 4-across row
     and round the wrong edges here.

     Both selectors below are deliberately two-class: a bare .meta-cell reset
     is only 0,1,0 and loses to .meta-cell:first-child at 0,2,0 no matter how
     late it appears, which left cell 1 still carrying its bottom-left radius
     into the 2x2. */
  .meta-grid{ grid-template-columns: repeat(2, 1fr); }
  .meta-grid .meta-cell{ border-radius: 0; }
  .meta-grid .meta-cell:nth-child(1){ border-top-left-radius: calc(var(--radius) - 1px); }
  .meta-grid .meta-cell:nth-child(2){ border-top-right-radius: calc(var(--radius) - 1px); }
  .meta-grid .meta-cell:nth-child(3){ border-bottom-left-radius: calc(var(--radius) - 1px); }
  .meta-grid .meta-cell:nth-child(4){ border-bottom-right-radius: calc(var(--radius) - 1px); }
  .tl-row{ grid-template-columns: 1fr; gap: 8px; }
  .scrubber-seg .seg-full{ display: none; }
  .scrubber-seg .seg-short{ display: block; font-size: 9px; left: 6px; bottom: 5px; }
  .scrubber-code{ width: 84px; font-size: 10px; }
  .nav-links{ gap: 16px; font-size: 13px; }
}

/* ---------- More work carousel ----------
   Vanilla port of the circular carousel: cards sit on an arc, the active one
   centred and front-most, neighbours scaled and faded back. Positions are
   written from JS so the radius can follow the container width. */

.morework{
  position: relative;
  margin-top: 34px;
  outline: none;
}

.mw-stage{
  position: relative;
  height: 380px;
  max-width: 980px;
  margin: 0 auto;
}

.mw-card{
  position: absolute;
  left: 50%;
  top: 50%;
  width: 300px;
  min-height: 186px;
  margin: -93px 0 0 -150px;   /* centre on the stage before JS offsets it */
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 12px;
  padding: 18px 20px;
  border: 1px solid var(--line);
  border-radius: 16px;
  background: var(--surface);
  box-shadow: 0 18px 40px -18px rgba(0,0,0,0.8);
  transition: transform .65s var(--ramp-soft), opacity .65s var(--ramp-soft),
              border-color .25s ease;
  will-change: transform, opacity;
}

.mw-card:hover{ border-color: var(--teal); }
.mw-card.is-active{ border-color: var(--orange-dim); }

.mw-tag{
  font-family: var(--mono);
  font-size: 10.5px;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--orange);
  line-height: 1.4;
}

.mw-title{
  display: block;
  font-family: var(--display);
  font-size: 20px;
  font-weight: 700;
  line-height: 1.15;
  color: var(--text);
}

.mw-desc{
  display: block;
  margin-top: 7px;
  font-size: 13px;
  line-height: 1.45;
  color: var(--muted);
  /* keep every card the same height regardless of copy length */
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  overflow: hidden;
}

/* Position readout, sitting under the arc. */
.mw-counter{
  position: absolute;
  left: 0; right: 0;
  bottom: 6px;
  text-align: center;
  pointer-events: none;
}

.mw-counter b{
  display: block;
  font-family: var(--display);
  font-size: 40px;
  line-height: 1;
  color: rgba(244,239,230,0.9);
  font-variant-numeric: tabular-nums;
}

.mw-counter span{
  font-family: var(--mono);
  font-size: 11px;
  color: var(--muted);
}

.mw-controls{
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 16px;
  margin-top: 18px;
}

.mw-arrow{
  width: 44px; height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  border: 1px solid var(--line);
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  transition: color .2s ease, border-color .2s ease;
}

.mw-arrow:hover{ color: var(--text); border-color: var(--teal); }
.mw-arrow svg{ width: 18px; height: 18px; }

.mw-dots{ display: flex; align-items: center; gap: 6px; }

.mw-dot{
  height: 6px; width: 6px;
  padding: 0;
  border: none;
  border-radius: 999px;
  background: rgba(244,239,230,0.22);
  cursor: pointer;
  transition: width .3s ease, background .3s ease;
}

.mw-dot:hover{ background: rgba(244,239,230,0.45); }
.mw-dot.is-active{ width: 24px; background: var(--orange); }

/* Between the wrap's full width and mobile the stage shrinks with its parent,
   but the card is a fixed width, so step it down before the arc runs out of
   room for the outermost pair. */
@media (max-width: 860px){
  .mw-stage{ height: 340px; }
  .mw-card{ width: 244px; min-height: 168px; margin: -84px 0 0 -122px; }
  .mw-title{ font-size: 18px; }
}

@media (max-width: 700px){
  .mw-stage{ height: 310px; }
  .mw-card{ width: 204px; min-height: 154px; margin: -77px 0 0 -102px; padding: 15px 16px; }
  .mw-title{ font-size: 16px; }
  .mw-desc{ font-size: 12px; }
}

@media (prefers-reduced-motion: reduce){
  .mw-card{ transition: none; }
}
