/* Dominic Francis - portfolio. Palette 019 + Plus Jakarta Sans. Dark base theme. */
:root{
  --hunter:#355418; --forest:#7ec13a;
  --rasp:#ff3d7f; --mustard:#efc403;

  --bg:#121208; --surface:#1b1b10; --raised:#242417;
  --fg:#f2f1e8; --fg-muted:#b7b5a4; --fg-faint:#8a8875;
  --line:rgba(242,241,232,.14);
  --ink:#0c0c07;
  /* Fill for the glass panels that sit over the hero photo and the // lamp. The
     .55 alpha is a contrast floor, not a taste call: behind it the photo peaks at
     rgb(176,197,184) once the 18px backdrop blur has averaged it, and the lamp at
     rgb(177,190,43). At .28 the placeholder text and field borders inside the
     panel fell to ~2.5:1 and ~1.7:1 against those peaks. At .55 every layer above
     clears AA. Don't lower it without re-checking both surfaces. */
  --glass:rgba(12,13,8,.55);
  /* Shared height for the two full-bleed lamp bands (What I offer + the CTA) so
     they read as a matched pair. Set above the taller one's natural desktop
     height (the 4-up card grid, ~405px); both bands centre their content within
     it. Below the 4-up breakpoint the card grid wraps and grows past this, which
     is fine - the bands are scrolled to separately there. */
  --band-min-h:340px;
  /* Green hairline that frames every lamp band (top + bottom). Forest at .5 reads
     as a soft green line whether it sits over the bright part of the lamp or over
     the dark page below, so the bands frame consistently regardless of how far the
     lamp has faded at that edge. */
  --lamp-edge:rgba(126,193,58,.15);

  --maxw:1160px;
  --font:'Plus Jakarta Sans',system-ui,-apple-system,Segoe UI,Roboto,sans-serif;
}
*{box-sizing:border-box}
/* The hidden attribute only sets a default display:none, which any author
   display rule outranks. This keeps it absolute. */
[hidden]{display:none!important}
html{-webkit-text-size-adjust:100%}
body{
  margin:0;font-family:var(--font);color:var(--fg);background:var(--bg);
  font-size:17px;line-height:1.65;-webkit-font-smoothing:antialiased;
}
h1,h2,h3{font-weight:700;letter-spacing:-.02em;line-height:1.1;margin:0}
p{margin:0}
img{max-width:100%;display:block}
a{color:var(--rasp);text-underline-offset:3px;text-decoration-color:rgba(255,61,127,.45)}
a:hover{color:var(--rasp)}
.container{max-width:var(--maxw);margin:0 auto;padding:0 32px}
.slashes{color:var(--forest);letter-spacing:-.22em}

/* Accessibility */
:focus-visible{outline:3px solid var(--mustard);outline-offset:2px;border-radius:2px}
.skip-link{position:absolute;left:-9999px;top:8px;z-index:200;background:var(--fg);color:var(--ink);
  padding:10px 16px;border-radius:8px;font-weight:600;text-decoration:none}
.skip-link:focus{left:16px}
.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);border:0}

/* Buttons - one system.
   Every button is a bordered box that INVERTS on hover: the border colour becomes
   the fill and the label flips to the dark ink. Nothing moves, so a page full of
   buttons doesn't twitch under the cursor.

   ONLY TWO looks, and both use the same cream (--fg) - on the page, on a photo,
   on the lamp, everywhere:
     .btn-primary   solid cream at rest. The page's single most important action.
     .btn-ghost     cream outline, fills cream on hover. Everything else.
   .btn-ghost-light is kept as an alias of .btn-ghost (it only adds a backdrop
   blur for sitting over a photo) so existing markup keeps working.
   .more is the card affordance and is identical to .btn-ghost - see the .more
   rule for why it must stay a <span>. */
.btn,.more{display:inline-flex;align-items:center;justify-content:center;gap:8px;
  border-radius:3px;font-weight:700;font-size:16px;padding:13px 24px;cursor:pointer;
  text-decoration:none;border:1.5px solid transparent;
  transition:background .18s,color .18s,border-color .18s}

.btn-primary{background:var(--fg);color:var(--ink);border-color:var(--fg)}
.btn-primary:hover{background:#fff;border-color:#fff;color:var(--ink)}

.btn-ghost,.btn-ghost-light{background:transparent;color:var(--fg);border-color:var(--fg)}
.btn-ghost:hover,.btn-ghost-light:hover{background:var(--fg);color:var(--ink);border-color:var(--fg)}
/* The blur is the only thing ghost-light adds: it sits over the hero photo. */
.btn-ghost-light{-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}

/* Header - subtle liquid glass */
.site-header{position:sticky;top:0;z-index:60;
  background:rgba(18,18,8,.68);-webkit-backdrop-filter:blur(22px) saturate(1.6);
  backdrop-filter:blur(22px) saturate(1.6);
  border-bottom:1px solid var(--line);box-shadow:0 1px 0 rgba(0,0,0,.2)}
/* Three columns, so the nav's right edge is fixed by the grid rather than by
   what precedes it: the contact page ships no CTA, and the empty middle column
   keeps the nav from sliding across into the gap.
   The middle column is the leftover space between wordmark and nav (not a page-
   centred track), so the CTA centres in that gap rather than on the page - the
   wordmark and nav are different widths, so the two are not the same point. */
.header-inner{display:grid;grid-template-columns:auto 1fr auto;align-items:center;
  gap:30px;height:66px}
/* Colour comes from .btn-ghost-light; only the size is header-specific, kept
   smaller than an in-page CTA so it reads as chrome on every page. */
.header-cta{justify-self:start;padding:8px 16px;font-size:14.5px}
/* One size for both header and footer: .wordmark-light only recolours. */
.wordmark{display:inline-flex;align-items:baseline;gap:7px;text-decoration:none;
  color:var(--fg);font-weight:700;font-size:24px;letter-spacing:-.01em}
.wordmark:hover{color:var(--fg)}
.primary-nav{display:flex;gap:24px;align-items:center;justify-self:end;grid-column:3}
.primary-nav a{text-decoration:none;font-weight:500;font-size:15.5px;color:var(--fg-muted);
  padding:4px 1px;border-bottom:2.5px solid transparent}
.primary-nav a:hover{border-bottom-color:var(--mustard);color:var(--fg-muted)}
.primary-nav a.current{color:var(--fg);border-bottom-color:var(--mustard)}
.nav-toggle{display:none;background:none;border:1.5px solid var(--fg);border-radius:8px;
  font-size:18px;line-height:1;padding:8px 12px;cursor:pointer;color:var(--fg)}
.mobile-menu{display:none}

@media (max-width:880px){
  /* Wordmark + toggle only: back to two columns, or the hidden middle/right
     columns would leave the toggle stranded mid-row. */
  .header-inner{grid-template-columns:1fr auto}
  .primary-nav,.header-cta{display:none}
  .nav-toggle{display:inline-block;justify-self:end}
  .mobile-menu[hidden]{display:none}
  /* height, not bottom:0: the header sets backdrop-filter, which per spec makes it
     the containing block for this fixed child. bottom:0 then resolves against the
     67px header - collapsing the panel to a sliver so its fill never covers the
     page and the links spill over the hero. A viewport-height keeps the panel
     full-height regardless. top:66px already pins it below the sticky header, so
     the height is the viewport minus that. overflow-y:auto for short landscape. */
  .mobile-menu{display:flex;position:fixed;top:66px;left:0;right:0;z-index:55;
    height:calc(100vh - 66px);height:calc(100dvh - 66px);overflow-y:auto;
    flex-direction:column;gap:6px;padding:28px 32px;
    background:rgba(14,14,6,.94);-webkit-backdrop-filter:blur(26px) saturate(1.5);
    backdrop-filter:blur(26px) saturate(1.5)}
  .mobile-menu a{text-decoration:none;font-weight:600;font-size:26px;color:var(--fg-muted);
    padding:12px 0;border-bottom:1px solid var(--line)}
  .mobile-menu a.current{color:var(--fg)}
  .mobile-menu .btn{align-self:flex-start;margin-top:20px;font-size:17px;padding:15px 22px}
  /* .mobile-menu a beats .btn-primary on specificity, so the CTA inherited the
     muted link colour and rendered grey-on-cream (1.8:1). Restore the button's
     own foreground. */
  .mobile-menu a.btn-primary{color:var(--ink)}
  .mobile-menu .m-github{font-size:16px;border:none;margin-top:14px}
}

/* Layout helpers */
main{display:block}
/* padding-top, not the shorthand: an element is often both .section and
   .container, and the shorthand would reset .container's 32px side gutter to 0. */
.section{padding-top:72px}
.section-canvas{background:var(--surface);border-top:1px solid var(--line);
  border-bottom:1px solid var(--line);margin-top:72px}
.section-canvas .inner{padding:72px 0}
.lede{font-size:18px;line-height:1.6;max-width:58ch;margin-top:20px;color:var(--fg-muted)}
h1.page-title{font-size:clamp(40px,6vw,72px);line-height:.98;letter-spacing:-.03em}
h2.section-title{font-size:clamp(28px,3.4vw,40px);line-height:1.05;letter-spacing:-.02em;margin-bottom:32px}

/* Inner-page hero: full-bleed // lamp behind the page title, same component as
   the CTA band. The lamp element is the full-width band; the .container inside
   re-centres the text, so the copy still lines up with the rest of the page.
   background is the no-WebGL fallback the canvas paints over. */
.page-hero{position:relative;overflow:hidden;background:var(--hunter);
  border-top:1px solid var(--lamp-edge);border-bottom:1px solid var(--lamp-edge);
  padding:78px 0 84px}
.page-hero canvas{display:block}
/* Scrim: the lamp's bright stripes sweep under the copy, and body text needs a
   guaranteed contrast floor rather than one that depends on the animation frame. */
.page-hero::after{content:"";position:absolute;inset:0;pointer-events:none;
  background:linear-gradient(100deg,rgba(6,7,3,.72) 0%,rgba(6,7,3,.45) 55%,rgba(6,7,3,.2) 100%)}
/* The canvas and scrim are absolutely positioned, so the content needs a
   stacking context of its own to sit in front of them. */
.page-hero .container{position:relative;z-index:1}
.page-hero .page-title{color:#fff}
.page-hero .lede{color:rgba(255,255,255,.88);max-width:60ch}
@media (max-width:640px){.page-hero{padding:52px 0 58px}}

/* About's hero is just the page title now; the intro text + portrait live in
   their own section beneath it (see .about-intro-section). */

/* Contact's lamp band runs the full page: the form lives inside it rather than on
   the surface below, so the card becomes the same glass panel as the home hero's
   and its fields go light-on-dark. */
.contact-hero{padding-bottom:88px}
.contact-hero .contact-grid{margin-top:44px}
.contact-hero .contact-form{border-color:rgba(255,255,255,.14);border-radius:6px;
  background:var(--glass);-webkit-backdrop-filter:blur(18px) saturate(1.4);
  backdrop-filter:blur(18px) saturate(1.4);
  box-shadow:0 20px 50px -30px rgba(0,0,0,.6),inset 0 1px 0 rgba(255,255,255,.06)}
.contact-hero .field label{color:rgba(255,255,255,.92)}
.contact-hero .field input,.contact-hero .field textarea{color:#fff;
  background:rgba(255,255,255,.08);border-color:rgba(255,255,255,.45)}
.contact-hero .field input::placeholder,
.contact-hero .field textarea::placeholder{color:rgba(255,255,255,.72)}
.contact-hero .field input:focus,.contact-hero .field textarea:focus{
  background:rgba(255,255,255,.13);border-color:rgba(255,255,255,.7)}
.contact-hero .no-email{color:rgba(255,255,255,.78)}
.contact-hero .contact-success{border-left-color:#fff}
.contact-hero .contact-success h2{color:#fff}
.contact-hero .contact-success p{color:rgba(255,255,255,.8)}
.contact-hero .contact-success a{color:#fff}
@media (max-width:640px){.contact-hero{padding-bottom:64px}}

/* 404's hero is centred rather than left-aligned. */
.page-hero.notfound-hero .container{display:flex;flex-direction:column;
  align-items:center;text-align:center;gap:18px}
.page-hero .notfound-big{font-weight:800;font-size:clamp(80px,12vw,140px);line-height:1;
  letter-spacing:-.04em;color:#fff}
.page-hero .notfound-big .g{color:var(--forest)}
.page-hero .notfound-big .m{box-shadow:inset 0 -.14em 0 var(--mustard)}
.page-hero .notfound-actions{display:flex;gap:14px;margin-top:12px;flex-wrap:wrap;justify-content:center}

/* Hero - full-bleed photo with parallax + glass panel, editorial split content on top */
/* flex-end: the content (title, copy, panel) sits at the bottom of the tall hero,
   with the extra height opening up as photo above it. */
.hero{position:relative;overflow:hidden;border-bottom:1px solid var(--line);
  min-height:81vh;display:flex;align-items:flex-end}
.hero-media{position:absolute;inset:0;overflow:hidden;transform-origin:60% 40%;
  animation:hero-kenburns 22s ease-out both;will-change:transform}
@keyframes hero-kenburns{from{transform:scale(1)}to{transform:scale(1.12)}}
@media (prefers-reduced-motion:reduce){.hero-media{animation:none}}
.hero-bg{position:absolute;left:0;top:-8%;width:100%;height:116%;object-fit:cover;object-position:center 30%;
  will-change:transform}
.hero-scrim{position:absolute;inset:0;
  background:linear-gradient(100deg,rgba(6,7,3,.82) 0%,rgba(6,7,3,.58) 52%,rgba(6,7,3,.28) 100%)}
/* padding-block, not the padding shorthand: .hero-inner is also a .container, and
   a shorthand with 0 sides wipes out the container's 32px gutter - which is what
   pushed the hero flush to the viewport edge on mobile. */
/* Equal padding top and bottom: the panel is the tallest thing in the row, so it
   sets the row height and this is what makes its outer spacing symmetric. */
.hero-inner{position:relative;padding-block:38px;display:grid;grid-template-columns:7fr 5fr;gap:56px;align-items:end;width:100%}
@media (max-width:880px){.hero-inner{grid-template-columns:1fr;gap:32px;padding-block:36px}}
.hero h1{font-size:clamp(36px,5vw,66px);line-height:1;letter-spacing:-.03em;max-width:16ch;color:#fff}
.hero .mark{color:var(--rasp)}
.hero-aside{padding:22px 24px;border-radius:6px;border:1px solid rgba(255,255,255,.14);
  background:var(--glass);-webkit-backdrop-filter:blur(18px) saturate(1.4);
  backdrop-filter:blur(18px) saturate(1.4);
  box-shadow:0 20px 50px -30px rgba(0,0,0,.6),inset 0 1px 0 rgba(255,255,255,.06)}
.hero p{font-size:18px;line-height:1.55;max-width:40ch;color:rgba(255,255,255,.88)}
/* The supporting copy now runs under the h1 in the wide column rather than inside
   the panel. Sized to the header's nav links (15.5px) so the two agree. */
.hero-lead p{font-size:15.5px;line-height:1.6;max-width:52ch;margin-top:20px}
.hero .actions{display:flex;gap:10px;margin-top:24px;flex-wrap:wrap}
.inline-brand{white-space:nowrap}
.inline-logo{display:inline-block;vertical-align:-4px;margin-right:4px;border-radius:3px}
.inline-link{color:#fff;font-weight:600;text-decoration:none}
.inline-link:hover{color:#fff}
@media (max-width:640px){.hero{min-height:auto}}

/* Art box with animated // lamp */
.art{position:relative;overflow:hidden;background:linear-gradient(150deg,var(--hunter),var(--ink))}
.art canvas{display:block}
.art .badge{position:absolute;left:14px;top:14px;background:var(--mustard);color:var(--ink);
  font-weight:600;font-size:12px;padding:4px 10px;border-radius:6px;z-index:2}
.art .shot{position:absolute;left:14px;bottom:12px;z-index:2;color:rgba(255,255,255,.92);
  font-size:12.5px;background:rgba(255,255,255,.14);-webkit-backdrop-filter:blur(10px);
  backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,.3);border-radius:8px;padding:3px 10px}

/* Project screenshots inside an .art frame.
   .card-img sits above the lamp canvas, so a card with no image falls back to it. */
.feature-img,.detail-hero-img{display:block;width:100%;height:100%;object-fit:cover}
.card-img{position:absolute;inset:0;z-index:1;width:100%;height:100%;object-fit:cover}

/* Portfolio preview - row list, not tiles */
.cards{display:flex;flex-direction:column;border-top:1px solid var(--line)}
/* Negative margin + equal padding bleeds the hover fill past the content column,
   so it softens outward instead of stopping on a hard vertical edge. */
.card{text-decoration:none;color:var(--fg);display:grid;
  grid-template-columns:160px 1fr auto;gap:22px;align-items:center;
  margin:0 -20px;padding:20px;border-radius:8px;
  border-bottom:1px solid var(--line);transition:background .15s}
.card:hover{color:var(--fg);background:var(--surface)}
.card .art{width:160px;height:100px;border-radius:4px;flex:none}
.card .body{display:flex;flex-direction:column;gap:5px;min-width:0}
.card h3{font-size:21px;font-weight:700;letter-spacing:-.015em}
.card .status{font-size:12.5px;font-weight:600;color:var(--forest)}
.card p{font-size:15px;line-height:1.5;color:var(--fg-muted);max-width:62ch}
/* Styled exactly as .btn-ghost, but still a <span>: it sits inside the card's
   wrapping <a>, and a nested <a>/<button> there would be invalid HTML. The whole
   card is the hit area, so this is an affordance, not a separate control - hence
   aria-hidden in the markup, and the invert keyed off the CARD's hover, not the
   span's, so it responds wherever in the card the cursor is. */
.more{font-size:14px;padding:9px 18px;white-space:nowrap;
  background:transparent;color:var(--fg);border-color:var(--fg)}
.card:hover .more,.feature:hover .more,.svc-snap:hover .more{
  background:var(--fg);color:var(--ink);border-color:var(--fg)}
@media (max-width:640px){
  .card{grid-template-columns:104px 1fr;gap:16px}
  .card .art{width:104px;height:65px}
  .card .more{display:none}
}
.creds{display:flex;gap:28px;flex-wrap:wrap;margin-top:40px;padding-top:28px;border-top:1px solid var(--line)}
.cred{font-size:14px;color:var(--fg-muted)}

/* Service snapshot list (home) */
/* Card grid, not stacked rows: four abreast, wrapping to 2 then 1 as space runs
   out. auto-fit + minmax means the count follows the width instead of needing a
   breakpoint per step. */
.svc-snaps{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(230px,100%),1fr));
  gap:20px}
/* The lamp canvas is the card's background, so no background colour here - and
   overflow:hidden clips the canvas to the rounded corners. */
/* position:relative is also set by .apw-lamp's injected CSS, but declare it here
   too: the ::after scrim must not depend on another stylesheet loading. */
.svc-snap{text-decoration:none;color:var(--fg);display:block;position:relative;
  border:1px solid var(--line);border-radius:8px;overflow:hidden;background:transparent;
  transition:border-color .15s,transform .15s}
/* a.svc-snap only: the "how projects run" cards on services are plain <div>s with
   nowhere to go, so they must not lift or brighten as though they were clickable. */
a.svc-snap:hover{color:var(--fg);border-color:var(--fg-faint);transform:translateY(-2px)}
/* Scrim between the lamp and the copy. The lamp sweeps bright mustard/lime
   streaks across the card, so white text on the bare canvas swings from ~14:1 to
   failing WCAG AA as the animation moves - and the reader can't scroll away from
   it. A flat dark veil fixes the text's background, so contrast is the same
   wherever the streaks happen to fall. Same trick as .hero-scrim. */
.svc-snap::after{content:"";position:absolute;inset:0;pointer-events:none;
  background:rgba(8,9,5,.42)}
/* Content sits above the canvas AND the scrim. Column + margin-top:auto on .more
   keeps the link at the foot of every card whatever the copy length. */
.svc-snap-body{position:relative;z-index:1;display:flex;flex-direction:column;gap:8px;
  padding:24px 22px;height:100%}
.svc-snap .g{display:none}
.svc-snap h3{font-size:18.5px;font-weight:700;color:#fff}
/* Whites, not the page's fg tokens: this text sits on the lamp, not on --bg.
   Near-solid rather than 82%, so the scrim's headroom isn't spent on the alpha. */
.svc-snap p{font-size:15px;line-height:1.55;color:rgba(255,255,255,.92)}
/* Same cream as every other button - only the position is card-specific. */
.svc-snap .more{margin-top:auto;align-self:flex-end}
/* No WebGL: the script sets this, and the canvas is hidden - so restore a fill. */
.svc-snap.apw-lamp--fallback{background:var(--surface)}

/* "What I offer" (home): a full-bleed lamp band that runs edge to edge and holds
   the section title, butting straight against the hero above with no gap. Same
   band component as .page-hero - lamp canvas + scrim + a centred .container - with
   the glass cards laid over it. --hunter is the no-WebGL fallback the canvas
   paints over. */
.svc-band{position:relative;overflow:hidden;background:var(--hunter);
  border-top:1px solid var(--lamp-edge);border-bottom:1px solid var(--lamp-edge);
  padding:30px 0 36px;margin-top:90px;
  min-height:var(--band-min-h);display:flex;flex-direction:column;justify-content:center}
/* width:100%, or the .container's own `margin:0 auto` makes it shrink-to-fit as a
   flex item - collapsing the card grid to two columns and blowing the height up. */
.svc-band > .container{width:100%}
.svc-band canvas{display:block}
/* Scrim, strongest at the top so the white title clears AA over the bright
   streaks, easing lower so the cards keep more of the lamp's glow through them. */
.svc-band::after{content:"";position:absolute;inset:0;pointer-events:none;
  background:linear-gradient(180deg,rgba(8,9,5,.55) 0%,rgba(8,9,5,.34) 55%,rgba(8,9,5,.3) 100%)}
.svc-band .container{position:relative;z-index:1}
.svc-band .section-title{color:#fff;margin-bottom:30px}
.svc-band .svc-snaps{gap:18px}
/* Glassmorphism cards: a translucent dark fill plus a backdrop blur, so the lamp
   behind reads as diffused colour through frosted glass. The fill doubles as the
   contrast floor - white text clears AA over the lamp's brightest streak - so the
   per-card scrim and transparent background from the base .svc-snap are dropped.
   A lit top edge (inset highlight) and soft drop shadow give the pane depth. */
.svc-band .svc-snap{border:1px solid rgba(255,255,255,.14);border-radius:12px;
  background:rgba(10,11,6,.5);-webkit-backdrop-filter:blur(16px) saturate(1.3);
  backdrop-filter:blur(16px) saturate(1.3);
  box-shadow:0 14px 40px -24px rgba(0,0,0,.7),inset 0 1px 0 rgba(255,255,255,.10)}
/* The base card carries a flat ::after veil for its own lamp; the glass fill
   replaces it here, so switch it off or the cards double-darken. */
.svc-band .svc-snap::after{display:none}
/* Diagonal sheen - the giveaway of the "2.0" look. Sits above the glass fill but
   below the copy (which is z-index:1), and never intercepts the click. */
.svc-band .svc-snap::before{content:"";position:absolute;inset:0;border-radius:inherit;
  pointer-events:none;background:linear-gradient(150deg,rgba(255,255,255,.12),rgba(255,255,255,0) 46%)}
.svc-band a.svc-snap:hover{border-color:rgba(255,255,255,.34);
  background:rgba(15,17,10,.56);transform:translateY(-3px)}
@media (max-width:640px){.svc-band{padding:44px 0 52px}}

/* Services page - list, not card grid */
.svc-grid{display:flex;flex-direction:column;border-top:1px solid var(--line)}
.svc-card{border-bottom:1px solid var(--line);padding:28px 0;display:grid;
  grid-template-columns:1fr 2fr;gap:28px;scroll-margin-top:90px}
.svc-card .g{display:none}
.svc-card h2{font-size:23px;font-weight:700;letter-spacing:-.015em}
.svc-card p{font-size:16px;line-height:1.6;color:var(--fg-muted)}
/* The CTA is a third grid child, so it would otherwise land in column 1 under the
   heading. Pin it to column 2 (beneath the copy) and push it to the right edge.
   align-self:start stops the implicit row stretching the button away from the
   copy, and row-gap:0 closes the gap the grid's 28px gutter would leave. */
.svc-card{row-gap:0}
.svc-card-cta{grid-column:2;justify-self:end;align-self:start;margin-top:16px}
@media (max-width:680px){
  .svc-card{grid-template-columns:1fr;gap:8px}
  /* Single column now, so the explicit column-2 placement would overflow. */
  .svc-card-cta{grid-column:1}
}


/* About */
.about-intro{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(300px,100%),1fr));gap:44px;align-items:center}
.about-lead{font-weight:500;font-size:22px;line-height:1.5;color:#fff}
.about-body{font-size:17.5px;line-height:1.68;color:var(--fg)}
.about-intro .about-body{margin-top:18px}
/* Breathing room above and below the intro row (the .section base only sets a
   top padding), so the smaller, panelled portrait isn't crowded. */
.about-intro-section{padding-top:64px;padding-bottom:64px}
/* Portrait encased in a glass panel: a frosted pane with a lit top edge and soft
   shadow, matching the cards elsewhere. Smaller than before so it reads as a
   framed photo, not a hero image. */
.about-intro-section .portrait{justify-self:center;max-width:320px;padding:14px;border-radius:16px;
  border:1px solid rgba(255,255,255,.14);background:rgba(10,11,6,.5);
  -webkit-backdrop-filter:blur(16px) saturate(1.3);backdrop-filter:blur(16px) saturate(1.3);
  box-shadow:0 18px 46px -26px rgba(0,0,0,.75),inset 0 1px 0 rgba(255,255,255,.10);
  position:relative}
/* Diagonal sheen for the "2.0" glass look, above the fill but below the photo. */
.about-intro-section .portrait::before{content:"";position:absolute;inset:0;border-radius:inherit;
  pointer-events:none;z-index:1;
  background:linear-gradient(150deg,rgba(255,255,255,.12),rgba(255,255,255,0) 46%)}
.about-intro-section .portrait-frame{border-radius:8px;position:relative;z-index:0}
.portrait{max-width:380px;justify-self:end;width:100%}
.portrait-frame{aspect-ratio:4/5;border:1px solid var(--line);border-radius:4px;overflow:hidden;
  background:linear-gradient(150deg,var(--hunter),var(--ink));position:relative}
.portrait-frame img{width:100%;height:100%;object-fit:cover;transform-origin:58% 42%;
  animation:portrait-kenburns 20s ease-out both;will-change:transform}
@keyframes portrait-kenburns{from{transform:scale(1)}to{transform:scale(1.12)}}
@media (prefers-reduced-motion:reduce){.portrait-frame img{animation:none}}
.portrait .cap{margin-top:8px;font-size:13.5px;color:var(--fg-faint)}
.themes{display:flex;flex-direction:column;border-top:1px solid var(--line)}
.theme{border-bottom:1px solid var(--line);padding:26px 0;display:grid;
  grid-template-columns:1fr 2fr;gap:28px}
.theme .g{display:none}
.theme h2{font-size:21px;font-weight:700;letter-spacing:-.015em}
.theme p{font-size:16px;line-height:1.6;color:var(--fg-muted)}
@media (max-width:680px){.theme{grid-template-columns:1fr;gap:8px}}
.credential{margin:0;padding:22px 28px;
  display:flex;gap:16px;align-items:center;flex-wrap:wrap;background:var(--surface)}
.credential .tag{background:var(--mustard);color:var(--ink);font-weight:600;font-size:14px;
  padding:6px 12px;border-radius:3px;display:inline-block;transform-origin:center}
.credential p{font-size:16px;flex:1;min-width:240px;color:var(--fg)}
/* Button in a flex row with the tag and copy, so keep it compact and unshrunk. */
.credential-cta{font-size:14px;padding:9px 18px;white-space:nowrap;flex:none}

/* Distinction band: the same full-bleed lamp band as "What I offer" (canvas +
   scrim + centred container), with the credential laid over it as a glass panel
   to match the home page's cards. */
.cred-band{position:relative;overflow:hidden;background:var(--hunter);
  border-top:1px solid var(--lamp-edge);border-bottom:1px solid var(--lamp-edge);padding:52px 0}
.cred-band canvas{display:block}
.cred-band::after{content:"";position:absolute;inset:0;pointer-events:none;
  background:linear-gradient(180deg,rgba(8,9,5,.55) 0%,rgba(8,9,5,.34) 55%,rgba(8,9,5,.3) 100%)}
.cred-band .container{position:relative;z-index:1}
/* Glass panel, matching .svc-band .svc-snap: translucent dark fill + backdrop
   blur so the lamp reads as diffused colour through frosted glass, a lit top
   edge and soft shadow for depth. The forest left-border is dropped for a full
   light hairline. */
.cred-band .credential{border:1px solid rgba(255,255,255,.14);border-radius:12px;
  background:rgba(10,11,6,.5);-webkit-backdrop-filter:blur(16px) saturate(1.3);
  backdrop-filter:blur(16px) saturate(1.3);
  box-shadow:0 14px 40px -24px rgba(0,0,0,.7),inset 0 1px 0 rgba(255,255,255,.10)}
.cred-band .credential p{color:#fff}
.cred-band .credential-cta{color:#fff;border-color:#fff}
.cred-band .credential-cta:hover{background:#fff;color:var(--ink);border-color:#fff}

/* Studio band - introduces Doubleslash Studio as a distinct entity */
.studio-band{border-left:3px solid var(--forest);background:var(--surface);
  padding:32px 36px;display:grid;grid-template-columns:auto 1fr auto;gap:28px;align-items:center}
.studio-band .studio-logo{width:44px;height:44px;flex:none;border-radius:6px}
.studio-band h2{font-size:15px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;
  color:var(--forest);margin-bottom:8px}
.studio-band p{font-size:16px;line-height:1.6;color:var(--fg);max-width:60ch}
.studio-band .btn{white-space:nowrap}
@media (max-width:760px){
  .studio-band{grid-template-columns:auto 1fr;padding:26px 24px}
  .studio-band .btn{grid-column:1 / -1;justify-self:start;margin-top:4px}
}

/* Portfolio index - flagship split (one deliberate accent, not a template) */
.feature{text-decoration:none;color:var(--fg);display:grid;
  grid-template-columns:repeat(auto-fit,minmax(min(300px,100%),1fr));gap:32px;
  padding-bottom:36px;border-bottom:1px solid var(--line)}
.feature:hover{color:var(--fg)}
.feature .art{min-height:250px;border-radius:4px}
.feature .body{display:flex;flex-direction:column;gap:14px;justify-content:center}
.feature h2{font-size:clamp(28px,3.4vw,40px);letter-spacing:-.025em}
.tags{display:flex;gap:8px;flex-wrap:wrap}
.tag-o{border:1px solid var(--line);border-radius:3px;padding:4px 10px;font-size:13.5px;color:var(--fg-muted)}
.stack{display:flex;gap:6px;flex-wrap:wrap}
.chip{background:var(--surface);border:1px solid rgba(126,193,58,.3);border-radius:3px;
  padding:3px 9px;font-size:12.5px;color:var(--forest);font-weight:500}
.feature p{font-size:16px;line-height:1.6;color:var(--fg-muted)}
/* align-self:start, or the flex column stretches the button to the full width. */
.feature .more{align-self:start}
.work-grid{margin-top:0}

/* Project detail */
.back-link{font-weight:600;font-size:15px;color:var(--forest);text-decoration-color:rgba(126,193,58,.4)}
.back-link:hover{color:var(--forest)}
.detail-hero{margin-top:30px;aspect-ratio:16/8;border-radius:4px}
.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(300px,100%),1fr));gap:40px;align-items:start}
.detail-body{display:flex;flex-direction:column;gap:16px}
.detail-body p{font-size:17.5px;line-height:1.68;color:var(--fg)}
.detail-fun{font-weight:500;font-size:16.5px;line-height:1.6;color:var(--forest)}
.detail-fun .g{color:var(--forest)}
.marker{border:1.5px dashed rgba(126,193,58,.5);border-radius:4px;padding:18px 22px;
  background:var(--surface);font-size:15px;color:var(--forest)}
.aside{border-top:1px solid var(--line);padding-top:24px;display:flex;
  flex-direction:column;gap:20px;max-width:420px}
.aside h2{font-size:13px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;
  color:var(--forest);margin-bottom:10px}
.demolist{display:flex;flex-direction:column;gap:7px}
.demolist p{font-size:15px;line-height:1.5;display:flex;gap:8px;margin:0;color:var(--fg)}
.demolist .g{color:var(--forest);font-weight:700;flex:none}
.aside .links{display:flex;flex-direction:column;gap:7px}
.aside .links a{font-size:15.5px;width:fit-content}
.prevnext{display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:16px;
  margin-top:52px;border-top:1px solid var(--line);padding-top:22px}
.prevnext a{font-weight:600;font-size:15.5px;color:var(--forest);text-decoration-color:rgba(126,193,58,.4)}
.prevnext a:hover{color:var(--forest)}
.prevnext .pn-next{justify-self:end}
.prevnext-top{margin-top:0;margin-bottom:22px;border-top:0;padding-top:0;
  border-bottom:1px solid var(--line);padding-bottom:22px}
@media(max-width:640px){
  .prevnext{grid-template-columns:1fr;justify-items:center;gap:10px;text-align:center}
  .prevnext .pn-next{justify-self:center}
}

/* Contact - single centred column now the "Elsewhere" aside has gone. */
.contact-grid{display:grid;gap:28px;align-items:start;
  max-width:640px;margin-inline:auto}
/* The no-email note and the submit share the form's last row: note on the left,
   button hard right, baseline-aligned. Stacks on narrow screens. */
.contact-foot{display:flex;gap:20px;align-items:center;justify-content:space-between;
  flex-wrap:wrap}
.contact-foot .no-email{margin:0;flex:1;min-width:200px}
/* Same compact size as the hero form's submit and the CTA band's button. */
.contact-foot .btn{flex:none;font-size:14px;padding:0 18px;height:38px;line-height:1}
.contact-form{border:1px solid var(--line);border-radius:4px;padding:30px;display:flex;
  flex-direction:column;gap:18px;background:var(--surface)}
/* Direct child only: in the hero form the submit sits straight in the flex column
   and would otherwise stretch full-width. On the contact page it lives inside
   .contact-foot, which does its own alignment - hence the > . */
.contact-form > button[type="submit"]{align-self:flex-end}
.field{display:flex;flex-direction:column;gap:6px}
.field label{font-weight:600;font-size:15px;color:var(--fg)}
.field input,.field textarea{border:1.5px solid var(--line);border-radius:3px;
  padding:12px 14px;font:400 16px var(--font);color:var(--fg);background:var(--bg)}
.field textarea{resize:vertical;min-height:110px}
.field .err{font-size:14px;font-weight:600;color:var(--ink);background:var(--mustard);
  border-radius:3px;padding:2px 8px;width:fit-content}
/* Honeypot: off-screen rather than display:none, which some bots skip. */
.hp{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}
.contact-success{border-left:3px solid var(--forest);padding:8px 0 8px 24px;
  display:flex;flex-direction:column;gap:12px}
.contact-success h2{font-size:24px;color:var(--forest)}
.contact-success h2 .g{color:var(--forest)}
.contact-success p{color:var(--fg-muted)}
/* Hero variant: lives inside .hero-aside, which is already the glass surface, so
   this drops the card's own border/background (a panel inside a panel) and uses
   translucent fields over the blur instead of the opaque --bg. Whites, not the
   page's fg tokens, because it sits on the photo like the rest of the hero. */
.contact-form-hero{border:0;border-radius:0;padding:0;gap:11px;background:transparent}
.contact-form-hero .field{gap:4px}
.contact-form-hero .field label{font-size:13.5px;color:rgba(255,255,255,.88)}
/* Border .45 and placeholder .72: the field sits on the glass over a photo whose
   blurred peak is near rgb(176,197,184), so the old .22 border (1.7:1) and .45
   placeholder (2.5:1) missed the 3:1 non-text and 4.5:1 text floors. */
.contact-form-hero .field input,.contact-form-hero .field textarea{
  padding:8px 12px;font-size:15px;color:#fff;
  background:rgba(255,255,255,.08);border-color:rgba(255,255,255,.45)}
.contact-form-hero .field input::placeholder,
.contact-form-hero .field textarea::placeholder{color:rgba(255,255,255,.72)}
.contact-form-hero .field input:focus,.contact-form-hero .field textarea:focus{
  background:rgba(255,255,255,.13);border-color:rgba(255,255,255,.7)}
.contact-form-hero .field textarea{min-height:56px}
/* The site's compact button (14px / 38px), not a bespoke one, so the panel's
   submit reads as the same component as every other button. */
.contact-form-hero button{
  font-size:14px;padding:0 18px;height:38px;line-height:1;align-self:flex-end}
.hero-aside .contact-success{border-left-color:#fff}
.hero-aside .contact-success h2{color:#fff;font-size:20px}
.hero-aside .contact-success p{font-size:16px;line-height:1.5;color:rgba(255,255,255,.8)}
.no-email{font-size:13.5px;line-height:1.55;color:var(--fg-faint)}

/* CTA band - the one deliberate boxed accent, used sparingly */
.cta-band{margin-top:80px}
.cta-band .panel{position:relative;overflow:hidden;background:var(--hunter);
  border-top:1px solid var(--lamp-edge);border-bottom:1px solid var(--lamp-edge);
  padding:32px 0;display:flex;flex-direction:column;gap:14px;
  min-height:var(--band-min-h);justify-content:center}
/* width:100%, or the .container's own margin:auto shrinks it to content width as a
   flex item, pulling the button in from the band's right edge. Full width lets the
   1fr/auto grid throw the copy and button to opposite sides. */
.cta-band .panel > .container{width:100%}
.cta-band canvas{display:block}
/* Scrim, as on .svc-snap: this copy also sits straight on the animated lamp, so
   the bright streaks would otherwise drag its contrast below AA as they pass. */
.cta-band .panel::after{content:"";position:absolute;inset:0;pointer-events:none;
  background:rgba(8,9,5,.46)}
/* z-index lifts the copy above the ::after scrim, which would otherwise veil it.
   Two-column grid: the heading and copy stack down the left, the button sits in
   its own right-hand column spanning both rows so it centres against the block. */
.cta-band .panel .container{position:relative;z-index:1;
  display:grid;grid-template-columns:1fr auto;align-items:center;
  column-gap:40px;row-gap:14px}
.cta-band h2{position:relative;grid-column:1;font-size:clamp(28px,3.6vw,42px);line-height:1.05;letter-spacing:-.02em;color:#fff;max-width:20ch}
.cta-band p{position:relative;grid-column:1;font-size:17.5px;line-height:1.6;color:rgba(255,255,255,.88);max-width:52ch}
/* z-index, or the ::after scrim added for contrast covers the button. Sized to
   match the hero form's send button (the site's compact button). Right column,
   spanning both text rows, so it sits at the right edge, vertically centred. */
.cta-band .btn{position:relative;z-index:1;grid-column:2;grid-row:1/3;
  justify-self:end;align-self:center;
  font-size:14px;padding:0 18px;height:38px;line-height:1}
/* Stack on narrow screens: button drops below the copy, left-aligned. */
@media (max-width:640px){
  .cta-band .panel .container{grid-template-columns:1fr}
  .cta-band .btn{grid-column:1;grid-row:auto;justify-self:start;margin-top:4px}
}

/* 404 - now sits on the lamp band, so the type is light-on-dark. */
.notfound-hero{padding:104px 0 96px}
.notfound-hero h1{font-size:clamp(26px,3vw,36px);line-height:1.15;letter-spacing:-.02em;color:#fff}
.notfound-hero p{font-size:17.5px;line-height:1.6;max-width:44ch;color:rgba(255,255,255,.88)}

/* Footer */
.site-footer{position:relative;overflow:clip;
  background:var(--ink);color:#fff;border-top:1px solid var(--line)}

/* Oversized // watermark, sheared and bled off the bottom-right corner.
   Decorative only: aria-hidden in the markup, and pointer-events:none so it can
   never sit in front of the nav links it passes behind. */
.footer-mark{position:absolute;right:-.10em;bottom:-.30em;z-index:0;
  font-size:clamp(190px,22vw,300px);font-weight:800;line-height:1;
  letter-spacing:-.20em;color:rgba(255,255,255,.05);
  pointer-events:none;-webkit-user-select:none;user-select:none}

/* Content sits above the watermark. Vertical padding only: .container supplies
   the side gutter, and the shorthand would wipe it out. */
.footer-inner{position:relative;z-index:1;padding-top:56px;padding-bottom:28px}

/* Brand block left, nav right. align-items:end pins the nav to the BOTTOM of the
   brand block, so it lines up with the last line of the tagline rather than the
   wordmark above it. */
.footer-top{display:flex;flex-wrap:wrap;gap:28px 40px;
  align-items:end;justify-content:space-between}
.footer-brand{flex:1 1 420px;min-width:0}
/* Colour only - size comes from .wordmark, so header and footer stay in step. */
.wordmark-light{color:#fff;line-height:1.2}
.footer-brand p{margin:26px 0 0;max-width:46ch;
  font-size:17px;line-height:1.6;color:rgba(255,255,255,.72)}

/* Same treatment as .primary-nav: muted text with a transparent bottom border
   that turns mustard on hover, rather than the text itself changing colour.
   align-items:center, not baseline, or the border sits under the text.
   Gap matches .primary-nav's 24px so both navs space their links alike. */
.footer-nav{display:flex;flex-wrap:wrap;gap:12px 24px;align-items:center}
.footer-nav a{text-decoration:none;font-weight:500;font-size:15.5px;color:var(--fg-muted);
  padding:4px 1px;border-bottom:2.5px solid transparent}
.footer-nav a:hover{border-bottom-color:var(--mustard);color:var(--fg-muted)}

/* GitHub mark replaces the word in the navs. Inline SVG (not <img>) so it picks
   up currentColor and the nav's hover states for free. The <a> keeps its
   accessible name via aria-label, since the icon carries no text.
   MUST come after both nav blocks: these share their specificity, so the later
   .primary-nav a:hover / .footer-nav a:hover would otherwise re-add the border. */
.gh-link{display:inline-flex;align-items:center}
.gh-icon{display:block}
/* The mark recolours on hover instead of gaining the text links' underline,
   which would float oddly beneath a glyph rather than under a word. */
.primary-nav .gh-link,.footer-nav .gh-link{border-bottom-color:transparent}
.primary-nav .gh-link:hover,.footer-nav .gh-link:hover{color:var(--mustard);
  border-bottom-color:transparent}

/* Bottom row: full-width rule above, copyright left, sign-off right. */
.footer-bottom{display:flex;flex-wrap:wrap;gap:6px 24px;
  align-items:baseline;justify-content:space-between;
  margin-top:44px;padding-top:22px;border-top:1px solid rgba(255,255,255,.12)}
/* .55, not .45: at .45 these land on 4.49:1 against the footer's #0c0c07 - a hair
   under the 4.5:1 AA floor for body text. */
.footer-copy,.footer-signoff{font-size:16px;color:rgba(255,255,255,.55)}
/* Legal cluster: Privacy link + the Cookie settings button that reopens the
   consent banner. The button is bare so it reads as a link, but stays a <button>
   so it is keyboard-operable and announced as an action. Same .55 as its
   neighbours - 6.1:1 on the footer, clears AA. */
.footer-legal{display:flex;flex-wrap:wrap;gap:6px 18px;align-items:baseline}
.footer-legal a,.cookie-settings-link{font-size:16px;color:rgba(255,255,255,.55);
  text-decoration:none;background:none;border:0;padding:0;margin:0;
  font:inherit;line-height:inherit;cursor:pointer}
.footer-legal a:hover,.cookie-settings-link:hover{color:rgba(255,255,255,.82)}

@media (max-width:640px){
  .footer-inner{padding-top:40px;padding-bottom:24px}
  .footer-top{align-items:start}
  .footer-brand p{margin-top:18px}
  .footer-nav{gap:12px 20px}
}

/* Cookie consent banner - a non-modal bar pinned to the bottom. z-index above the
   header (60) and mobile menu (55) so the choice is never obscured. Injected by
   js/consent.js only when a choice is needed. */
.cookie-banner{position:fixed;left:0;right:0;bottom:0;z-index:70;
  background:rgba(14,14,6,.96);-webkit-backdrop-filter:blur(20px) saturate(1.4);
  backdrop-filter:blur(20px) saturate(1.4);
  border-top:1px solid var(--line);box-shadow:0 -12px 34px -22px rgba(0,0,0,.85)}
.cookie-banner[hidden]{display:none}
.cookie-banner__inner{max-width:var(--maxw);margin-inline:auto;padding:16px 32px;
  display:flex;gap:16px 28px;align-items:center;justify-content:space-between;flex-wrap:wrap}
.cookie-banner__text{margin:0;max-width:66ch;font-size:14.5px;line-height:1.55;
  color:rgba(255,255,255,.9)}
.cookie-banner__text a{color:#fff;text-decoration:underline}
.cookie-banner__actions{display:flex;gap:10px;flex-shrink:0}
/* Same compact size for both, so Reject is exactly as easy to hit as Accept. */
.cookie-banner__actions .btn{display:inline-flex;align-items:center;
  font-size:14px;padding:0 20px;height:40px;line-height:1}
@media (max-width:640px){
  .cookie-banner__inner{padding:14px 20px;gap:12px}
  .cookie-banner__actions{width:100%}
  .cookie-banner__actions .btn{flex:1;justify-content:center}
}

/* Pre-launch placeholder markers */
.ph{color:var(--forest);font-style:italic}

/* Privacy page - long-form legal prose. */
.legal{max-width:74ch}
.legal h2{font-size:clamp(22px,2.6vw,28px);line-height:1.2;letter-spacing:-.01em;
  color:var(--fg);margin:40px 0 12px}
.legal h2:first-of-type{margin-top:0}
.legal p{font-size:16.5px;line-height:1.65;color:var(--fg-muted);margin:0 0 14px}
.legal ul{margin:0 0 16px;padding-left:22px}
.legal li{font-size:16.5px;line-height:1.6;color:var(--fg-muted);margin:0 0 8px}
.legal a{color:var(--fg);text-decoration:underline;text-underline-offset:2px}
.legal a:hover{color:var(--rasp)}
.legal strong{color:var(--fg);font-weight:600}
.legal .updated{font-size:14.5px;color:var(--fg-faint);margin-bottom:28px}
.legal .table-wrap{overflow-x:auto;margin:6px 0 20px}
.legal table{width:100%;border-collapse:collapse;font-size:15px}
.legal th,.legal td{text-align:left;padding:10px 12px;border-bottom:1px solid var(--line);
  vertical-align:top;color:var(--fg-muted)}
.legal th{color:var(--fg);font-weight:600}

/* Reveal animation */
/* Hidden-until-revealed ONLY when JS is present (html.js). No JS = fully visible. */
.js [data-reveal]{opacity:0;transform:translateY(24px);
  transition:opacity .8s cubic-bezier(.2,.7,.2,1),transform .8s cubic-bezier(.2,.7,.2,1)}
.js [data-reveal].is-visible{opacity:1;transform:none}

/* About: theme cards fade in one after another (the .themes list is the reveal
   trigger, not a block fade), matching the services page. */
.js .themes[data-reveal]{opacity:1;transform:none}
.js .theme{opacity:0;transform:translateY(20px);
  transition:opacity .8s cubic-bezier(.2,.7,.2,1),transform .8s cubic-bezier(.2,.7,.2,1)}
.js .themes[data-reveal].is-visible .theme{opacity:1;transform:none}
.theme:nth-child(1){transition-delay:0s}
.theme:nth-child(2){transition-delay:.28s}
.theme:nth-child(3){transition-delay:.56s}
.theme:nth-child(4){transition-delay:.84s}

/* About: the Distinction pill makes an entrance - it pops in with an overshoot
   as its section reveals, then throws off two fading rings to catch the eye. */
.js [data-reveal].is-visible .credential .tag{
  animation:pill-pop .6s cubic-bezier(.2,1.4,.4,1) both,
            pill-glow 1.1s ease-out .55s 2;
}
@keyframes pill-pop{
  0%{opacity:0;transform:scale(.4)}
  60%{opacity:1;transform:scale(1.15)}
  80%{transform:scale(.96)}
  100%{opacity:1;transform:scale(1)}
}
@keyframes pill-glow{
  0%{box-shadow:0 0 0 0 rgba(239,196,3,.55)}
  100%{box-shadow:0 0 0 16px rgba(239,196,3,0)}
}

/* Blur-to-focus reveal: on top of the standard fade, the block starts out of
   focus and sharpens as it arrives. Restates the transition to add `filter`. */
.js .blur-reveal[data-reveal]{filter:blur(12px);
  transition:opacity .9s cubic-bezier(.2,.7,.2,1),transform .9s cubic-bezier(.2,.7,.2,1),filter .9s cubic-bezier(.2,.7,.2,1)}
.js .blur-reveal[data-reveal].is-visible{filter:blur(0)}

/* Services: the svc-grid is the reveal trigger, but it shouldn't fade as one
   block - each card fades in turn instead. Neutralise the grid's own fade
   (higher specificity than the generic rule above), hide the cards, then
   stagger them so they arrive one after another starting with the first. */
.js .svc-grid[data-reveal]{opacity:1;transform:none}
.js .svc-card{opacity:0;transform:translateY(20px);
  transition:opacity .8s cubic-bezier(.2,.7,.2,1),transform .8s cubic-bezier(.2,.7,.2,1)}
.js .svc-grid[data-reveal].is-visible .svc-card{opacity:1;transform:none}
.svc-card:nth-child(1){transition-delay:0s}
.svc-card:nth-child(2){transition-delay:.28s}
.svc-card:nth-child(3){transition-delay:.56s}
.svc-card:nth-child(4){transition-delay:.84s}

@media (prefers-reduced-motion:reduce){
  *{transition:none!important;animation:none!important}
  [data-reveal]{opacity:1;transform:none}
  .theme{opacity:1;transform:none}
  .svc-card{opacity:1;transform:none}
  .blur-reveal{filter:none}
}

/* Cross-document View Transitions for project prev/next navigation (Chromium
   126+). main.js scopes this to the pager: it types the transition 'forward'
   (Next) or 'back' (Previous) and skips it for every other navigation. Browsers
   without support just navigate normally. Reduced-motion falls back to a plain
   cross-fade via the default animations below being suppressed. */
@view-transition{navigation:auto}

@media (prefers-reduced-motion:no-preference){
  ::view-transition-old(root),
  ::view-transition-new(root){
    animation-duration:.42s;animation-timing-function:cubic-bezier(.22,.61,.36,1);
  }
  /* Next: incoming project swipes in from the right, old exits to the left. */
  html:active-view-transition-type(forward)::view-transition-old(root){animation-name:vt-exit-left}
  html:active-view-transition-type(forward)::view-transition-new(root){animation-name:vt-enter-right}
  /* Previous: incoming swipes in from the left, old exits to the right. */
  html:active-view-transition-type(back)::view-transition-old(root){animation-name:vt-exit-right}
  html:active-view-transition-type(back)::view-transition-new(root){animation-name:vt-enter-left}
}
@keyframes vt-enter-right{from{transform:translateX(100%)}to{transform:translateX(0)}}
@keyframes vt-exit-left{from{transform:translateX(0)}to{transform:translateX(-100%)}}
@keyframes vt-enter-left{from{transform:translateX(-100%)}to{transform:translateX(0)}}
@keyframes vt-exit-right{from{transform:translateX(0)}to{transform:translateX(100%)}}
