/* ---- self-hosted type ----
   Latin-subset woff2, same-origin: the Google stylesheet blocked first render from a second
   origin. Bricolage and Instrument Sans are variable, one file each. */
@font-face {
  font-family: "Bricolage Grotesque";
  font-style: normal;
  font-weight: 500 800;
  font-display: swap;
  src: url("../assets/fonts/bricolage-grotesque-latin.woff2") format("woff2");
}
@font-face {
  font-family: "Instrument Sans";
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url("../assets/fonts/instrument-sans-latin.woff2") format("woff2");
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../assets/fonts/ibm-plex-mono-400-latin.woff2") format("woff2");
}
@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("../assets/fonts/ibm-plex-mono-500-latin.woff2") format("woff2");
}

:root {
  /* ---- brand palette: fixed values, identical in both themes ---- */
  --sky-core: #99cdf9;
  --sky-mid: #79b0e2;
  --sky-deep: #6094c8;
  --sky-edge: #4c7cb0;
  /* brand colour, sampled straight from the logo artwork */
  --ink: #060644;
  /* deliberately darker than the old #2c3a66: secondary hero copy sits on the
     mid-blue gradient, where a lighter tint would drop below AA */
  --ink-soft: #2a2a5c;
  --night: #060644;
  --turban: #b71208;
  --turban-hover: #93100a;
  --paper: #f2f7fd;
  /* --turban is a fill colour: as text it lands at 1.9:1 on the dark --surface. --accent-
     ink is the same accent lightened per theme, for the few places red carries type. */
  --accent-ink: var(--turban);

  /* The sprite's backdrop is baked light into the WebP, so the hero stays light blue in
     both themes. Hero text uses these fixed inks, never the themed --text tokens. */
  --hero-ink: var(--ink);
  /* Own value, measured on the render: --ink-soft #2a2a5c hits 3.58:1 on the hero gradient
     and fails AA. #141438 gives 4.78:1, and the nav chip 4.41 -> 5.89:1. */
  --hero-ink-soft: #141438;

  /* The canvas mask's bottom band. A length, not a percentage: the band lands at a
     different fraction of the canvas in each mode (82.6% A, 70.6% B, 66.9% C). */
  --floor-h: clamp(120px, 24svh, 300px);

  /* ---- semantic tokens: LIGHT theme ---- */
  --bg: var(--paper);
  --bg-alt: #eaf2fb;
  --surface: var(--paper);
  --text: var(--ink);
  --text-soft: var(--ink-soft);
  --border: rgba(6, 6, 68, .12);
  --border-soft: rgba(6, 6, 68, .08);
  --header-bg: rgba(242, 247, 253, .92);
  --shadow-sm: rgba(6, 6, 68, .08);
  --shadow-md: rgba(6, 6, 68, .12);
  --shadow-lg: rgba(6, 6, 68, .1);
  --placeholder-a: rgba(96, 148, 200, .12);
  --placeholder-b: rgba(96, 148, 200, .22);
  --placeholder-border: var(--sky-edge);

  /* the footer card reads as deep navy in both themes */
  --feature-bg: var(--night);
  --feature-text: var(--paper);
  --feature-soft: rgba(242, 247, 253, .8);
  --feature-border: rgba(242, 247, 253, .35);

  --font-display: "Bricolage Grotesque", "Instrument Sans", system-ui, sans-serif;
  --font-body: "Instrument Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
  /* Pinned rather than a system stack: the same 11px label measured 166.9px to 183.8px
     across four platform faces, and this family now carries every label on the site. */
  --font-mono: "IBM Plex Mono", ui-monospace, "SFMono-Regular", "Cascadia Mono", Menlo, Consolas, monospace;

  /* Tracks the viewport. A flat 1080px marooned every section, leaving 740px of empty page
     each side at 2560. The 1600px ceiling is where a line stops being scannable. */
  --content-width: clamp(1080px, 84vw, 1600px);
  --side-pad: clamp(16px, 5.5vw, 84px);
  --ease: cubic-bezier(.22, 1, .36, 1);
  /* One fluid ramp per spacing job: lower bound is the 320px value, upper the >=1440px one,
     so nothing here needs a media query to resize. */
  --gap: clamp(16px, 2vw, 24px);
  --section-pad: clamp(48px, 7vw, 96px);
  --card-pad: clamp(22px, 2.4vw, 28px);
  /* Two radius steps -- panels and card grids, then the larger picture frame.
     Much of what makes the layout read as current rather than 2019. */
  --radius: 20px;
  --radius-lg: 28px;
  /* Sized so the lockup's "ARJI" (0.359 of the mark's height, measured off the artwork)
     still renders at 16.5px on a 320px phone. */
  --mark-h: clamp(46px, .9vw + 43px, 52px);
  --nav-pad-y: clamp(8px, .5vw + 6px, 12px);
  /* Derived, not guessed: nothing in the bar is taller than the mark, so the bar is exactly
     this tall at every viewport. The +1px is its bottom rule. */
  --header-h: calc(max(var(--mark-h), 44px) + 2 * var(--nav-pad-y) + 1px);
  /* Where anything pinned under the fixed bar starts -- the tech panel, and the hero copy
     on short viewports. One value so the two cannot drift apart. */
  --hero-top: calc(var(--header-h) + 24px);

  /* ---- nav bar, LIGHT ---- */
  --nav-bg: rgba(242, 247, 253, .72);
  --nav-hi: rgba(255, 255, 255, .6);
  --nav-marker: rgba(255, 255, 255, .92);
  --nav-chip: rgba(6, 6, 68, .05);
  --nav-pulse: rgba(96, 148, 200, .55);

  color-scheme: light;
}

/* ---- semantic tokens: DARK theme ----
   Follows the OS/browser setting and nothing else: no in-page switch, no JavaScript in the
   theme path. */
@media (prefers-color-scheme: dark) {
  :root {
    --bg: #08082a;
    --bg-alt: #0e0e35;
    --surface: #14143f;
    --text: #e8e8f7;
    --text-soft: #a8a8c9;
    --border: rgba(232, 232, 247, .14);
    --border-soft: rgba(232, 232, 247, .07);
    --header-bg: rgba(8, 8, 42, .92);
    --shadow-sm: rgba(0, 0, 0, .5);
    --shadow-md: rgba(0, 0, 0, .55);
    --shadow-lg: rgba(0, 0, 0, .5);
    --placeholder-a: rgba(120, 165, 215, .10);
    --placeholder-b: rgba(120, 165, 215, .18);
    --placeholder-border: #5b86b8;
    --feature-bg: #04041d;
    --feature-text: #e8e8f7;
    --feature-soft: rgba(232, 232, 247, .78);
    --feature-border: rgba(232, 232, 247, .3);
    --accent-ink: #ff8f7f;
    /* Measured against the hero gradient the bar sits on, not against --bg: --text-soft
       lands at 5.29:1 there. See DESIGN.md. */
    --nav-bg: rgba(8, 8, 42, .72);
    --nav-hi: rgba(232, 232, 247, .08);
    --nav-marker: rgba(232, 232, 247, .10);
    --nav-chip: rgba(232, 232, 247, .06);
    --nav-pulse: rgba(120, 165, 215, .5);

    color-scheme: dark;
  }
}

* { margin: 0; padding: 0; box-sizing: border-box; }
/* The UA's [hidden] rule loses to any author display:, which silently showed the demo
   carousel nav on services that have no demo. This backstops every such component. */
[hidden] { display: none !important; }
/* Zoom safety net: no replaced element may burst its column when the layout narrows. Every
   img already sizes itself, so this only catches one that does not. */
img, svg, video { max-width: 100%; height: auto; }
html { scroll-behavior: smooth; }

body {
  font-family: var(--font-body);
  color: var(--text);
  background: var(--bg);
  overflow-x: hidden;
  -webkit-tap-highlight-color: transparent;
  /* safety net at 320px and below: long unbroken strings (a URL, a long
     compound) wrap instead of forcing the page to scroll sideways */
  overflow-wrap: break-word;
  transition: background-color .25s ease, color .25s ease;
}

/* anchors land clear of the fixed header instead of hiding under it */
section[id], footer[id] { scroll-margin-top: var(--header-h); }

h1, h2, h3 {
  font-family: var(--font-display);
  color: var(--text);
  letter-spacing: -0.02em;
}

.skip-link {
  position: absolute;
  left: -9999px;
  top: 0;
  background: var(--surface);
  color: var(--text);
  padding: .625rem 1rem;
  z-index: 100;
}
.skip-link:focus { left: 10px; top: 10px; }

/* ---------- header ---------- */

/* ---------- nav bar ----------
   Fixed not sticky: a sticky bar takes flow space above a 100svh hero and pushes the fold off
   screen. Square outer edges, radii on the pieces inside. See DESIGN.md. */

/* Full-bleed: the glass, the rule and the highlight belong to the fixed shell so they run
   edge to edge. .header-inner is only the centred content row. */
.site-header {
  position: fixed;
  top: 0; left: 0; right: 0;
  z-index: 50;
  background: var(--nav-bg);
  backdrop-filter: saturate(180%) blur(18px);
  -webkit-backdrop-filter: saturate(180%) blur(18px);
  border-bottom: 1px solid var(--border);
  box-shadow: 0 1px 0 var(--nav-hi) inset;
  transition: background .35s var(--ease), backdrop-filter .35s var(--ease),
              border-color .35s var(--ease), box-shadow .35s var(--ease);
}

/* Transparent until the page moves, hero pages only. The hero's backdrop is baked light, so
   the bar is a light surface in BOTH themes. See DESIGN.md, Color palette. */
.site-header.at-top {
  background: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  border-bottom-color: transparent;
  box-shadow: none;
  --text: var(--hero-ink);
  --text-soft: var(--hero-ink-soft);
  --nav-chip: rgba(6, 6, 68, .05);
  --nav-marker: rgba(255, 255, 255, .92);
  --border-soft: rgba(6, 6, 68, .10);
  --nav-pulse: rgba(96, 148, 200, .55);
}
.header-inner {
  max-width: var(--content-width);
  margin: 0 auto;
  /* brand | links | right cluster. The 1fr centres the links against the
     content column, not against whatever width the brand happens to be. */
  display: grid;
  grid-template-columns: auto 1fr auto;
  align-items: center;
  gap: var(--gap);
  min-height: calc(var(--header-h) - 1px);
  padding: var(--nav-pad-y) var(--side-pad);
}

/* min-width, or the site-wide img{max-width:100%} lets the mark collapse -- measured 5px
   wide at 1101px. Pins the column to the artwork and lets the 1fr absorb it. */
.brand {
  display: flex;
  align-items: center;
  min-width: max-content;
  text-decoration: none;
}
.brand-mark {
  display: block;
  height: var(--mark-h);
  width: auto;
  flex: 0 0 auto;
}
.brand-mark--dark { display: none; }

/* ---------- links and the sliding marker ----------
   One absolutely positioned box that JS moves and resizes, with the links above it on
   z-index. One element for any number of links. */

.primary-nav {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 2px;
  font-size: 0.875rem;
  font-weight: 500;
}
.nav-marker {
  position: absolute;
  top: 50%;
  left: 0;
  height: 2rem;
  background: var(--nav-marker);
  border: 1px solid var(--border-soft);
  border-radius: 999px;
  opacity: 0;
  transform: translateY(-50%);
  transition: transform .5s var(--ease), width .5s var(--ease), opacity .25s ease;
  pointer-events: none;
}
.primary-nav a {
  position: relative;
  z-index: 1;
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: 8px 12px;
  white-space: nowrap;
  color: var(--text-soft);
  text-decoration: none;
  transition: color .3s var(--ease);
}
.primary-nav a.is-active { color: var(--text); }
.primary-nav a:focus-visible { outline: 2px solid var(--turban); outline-offset: -2px; border-radius: 999px; }
/* --accent-ink, not --turban: at 4px the raw red disappears against the dark
   bar, and this token is the same accent already resolved per theme. */
.nav-dot {
  width: 4px;
  height: 4px;
  flex: 0 0 auto;
  background: var(--accent-ink);
  opacity: 0;
  transform: scale(0);
  transition: opacity .3s var(--ease), transform .3s var(--ease);
}
.primary-nav a.is-active .nav-dot { opacity: 1; transform: scale(1); }

/* ---------- right cluster ---------- */

.header-right {
  display: flex;
  align-items: center;
  gap: 10px;
}
/* The availability line, cut to two words to fit the bar. Hidden below the width where it
   stops fitting. */
.nav-status {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  padding: 6px 10px;
  border-radius: 999px;
  background: var(--nav-chip);
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: .06em;
  text-transform: uppercase;
  white-space: nowrap;
  color: var(--text-soft);
}
.nav-status::before {
  content: "";
  width: 6px;
  height: 6px;
  border-radius: 50%;
  background: var(--sky-deep);
  animation: nav-pulse 2.4s ease-out infinite;
}
@keyframes nav-pulse {
  from { box-shadow: 0 0 0 0 var(--nav-pulse); }
  70%, to { box-shadow: 0 0 0 7px transparent; }
}

.nav-cta {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  padding: .5rem 1rem;
  border-radius: 999px;
  background: var(--turban);
  color: var(--paper);
  font-size: 0.84375rem;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
  transition: background .3s ease, transform .3s var(--ease);
}
.nav-cta:focus-visible { outline: 2px solid var(--ink); outline-offset: 2px; }
.nav-cta svg { transition: transform .3s var(--ease); }

.nav-toggle {
  display: none;
  position: relative;
  width: 44px;
  height: 44px;
  background: none;
  border: 1px solid var(--border);
  border-radius: 50%;
  cursor: pointer;
}
.nav-toggle:focus-visible { outline: 2px solid var(--turban); outline-offset: 2px; }
.nav-toggle span {
  position: absolute;
  left: 12px;
  width: 18px;
  height: 2px;
  background: var(--text);
  transition: transform .4s var(--ease), top .4s var(--ease), opacity .2s ease;
}
.nav-toggle span:nth-child(1) { top: 16px; }
.nav-toggle span:nth-child(2) { top: 21px; }
.nav-toggle span:nth-child(3) { top: 26px; }
.nav-toggle[aria-expanded="true"] span:nth-child(1) { top: 21px; transform: rotate(45deg); }
.nav-toggle[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.nav-toggle[aria-expanded="true"] span:nth-child(3) { top: 21px; transform: rotate(-45deg); }

/* ---------- full-screen overlay menu ----------
   Driven by visibility, not [hidden]: this file's own [hidden]{display:none!important}
   would kill the fade, and visibility still takes the links out of the tab order. */

.nav-overlay {
  position: fixed;
  inset: 0;
  z-index: 40;
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  gap: 24px;
  padding: calc(var(--header-h) + 28px) var(--side-pad) 32px;
  background: var(--bg);
  overflow-y: auto;
  opacity: 0;
  visibility: hidden;
  transition: opacity .4s var(--ease), visibility .4s;
}
.nav-overlay.open { opacity: 1; visibility: visible; }
.nav-overlay ul { list-style: none; }
/* Scoped to the list: unscoped, .nav-overlay a also matched the foot's email link, which
   then rendered at the 54px display size and ran off a 390px screen. */
.nav-overlay ul a {
  display: inline-flex;
  align-items: baseline;
  padding: 6px 0;
  font-family: var(--font-display);
  font-weight: 500;
  font-size: clamp(28px, 7.2vw, 54px);
  line-height: 1.08;
  letter-spacing: -.02em;
  color: var(--text);
  text-decoration: none;
  opacity: 0;
  transform: translateY(18px);
  transition: opacity .6s var(--ease), transform .6s var(--ease), color .3s ease;
}
.nav-overlay.open ul a { opacity: 1; transform: none; }
/* the reference's stagger; eight items is the most this list holds */
.nav-overlay.open li:nth-child(1) a { transition-delay: .04s; }
.nav-overlay.open li:nth-child(2) a { transition-delay: .08s; }
.nav-overlay.open li:nth-child(3) a { transition-delay: .12s; }
.nav-overlay.open li:nth-child(4) a { transition-delay: .16s; }
.nav-overlay.open li:nth-child(5) a { transition-delay: .20s; }
.nav-overlay.open li:nth-child(6) a { transition-delay: .24s; }
.nav-overlay.open li:nth-child(7) a { transition-delay: .28s; }
.nav-overlay.open li:nth-child(8) a { transition-delay: .32s; }
.nav-overlay ul a[aria-current] { color: var(--accent-ink); }
.nav-overlay ul a em { font-style: normal; color: var(--accent-ink); }
.nav-overlay-foot {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 8px 24px;
  padding-top: 18px;
  border-top: 1px solid var(--border);
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--text-soft);
}
/* 24px floor, as everywhere else. This one is only ever shown on the widths
   that have no cursor, so it is the last place a 14px target belongs. */
.nav-overlay-foot a {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  color: inherit;
  text-decoration: none;
  overflow-wrap: anywhere;
}

/* The mark follows the theme, except over the hero with the bar transparent -- there the
   surface is light in both themes and the light-ground mark is the one that reads. */
@media (prefers-color-scheme: dark) {
  .brand-mark--light { display: none; }
  .brand-mark--dark { display: block; }
  .site-header.at-top .brand-mark--light { display: block; }
  .site-header.at-top .brand-mark--dark { display: none; }
}

/* A page with no hero still needs its <main> pushed clear of the fixed bar. */
.no-hero main { padding-top: var(--header-h); }

/* ---------- hero: three modes, one ordered cascade ----------
   C (stacked, no query) is the default; B then A override it in source order. That ordering
   IS the partition -- never carve out a neighbour's complement. See DESIGN.md, Hero modes. */

:root {
  /* Sprite geometry, measured off the idle frame by scanning for the topmost pixel where R >
     B + 6. Crown lands at y=15 of the 540px frame. See DESIGN.md, Hero modes. */
  --crown-frac: .0278;          /* frame top -> crown */
  --subject-frac: .9722;        /* crown -> frame bottom, i.e. 1 - crown-frac */
  /* The upscale ceiling: the sprite frame is 960px wide, so past ~1.67x the
     face goes soft. --content-width keys off this same number. */
  --frame-max: 1600px;

  --rail-inset: clamp(16px, 4vh, 40px);
  /* Chips per rail = ceil(TECH.length / 2); js/main.js deals them alternately. CSS cannot
     count them, so this is the one number here that can silently go stale. */
  --rail-chips: 7;
  /* The tallest a chip can be while a whole rail still stands BESIDE the portrait rather
     than across the copy. Measured: unfitted rails crossed the copy by 32px at 390x844 and
     238px at 320x568. */
  --chip-fit: calc(
    (var(--subject-h) - var(--rail-inset) - (var(--rail-chips) - 1) * var(--rail-gap))
    / var(--rail-chips));

  /* MODE C -- stacked. Phones and anything portrait-shaped. See DESIGN.md, Hero modes. */
  --subject-h: 44svh;
  --frame-fit: 190vw;
  /* Read by tools/viewport-check.html and by js/main.js, which picks the
     sprite sheet off it -- so the breakpoints live in exactly one place. */
  --hero-mode: "C";
}

.hero {
  position: relative;
  min-height: 100vh; /* fallback for browsers without svh support */
  min-height: 100svh;
  display: flex;
  flex-direction: column;
  background: radial-gradient(115% 90% at 50% 58%, var(--sky-core) 0%, var(--sky-mid) 42%, var(--sky-deep) 72%, var(--sky-edge) 100%);
  overflow: hidden;
  padding-top: var(--header-h);

  /* Undo the sprite's crop for a frame height, then its 16:9 for a width -- that division is
     what sizes the hero BY the subject. On .hero, not .stage, because .copy reads it too. */
  --stage-w: min(
    var(--frame-max),
    calc(var(--subject-h) / var(--subject-frac) * 16 / 9),
    var(--frame-fit)
  );
}
.stage {
  position: relative;
  width: var(--stage-w);
  /* Horizontal centring is align-self, never auto side margins: those collapse to 0 once
     the stage is wider than its container and silently left-align it. */
  margin: 0;
  align-self: center;
  flex: 0 0 auto;
}

.stage canvas {
  display: block;
  width: 100%;
  aspect-ratio: 16 / 9;
  /* Three mask layers, intersected: the horizontal softens left/right, the vertical kills the
     seam where the baked backdrop meets the CSS gradient. See DESIGN.md. */
  --edge-fade: linear-gradient(to right, transparent 0, #000 11%, #000 89%, transparent 100%);
  --top-fade: linear-gradient(to bottom, transparent 0, #000 20%);
  --bottom-fade: linear-gradient(to top, transparent 0, #000 calc(var(--floor-h) * .45));
  -webkit-mask-image: var(--edge-fade), var(--top-fade), var(--bottom-fade);
  -webkit-mask-composite: source-in, source-in;
  mask-image: var(--edge-fade), var(--top-fade), var(--bottom-fade);
  mask-composite: intersect, intersect;
  opacity: 0;
  transition: opacity .8s ease .1s;
  will-change: transform;
  /* the source sprite is upscaled to fill the stage; a slight contrast/saturation
     lift reads as sharper without introducing sharpening artifacts on a face */
  filter: contrast(1.06) saturate(1.08);
}
.stage canvas.ready { opacity: 1; }

/* MODE C's copy: centred over the bottom-anchored portrait. */
.copy {
  position: static;
  z-index: 2;
  /* Auto block margins soak up the hero's free space: centres the copy AND pins the portrait
     to the hero's bottom edge. A margin-top on .stage dumped 155px of that slack below him on
     a 390x844 phone. */
  margin: auto;
  padding: 0 var(--side-pad);
  /* 520px until the rails would reach the text, then only the space between them -- measured,
     at 568x320 the .sub ran under both rails at a flat 520px. The fallback gives the plain
     cap where rails are absent. */
  max-width: min(520px, 100% - 2 * var(--rail-gutter, 0px));
  text-align: center;
}
/* Scoped to .copy and they must stay scoped: unscoped, these two centred the dash and
   button row of EVERY .eyebrow and .row on the page. Modes B and A put both back. */
/* .hero .copy, not .copy: the base .hero .eyebrow::before ties a bare .copy
   .eyebrow::before on specificity and is written later, so it won and the dash rendered
   inline. */
.hero .copy .eyebrow::before { display: block; margin: 0 auto 10px; }
.copy .row { justify-content: center; }

.hero h1 { color: var(--hero-ink); }
/* The site-wide label voice, shared with .svc-kicker / .svc-label: one mono micro-label for
   every section, card, footer column and gallery stat. See DESIGN.md. */
/* One declaration for all three names so the micro-label cannot drift into three near-
   copies. 11px / 500 / .14em / uppercase / mono is the voice; each class keeps only its own
   structure. */
.eyebrow,
.svc-kicker,
.svc-label {
  font-family: var(--font-mono);
  font-size: 0.6875rem; /* 11px */
  font-weight: 500;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text-soft);
}
/* Deliberately not shared: margin-bottom would push every .svc-kicker down 14px, and text-
   wrap is inherited, so balancing a flex row's children is not the same thing. */
.eyebrow {
  margin-bottom: 14px;
  /* Evens line lengths. It does NOT hold a phrase together -- measured, the hero job title
     still broke at 1101, 1200 and 1440 with this in force. That is .nb's job, below. */
  text-wrap: balance;
}
/* Opt-in, so it cannot leak the way a bare .eyebrow::before rule once did. A wrap may land
   on a separator but never inside one of these phrases. */
.nb { white-space: nowrap; }
/* Only the colour is hero-specific. The h1, the dash and the rest of the hero stay exempt;
   a small label is not what the hero's measured breakpoints are about. */
.hero .eyebrow { color: var(--hero-ink-soft); }
/* scoped to .hero, so the <=1100px block's `.copy .eyebrow::before` still
   resolves (.copy is inside .hero) and nothing else grows a dash */
.hero .eyebrow::before {
  content: "";
  display: inline-block;
  width: 22px; height: 2px;
  background: var(--turban);
  vertical-align: middle;
  margin-right: 10px;
  border-radius: 2px;
}
h1 {
  font-weight: 800;
  font-size: clamp(2.125rem, 4.6vw, 3.875rem);
  line-height: 1.02;
  text-wrap: balance;
}
/* Scoped to .hero: unscoped this drew the animated red rule under every h1 em on the site,
   and its nowrap stopped the services display line wrapping at all. */
.hero h1 em {
  font-style: normal;
  position: relative;
  white-space: nowrap;
}
.hero h1 em::after {
  content: "";
  position: absolute;
  left: 0; right: 0; bottom: .03em;
  height: .10em;
  background: var(--turban);
  border-radius: 99px;
  transform: scaleX(0);
  transform-origin: left;
  animation: ink .7s cubic-bezier(.6,0,.2,1) .9s forwards;
}
@keyframes ink { to { transform: scaleX(1); } }

.sub {
  margin-top: 18px;
  font-size: 1.03125rem;
  line-height: 1.55;
  color: var(--hero-ink-soft);
  font-weight: 500;
}
/* Two winks, one shown: a phone has no cursor to move, and the head-turn there is touch-
   and scroll-driven. Done with a media query, not script, so it is right before JS runs and
   with JS off. */
.sub .wink,
.sub .wink-tap { color: var(--hero-ink); font-weight: 600; }
.sub .wink-tap { display: none; }
@media (hover: none) {
  .sub .wink { display: none; }
  .sub .wink-tap { display: inline; }
}

.row {
  margin-top: 26px;
  display: flex;
  flex-wrap: wrap;
  gap: 10px 18px;
  font-size: 0.90625rem;
  font-weight: 600;
}
/* An underline with a 24px floor, not a border-bottom: a border sits on the box, so growing
   the box to 24px peels the rule off the word. These were 22px, under WCAG 2.5.8. */
.hero .row a:not(.btn) {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  color: var(--hero-ink);
  text-decoration: underline;
  text-decoration-color: rgba(6, 6, 68, .3);
  text-decoration-thickness: 2px;
  text-underline-offset: 4px;
  transition: text-decoration-color .2s ease, color .2s ease;
}
.row a:focus-visible { outline: 2px solid var(--turban); outline-offset: 4px; }

/* ---------- hero tech-stack orbit ----------
   Absolutely positioned over the hero, never in its flow, so .stage keeps its sizing and the
   head-turn canvas is untouched. See DESIGN.md. */

.tech-orbit {
  position: absolute;
  z-index: 3;
  display: none;
  /* mirrors .copy's left formula so the panel tracks the centred portrait
     instead of stranding against the edge of a very wide screen */
  right: max(clamp(20px, 5.5vw, 84px), calc(50vw - var(--stage-w) / 2 - 170px));
  top: var(--hero-top);
  width: 382px;
  padding: 20px;
  border: 1px solid rgba(255, 255, 255, .62);
  border-radius: 24px;
  background:
    radial-gradient(circle at 50% 48%, rgba(104, 198, 255, .26), transparent 39%),
    linear-gradient(145deg, rgba(255, 255, 255, .42), rgba(216, 241, 255, .14));
  box-shadow: 0 20px 46px rgba(4, 47, 105, .22), inset 0 1px 0 rgba(255, 255, 255, .65);
  backdrop-filter: blur(12px);
  /* The pointer tilt arrives as --ry/--rx from main.js rather than as an
     inline transform, so the short-viewport scale below survives it. */
  transform-origin: top right;
  transform: perspective(900px) rotateY(var(--ry, 0deg)) rotateX(var(--rx, 0deg))
             scale(var(--orbit-scale, 1));
  transition: transform .18s ease-out, box-shadow .18s ease;
}
.tech-orbit:hover {
  box-shadow: 0 26px 55px rgba(4, 47, 105, .3), inset 0 1px 0 rgba(255, 255, 255, .75);
}

.orbit-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 12px;
}
/* .eyebrow and section h2 both already have site-wide rules; the panel needs
   its own scale, not the section scale */
.tech-orbit .eyebrow { margin-bottom: 4px; }
/* the hero's exemption from the section heading scale above: this panel runs
   on its own smaller type, so it restates every axis that rule now sets */
.tech-orbit h2 {
  margin: 0;
  font-size: 1.375rem;
  line-height: 1.15;
  letter-spacing: -.02em;
  color: var(--hero-ink);
}
.stack-count {
  display: grid;
  place-items: center;
  min-width: 2.875rem;
  min-height: 2.875rem;
  border: 1px solid rgba(6, 6, 68, .13);
  border-radius: 14px;
  background: rgba(255, 255, 255, .37);
  color: var(--hero-ink);
  font-size: 0.8125rem;
  font-weight: 800;
}

.orbit-scene {
  position: relative;
  height: 326px;
  perspective: 800px;
}
.orbit-ring {
  position: absolute;
  top: 50%; left: 50%;
  width: 243px; height: 243px;
  border: 1px solid rgba(15, 59, 136, .25);
  border-radius: 50%;
  transform: translate(-50%, -50%) rotateX(64deg) rotateZ(-18deg);
  box-shadow: 0 0 26px rgba(45, 136, 225, .09), inset 0 0 18px rgba(255, 255, 255, .3);
}
.orbit-ring.two {
  width: 292px; height: 292px;
  border-color: rgba(255, 255, 255, .52);
  transform: translate(-50%, -50%) rotateX(68deg) rotateZ(48deg);
}
.orbit-ring.three {
  width: 196px; height: 196px;
  border-style: dashed;
  border-color: rgba(11, 62, 147, .22);
  transform: translate(-50%, -50%) rotateY(62deg) rotateX(55deg);
}

.orbit-core {
  position: absolute;
  top: 50%; left: 50%;
  display: grid;
  place-items: center;
  width: 100px; height: 100px;
  border: 1px solid rgba(255, 255, 255, .77);
  border-radius: 50%;
  background: radial-gradient(circle at 35% 28%, #e9fbff 0 4%, var(--sky-core) 30%, #166cb8 70%, var(--ink) 100%);
  box-shadow: 0 0 0 9px rgba(255, 255, 255, .15), 0 0 35px rgba(70, 182, 255, .85),
              inset -10px -12px 18px rgba(1, 38, 104, .45);
  color: #fff;
  transform: translate(-50%, -50%) translateZ(38px);
}
.orbit-core svg { width: 34px; height: 34px; stroke: currentColor; fill: none; stroke-width: 2; }
.orbit-core::after {
  content: "";
  position: absolute;
  inset: -16px;
  border: 1px solid rgba(255, 255, 255, .36);
  border-radius: inherit;
  animation: orbit-pulse 2.8s ease-out infinite;
}
@keyframes orbit-pulse {
  0%, 100% { transform: scale(.86); opacity: .9; }
  60% { transform: scale(1.16); opacity: 0; }
}

.tech-node {
  position: absolute;
  top: 50%; left: 50%;
  display: grid;
  place-items: center;
  width: 47px; height: 47px;
  padding: 4px;
  border: 1px solid rgba(255, 255, 255, .82);
  /* 11px, not 14: the vendored icon art bakes its own rounded rect at rx=60 on a 256
     viewBox, which lands at 11.0px when drawn at 47px. Both tiers share it so they read as
     one shape. */
  border-radius: 11px;
  background: rgba(248, 253, 255, .82);
  box-shadow: 0 8px 17px rgba(3, 49, 112, .18);
  color: var(--hero-ink);
  font-family: var(--font-body);
  font-size: 0.65625rem;
  font-weight: 700;
  line-height: 1.15;
  text-align: center;
  cursor: pointer;
  transform: translate(-50%, -50%) translate(var(--x), var(--y));
  /* the bob rides on the independent `translate` property so it never has to
     fight the positioning transform above */
  animation: orbit-float 4s ease-in-out infinite var(--delay);
  transition: box-shadow .2s ease, background .2s ease, scale .25s cubic-bezier(.2, .8, .2, 1);
}
/* 4px, down from 8: the lift is relative motion between neighbours, so on a fourteen-node
   ring it eats the spacing twice over and the header clearance as well. */
@keyframes orbit-float {
  0%, 100% { translate: 0 0; }
  50% { translate: 0 -4px; }
}
.tech-node:hover,
.tech-node[aria-pressed="true"] {
  z-index: 4;
  background: #fff;
  box-shadow: 0 14px 25px rgba(3, 49, 112, .28), 0 0 0 5px rgba(255, 255, 255, .28);
  /* 1.08, down from 1.14: with fourteen tiles on the ring instead of eight the
     grown tile has to still clear its neighbours, and the header above it */
  scale: 1.08;
}
.tech-node:focus-visible { outline: 2px solid var(--turban); outline-offset: 3px; }

/* Tier one: a real product logo. The art is already a filled rounded tile, so
   the node's own chrome would only double it — the icon becomes the tile. */
.tech-node--icon {
  padding: 0;
  border: 0;
  background: none;
}
.tech-node--icon img { display: block; width: 100%; height: 100%; }
/* the shared white-tile hover would show as a rim behind an opaque icon */
.tech-node--icon:hover,
.tech-node--icon[aria-pressed="true"] { background: none; }

.stack-detail {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: 68px;
  margin-top: 4px;
  padding: 11px 13px;
  border: 1px solid rgba(255, 255, 255, .52);
  border-radius: 16px;
  background: rgba(255, 255, 255, .31);
}
.detail-dot {
  flex: 0 0 2.625rem;
  display: grid;
  place-items: center;
  width: 2.625rem; height: 2.625rem;
  /* the icon art's own rx, redrawn at 42px — same reasoning as .tech-node */
  border-radius: 10px;
  background: var(--detail-color, var(--sky-edge));
  box-shadow: inset 0 1px 0 rgba(255, 255, 255, .25);
  color: var(--paper);
  font-size: 0.75rem;
  font-weight: 800;
}
/* same swap as the nodes: logo art replaces the tinted lettered badge */
.detail-dot.is-icon { background: none; box-shadow: none; }
.detail-dot img { display: block; width: 100%; height: 100%; border-radius: inherit; }
.detail-copy { min-width: 0; }
.detail-copy strong {
  display: block;
  margin-bottom: 3px;
  color: var(--hero-ink);
  font-size: 0.875rem;
}
.detail-copy span {
  display: block;
  color: var(--hero-ink-soft);
  font-size: 0.6875rem;
  line-height: 1.3;
}

/* WHERE the panel renders is decided by mode A's block at the end of this file, in the same
   place that turns the rails off -- so the two can never disagree. */

/* The panel is 486px under a 112px offset, more than a 720p laptop leaves. Scaling from the
   top-right keeps it pinned under the header. .88 = (561 - 112 - 16) / 486. */
@media (min-width: 1101px) and (max-height: 639px) {
  .tech-orbit { --orbit-scale: .88; }
}
/* Second step: .84 = (525 - 101 - 16) / 486. Scoped to <=560 so nothing at 561 and up
   changes size -- .88 still governs 561-639, where a 720p laptop lands. */
@media (min-width: 1101px) and (max-height: 560px) {
  .tech-orbit { --orbit-scale: .84; }
}
/* Third step, down to the floor: .78 = (500 - 101 - 16) / 486. Steps not a fluid ratio,
   because CSS cannot divide a length by a length. */
@media (min-width: 1101px) and (max-height: 524px) {
  .tech-orbit { --orbit-scale: .78; }
}

@media (prefers-reduced-motion: reduce) {
  .tech-node { animation: none; }
  .orbit-core::after { animation: none; opacity: 0; }
}

/* ---------- hero tech-stack rails (stacked-hero viewports) ----------
   Two vertical rails flanking the portrait, layered over the hero and out of .stage's flow
   exactly as .tech-orbit is. Gated on the hero's own stacking condition, nothing else. */

.tech-rail {
  position: absolute;
  z-index: 3;
  /* Anchored to the hero's bottom edge until the hero outgrows the fold; the second term is
     that excess, lifting the rail back into view on a short landscape viewport.
     Where the hero fits, 100% - 100svh is zero or negative and the plain inset wins. */
  bottom: max(var(--rail-inset), calc(100% - 100svh + var(--rail-inset)));
  /* on by default -- modes C and B both use rails; mode A turns them off */
  display: flex;
  flex-direction: column;
  gap: var(--rail-gap);
}
.tech-rail--left { left: clamp(6px, 2.5vw, 18px); align-items: flex-start; }
.tech-rail--right { right: clamp(6px, 2.5vw, 18px); align-items: flex-end; }
/* dashed spine behind the chips; z-index:-1 stays inside the rail's own
   stacking context, so it cannot slip behind the hero itself */
.tech-rail::before {
  content: "";
  position: absolute;
  top: 8px; bottom: 8px; left: 50%;
  z-index: -1;
  border-left: 2px dashed rgba(6, 6, 68, .22);
  transform: translateX(-50%);
}
/* zig-zag rhythm: every other chip steps inward, toward the portrait */
.tech-rail--left .chip-wrap:nth-child(even) { margin-left: 10px; }
.tech-rail--right .chip-wrap:nth-child(even) { margin-right: 10px; }

.chip-wrap {
  opacity: 0;
  transition: opacity .6s ease, transform .6s cubic-bezier(.22, 1, .36, 1);
  transition-delay: calc(var(--i, 0) * 90ms + 200ms);
}
.tech-rail--left .chip-wrap { transform: translateX(-28px); }
.tech-rail--right .chip-wrap { transform: translateX(28px); }
.tech-rail.rails-in .chip-wrap { opacity: 1; transform: translateX(0); }

.tech-chip {
  position: relative;
  display: grid;
  place-items: center;
  width: var(--chip); height: var(--chip);
  padding: 0;
  border: 1px solid rgba(255, 255, 255, .78);
  border-radius: 30%;
  background: rgba(255, 255, 255, .55);
  backdrop-filter: blur(10px);
  box-shadow: 0 10px 24px rgba(6, 6, 68, .16), inset 0 1px 0 rgba(255, 255, 255, .65);
  /* .hero keeps its light-blue identity in both themes, so the chips use the
     fixed hero ink rather than the themed --text tokens */
  color: var(--hero-ink);
  font-family: var(--font-body);
  font-size: 0.75rem;
  font-weight: 800;
  cursor: pointer;
  animation: chip-bob var(--bob, 4s) ease-in-out var(--bob-delay, 0s) infinite alternate;
}
@keyframes chip-bob { from { translate: 0 0; } to { translate: 0 -9px; } }
.tech-chip--icon { background: none; border: 0; box-shadow: none; }
.tech-chip img {
  width: 100%; height: 100%;
  border-radius: 30%;
  box-shadow: 0 10px 24px rgba(6, 6, 68, .22);
}
.tech-chip:focus-visible { outline: 2px solid var(--turban); outline-offset: 3px; }

/* name flag, above the chip so the portrait never covers it */
.tech-chip::after {
  content: attr(data-name);
  position: absolute;
  bottom: calc(100% + 9px);
  left: 50%;
  padding: .375rem .625rem;
  border-radius: 8px;
  background: var(--ink);
  color: var(--paper);
  font-size: 0.6875rem;
  font-weight: 700;
  white-space: nowrap;
  opacity: 0;
  transform: translate(-50%, 5px);
  transition: opacity .25s ease, transform .25s ease;
  pointer-events: none;
}
.tech-chip:hover::after,
.tech-chip:focus-visible::after,
.tech-chip.show::after { opacity: 1; transform: translate(-50%, 0); }

/* WHERE the rails render: modes C and B, expressed by them being display:flex in the base
   rule and mode A switching them off -- not by a query restating A's complement. */

/* HOW BIG the chips are: a different axis binds per orientation, so each gets its own ramp.
   See DESIGN.md, Hero modes. */
@media (orientation: portrait) {
  :root {
    --rail-gap: clamp(10px, 2.4vw, 15px);
    --chip: max(2.25rem, min(12.5vw, 3.75rem, var(--chip-fit)));
  }
}
/* short portrait viewports: shrink rather than crowd the portrait */
@media (orientation: portrait) and (max-height: 720px) {
  :root {
    --rail-gap: clamp(8px, 2vw, 12px);
    --chip: max(2.25rem, min(11.5vw, 3.25rem, var(--chip-fit)));
  }
}

/* The PORTRAIT floor, swept at 390 wide: copy clear at 720 and 740, overrun at 700, 680 and
   640. So 720 is where seven chips stop fitting beside him. See DESIGN.md, Hero modes. */
@media (orientation: portrait) and (max-height: 719px) {
  .tech-rail { display: none; }
}
/* Landscape -- wide but short, and a rail is a COLUMN, so height binds and vw would size off
   the wrong axis. See DESIGN.md, Hero modes. */
@media (orientation: landscape) and (max-width: 1100px),
       (orientation: landscape) and (max-height: 499px) {
  :root {
    --chip: clamp(2.25rem, 8vh, 3.5rem);
    --rail-gap: clamp(8px, 1.6vh, 14px);
    /* What a rail costs the copy: its inset, its width, and 14px of air. Only landscape
       declares it -- there the rails sit beside the copy. Portrait stacks them below it,
       next to the sprite. */
    --rail-gutter: calc(clamp(6px, 2.5vw, 18px) + var(--chip) + 14px);
  }
}

/* The landscape floor, measured, and it must come last because zeroing the gutter is half of
   it: the h1 holds two lines to 460, three at 440, five at 320. See DESIGN.md, Hero modes. */
@media (orientation: landscape) and (max-width: 459px) {
  .tech-rail { display: none; }
  :root { --rail-gutter: 0px; }
}

@media (prefers-reduced-motion: reduce) {
  .tech-chip { animation: none; }
  .chip-wrap { opacity: 1; transform: none; transition: none; }
}

/* ---------- shared section layout ---------- */

section:not(.hero) { padding: var(--section-pad) 0; }
.section-inner {
  max-width: var(--content-width);
  margin: 0 auto;
  padding: 0 var(--side-pad);
}
/* .svc-display's metrics at section scale. Left at `section h2` (0,0,2) deliberately:
   `section:not(.hero)` is (0,1,2) and would outrank .svc-display--sm (0,1,0) and shrink the
   services heading. */
/* .gallery-head h1 rides along here rather than the generic h1 rule: hobbies.html needs a
   real h1 for the outline, but it is a section heading, not a hero line. (0,1,1) beats
   (0,0,1). */
section h2,
.gallery-head h1,
.site-footer h2 {
  font-size: clamp(1.75rem, 3.4vw, 2.75rem);
  font-weight: 800;
  /* .92, the same leading as .svc-display -- at .98 a section heading sat looser than the
     services display line at identical size and tracking. */
  line-height: .92;
  letter-spacing: -.035em;
  margin: 6px 0 28px;
  max-width: 640px;
  text-wrap: balance;
}
/* the accented tail, same device as .svc-display em */
section h2 em,
.gallery-head h1 em,
.site-footer h2 em { font-style: normal; color: var(--accent-ink); }
/* The footer is an inverted navy surface in BOTH themes, so a themed token is wrong:
   --accent-ink resolves to raw --turban in light and measures 2.78:1 on See DESIGN.md, Color
   palette. */
.site-footer h2 em { color: var(--sky-core); }
.lead {
  font-size: 1.03125rem;
  line-height: 1.6;
  color: var(--text-soft);
  max-width: 680px;
  margin-bottom: 18px;
}
.lead:last-child { margin-bottom: 0; }

.about { background: var(--bg); }
.skills { background: var(--bg-alt); }
.experience { background: var(--bg); }

/* Heading left, availability right. flex-wrap not grid: the two halves have nothing to
   align to each other, they just share a line while both fit. */
.section-head {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: 20px clamp(24px, 4vw, 56px);
  margin-bottom: clamp(26px, 3vw, 40px);
}
.section-head > div:first-child { flex: 1 1 360px; }
.section-head h2 { margin-bottom: 0; }
.service-note {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 12px;
  /* Grow with a max-width rather than grow:0 -- on its own line it then fills the row
     instead of stranding at 260px, and the cap stops it eating the heading's space. */
  flex: 1 1 clamp(260px, 24vw, 340px);
  max-width: 340px;
  font-size: 0.84375rem;
  line-height: 1.5;
  color: var(--text-soft);
}
.service-note strong {
  display: block;
  color: var(--text);
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  font-weight: 600;
  letter-spacing: .1em;
  text-transform: uppercase;
  margin-bottom: 4px;
}
/* A live-status dot in brand blue, not the conventional green: the palette runs one accent
   plus the --sky ramp. See DESIGN.md. */
.status-dot {
  position: relative;
  display: inline-block;
  width: 8px;
  height: 8px;
  margin-top: 5px;
  border-radius: 50%;
  background: var(--sky-deep);
}
.status-dot::after {
  content: "";
  position: absolute;
  inset: -5px;
  border: 1px solid var(--sky-deep);
  border-radius: inherit;
  animation: status-pulse 2.4s ease-out infinite;
}
@keyframes status-pulse {
  from { opacity: .6; transform: scale(.75); }
  80%, to { opacity: 0; transform: scale(1.5); }
}

.projects { background: var(--bg-alt); }
.open-to { background: var(--bg-alt); }
/* The resume button rides .section-head's space-between to .section-inner's right edge --
   the same edge --content-width gives .header-inner, so it lands under the nav CTA. */
.open-to-actions { margin-top: 0; }
.open-to-actions .btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  border-radius: 999px;
}
.open-to-actions .btn svg { width: 16px; height: 16px; }
/* the address and its copy control share a line inside the Contact cell */
.fact-contact { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }
/* 24px floor, as .footer-links a. This one measured 160x14 -- the email
   address in the band a recruiter reads first. */
.fact-contact a {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  color: var(--accent-ink);
  overflow-wrap: anywhere;
}

/* Name/value pairs, not cards: a recruiter reads answers, and this survives into the print CV
   as its header block. See DESIGN.md, Print. */
.facts {
  margin-top: 32px;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(max(min(260px, 100%), (100% - 2px) / 3), 1fr));
  gap: 1px;
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}
.facts > div { padding: 18px 20px; background: var(--bg); box-shadow: 0 0 0 1px var(--border); }
.facts dt {
  font-family: var(--font-mono);
  font-size: 0.5625rem;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.facts dd { margin-top: 8px; font-size: 0.875rem; line-height: 1.5; }

/* Screen-hidden, print-only. Its display:none keeps it out of the
   accessibility tree here, which is why it carries no aria-hidden. */
.print-head { display: none; }

/* ---------- cards ---------- */

/* Grid, not flex: flex-basis + max-width let the last row's cards take a different width
   from the rows above. auto-fit columns are always equal. */
.card-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr));
  gap: 1px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}
/* The ring-in-the-gap device: the 1px spread sits in the 1px gap and overlaps its
   neighbour's, so an interior rule is one pixel and never two.
   A row that runs out of cards leaves --surface rather than a block of border colour. */
.card { box-shadow: 0 0 0 1px var(--border); }
.card {
  /* stretch, not the old align-items:start -- on a ruled grid a short card
     ends mid-cell and leaves the hairline running past it into nothing */
  position: relative;
  display: flex;
  flex-direction: column;
  padding: var(--card-pad);
  background: var(--surface);
  transition: background .3s var(--ease);
}
/* Numbered meta row above the title: the index gives five otherwise identical
   cards a reading order, which is the whole reason the row exists. */
.card-meta {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 10px;
  font-family: var(--font-mono);
  font-size: 0.625rem;
  font-weight: 500;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.card-index { color: var(--accent-ink); }
/* Grouped with .svc-card-title rather than restated -- a card title is a card title on
   every page, and one declaration is what stops the two drifting. */
.card h3,
.svc-card-title {
  font-size: clamp(1.625rem, 2.4vw, 2.125rem);
  line-height: 1;
}
.card h3 {
  font-weight: 700;
  margin-bottom: 14px;
}
.card h3 em,
.svc-card-title em { font-style: normal; color: var(--accent-ink); }
/* Card foot pushed to the bottom of the flex column so two cards' feet line up whatever
   their copy length. Ties .card-meta at (0,1,0) and wins on order. */
.project-foot {
  margin-top: auto;
  margin-bottom: 0;
  padding-top: 26px;
}
/* Same 24px floor and the same border-to-underline swap as the hero row
   above: these measured 44x17. */
.project-foot a {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  color: var(--text);
  white-space: nowrap;
  text-decoration: underline;
  text-decoration-color: var(--border);
  text-decoration-thickness: 1px;
  text-underline-offset: 4px;
  transition: color .2s ease, text-decoration-color .2s ease;
}
.project-foot a:focus-visible { outline: 2px solid var(--turban); outline-offset: 3px; }

/* ---------- proof-of-work disclosure (project cards) ----------
   The eval trace behind the number a card claims, in a native <details> so nothing lands in
   js/main.js. See DESIGN.md, Print. */
.proof { margin-top: 18px; }
.proof > summary {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  /* 24px floor; measured 301x22 */
  min-height: 24px;
  gap: 8px;
  cursor: pointer;
  list-style: none;
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text-soft);
  transition: color .2s ease;
}
/* Safari paints the triangle without this; list-style alone does not reach it */
.proof > summary::-webkit-details-marker { display: none; }
.proof > summary::before {
  content: "+";
  display: grid;
  place-items: center;
  width: 1.25rem; height: 1.25rem;
  flex: none;
  border: 1px solid var(--border);
  border-radius: 6px;
  font-size: .8125rem;
  line-height: 1;
  transition: transform .3s var(--ease), color .2s ease, border-color .2s ease;
}
.proof[open] > summary::before { transform: rotate(45deg); }
.proof > summary:hover { color: var(--accent-ink); }
.proof > summary:hover::before { color: var(--accent-ink); border-color: var(--accent-ink); }
.proof > summary:focus-visible { outline: 2px solid var(--turban); outline-offset: 4px; }
.proof-hint { letter-spacing: .1em; opacity: .7; }
/* The trace runs wider than any card at any breakpoint, so it scrolls in its
   own box rather than widening the card or the page. */
.proof-trace {
  margin-top: 12px;
  padding: 12px 14px;
  overflow-x: auto;
  background: var(--bg-alt);
  border: 1px solid var(--border-soft);
  border-radius: 10px;
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  line-height: 1.7;
  color: var(--text-soft);
  /* the rows are aligned by hand and must not re-wrap */
  white-space: pre;
  -webkit-overflow-scrolling: touch;
}
.proof-note {
  margin-top: 10px;
  font-size: 0.8125rem;
  line-height: 1.55;
  color: var(--text-soft);
}
.card ul { list-style: none; }
.card li {
  padding: .375rem 0;
  color: var(--text-soft);
  font-size: 0.9375rem;
  border-top: 1px solid var(--border-soft);
}
.card li:first-child { border-top: none; }

/* Was .project-card p, which left the five service teaser cards falling through to the
   16.5px body size. Same declaration as .svc-card-desc. */
.card p:not(.card-meta),
.svc-card-desc {
  margin-top: 14px;
  max-width: 34ch;
  font-size: 0.9375rem;
  line-height: 1.5;
  color: var(--text-soft);
}

/* The teaser card's mark, inline SVG rather than a file -- no request, no asset, and it
   inherits currentColor so it themes with everything else. */
.card-icon {
  display: grid;
  place-items: center;
  width: 2.75rem;
  height: 2.75rem;
  flex: 0 0 auto;
  /* .card is a flex column, so without this the mark stretches the full width */
  align-self: flex-start;
  margin-bottom: 22px;
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--text-soft);
}
.card-icon svg { width: 20px; height: 20px; }

/* Cover link: the whole teaser card opens that service's rate card via the #id deep link.
   An <a>, not a button, so middle-click and open-in-new-tab work for free. */
.card-link { position: absolute; inset: 0; }
.card-link:focus-visible { outline: 2px solid var(--turban); outline-offset: -3px; }

/* The sixth cell is a link, not a service, and always spans the full row: six items do not
   divide evenly by every column count auto-fit can pick, and grid See DESIGN.md, Layout
   conventions. */
.card-cta {
  grid-column: 1 / -1;
  align-items: center;
  justify-content: center;
  gap: 16px;
  text-align: center;
  color: inherit;
  text-decoration: none;
}
/* Small on purpose: this cell is a way out of the grid, not a sixth service, so it takes
   the mono micro-label voice. Ties .card h3 at (0,1,1) and wins on order. */
.card-cta h3 {
  margin-bottom: 0;
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: .14em;
  text-transform: uppercase;
  line-height: 1.5;
}
.card-cta-arrow {
  display: grid;
  place-items: center;
  width: 2.25rem;
  height: 2.25rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--text-soft);
  transition: background .35s var(--ease), color .35s var(--ease),
              transform .35s var(--ease), border-color .35s var(--ease);
}
.card-cta-arrow svg { width: 14px; height: 14px; }
.card-cta:focus-visible { outline: 2px solid var(--turban); outline-offset: -3px; }

/* The min track is at least a third of the row so auto-fit can never fit a fourth column, and
   the CTA takes an ordinary cell: at 1440 and 1600 it chose four See DESIGN.md, Layout
   conventions. */
#services .card-grid {
  grid-template-columns: repeat(auto-fit, minmax(max(min(260px, 100%), (100% - 2px) / 3), 1fr));
}
#services .card-cta { grid-column: auto; }

/* ---------- placeholder blocks ---------- */

.placeholder-block {
  background: repeating-linear-gradient(
    135deg,
    var(--placeholder-a),
    var(--placeholder-a) 10px,
    var(--placeholder-b) 10px,
    var(--placeholder-b) 20px
  );
  border: 1px dashed var(--placeholder-border);
  border-radius: 8px;
}
/* Wide, not the square other marks use: these are three real lockups. Wipro is 1.27:1 and
   LTIMindtree 5.26:1, and a square box shrinks both to a smudge. White in both themes. */
.timeline-mark {
  width: 6.5rem;
  height: 3.25rem;
  flex: 0 0 auto;
  padding: 6px;
  border-radius: 8px;
  background: #fff;
}
/* height:100% needs a definite containing block, so the chip stays a plain block -- as a
   centred grid item the percentage falls back to auto and Wipro's art overflows. */
.timeline-mark img {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}

/* ---------- photo gallery (hobbies.html) ----------
   Bento layout, four tracks, dense flow, re-expressed in the site's theme tokens so it works
   in both. See DESIGN.md, Layout conventions. */

/* The gallery before there are photographs, dashed in the site's existing "not yet" language.
   margin-top must be stated here: .gallery-head h1 zeroes `section h2`'s margin, which left
   this flush against the heading. The value is .filters' own, so the two open the same gap. */
.gallery-soon {
  display: grid;
  justify-items: center;
  gap: 12px;
  margin-top: clamp(24px, 3vw, 34px);
  padding: clamp(48px, 9vw, 96px) var(--card-pad);
  text-align: center;
  background: var(--surface);
  border: 1px dashed var(--border);
  border-radius: var(--radius);
}
.gallery-soon .eyebrow { margin-bottom: 0; }
/* This was an h3 until the page got a real h1. As an h2 it started matching `section h2`
   and inherited a 28px margin, a 640px measure and tighter tracking -- the three resets
   below undo exactly that. */
.gallery-soon h2 {
  font-size: clamp(1.625rem, 2.4vw, 2.125rem);
  font-weight: 700;
  line-height: 1.05;
  text-wrap: balance;
  margin: 0;
  max-width: none;
  letter-spacing: -.02em;
}
.gallery-soon h2 em { font-style: normal; color: var(--accent-ink); }
.gallery-soon p {
  max-width: 44ch;
  font-size: 0.9375rem;
  line-height: 1.5;
  color: var(--text-soft);
}

.gallery-head {
  display: flex;
  justify-content: space-between;
  align-items: flex-end;
  gap: clamp(20px, 3vw, 36px);
  flex-wrap: wrap;
}
.gallery-head h1 { margin-bottom: 0; }
.gallery-note {
  max-width: 330px;
  color: var(--text-soft);
  font-size: 0.875rem;
  line-height: 1.65;
}
.gallery-stat {
  display: flex;
  gap: 28px;
  margin-top: 14px;
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: .18em;
  text-transform: uppercase;
  color: var(--text);
}
.gallery-stat b { color: var(--turban); font-weight: 600; margin-right: 6px; }

/* sticks under the fixed site header, not under the viewport top */
.filters {
  position: sticky;
  top: var(--header-h);
  z-index: 10;
  display: flex;
  gap: 10px;
  flex-wrap: wrap;
  align-items: center;
  /* Zoom: five wrapped chips are ~5 rows tall at 320 CSS px, which at 400% is most of the
     screen. Cap the bar and scroll it so the gallery is never pushed out of view. */
  max-height: 50svh;
  overflow-y: auto;
  margin: clamp(24px, 3vw, 34px) calc(var(--side-pad) * -1) 0;
  padding: 14px var(--side-pad);
  background: var(--header-bg);
  backdrop-filter: blur(12px);
  border-top: 1px solid var(--border);
  border-bottom: 1px solid var(--border);
}
.chip {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--text-soft);
  font-family: inherit;
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: .14em;
  text-transform: uppercase;
  padding: .625rem clamp(.875rem, 1.4vw, 1.125rem);
  border-radius: 100px;
  cursor: pointer;
  display: inline-flex;
  align-items: baseline;
  gap: 7px;
  transition: border-color .35s var(--ease), color .35s var(--ease),
              background .35s var(--ease), transform .35s var(--ease);
}
.chip sup { font-size: 0.53125rem; opacity: .6; letter-spacing: .05em; }
.chip[aria-pressed="true"] {
  background: var(--turban);
  border-color: var(--turban);
  color: var(--paper);
  font-weight: 600;
}
.chip:focus-visible { outline: 2px solid var(--turban); outline-offset: 2px; }

.gallery {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  grid-auto-rows: clamp(200px, 22vw, 280px);
  grid-auto-flow: dense;
  gap: clamp(10px, 1.1vw, 14px);
  padding: clamp(24px, 3vw, 34px) 0 26px;
}
.shot-2x2 { grid-column: span 2; grid-row: span 2; }
.shot-1x2 { grid-row: span 2; }
.shot-2x1 { grid-column: span 2; }

/* a button, not the reference's <article>: the tile opens a viewer, so it has
   to be reachable and operable from the keyboard for free */
.shot {
  position: relative;
  overflow: hidden;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: var(--surface);
  padding: 0;
  font: inherit;
  color: inherit;
  text-align: left;
  cursor: pointer;
  transition: box-shadow .45s var(--ease);
}
.shot:hover { box-shadow: 0 26px 70px -26px var(--shadow-lg); }
.shot:focus-visible { outline: 2px solid var(--turban); outline-offset: 3px; }

/* Reveal is opt-in: JS adds .js-reveal before observing, so with JS off every tile is
   simply visible. Hiding in CSS and showing from script leaves an empty page when the
   script does not run. */
.gallery.js-reveal .shot {
  opacity: 0;
  transform: translateY(46px) scale(.97);
  transition: opacity .9s var(--ease) var(--reveal-delay, 0ms),
              transform .9s var(--ease) var(--reveal-delay, 0ms),
              box-shadow .45s var(--ease);
}
.gallery.js-reveal .shot.reveal { opacity: 1; transform: none; }
.gallery.js-reveal .shot.hide { opacity: 0; transform: scale(.92); pointer-events: none; }

.shot-media {
  width: 100%;
  height: 100%;
  display: block;
  border: 0;
  border-radius: 0;
  object-fit: cover;
  transform: scale(1.02);
  transition: transform 1.1s var(--ease);
}
.shot:hover .shot-media { transform: scale(1.08); }

.shot::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background: linear-gradient(to top, rgba(6, 6, 68, .88) 0%, rgba(6, 6, 68, .28) 46%, transparent 72%);
  opacity: 0;
  transition: opacity .5s var(--ease);
}
.shot:hover::after, .shot:focus-visible::after { opacity: 1; }

.shot-arrow {
  position: absolute;
  top: 14px; right: 14px;
  width: 40px; height: 40px;
  border-radius: 50%;
  z-index: 2;
  background: rgba(6, 6, 68, .5);
  backdrop-filter: blur(8px);
  display: grid;
  place-items: center;
  opacity: 0;
  transform: translateY(-8px) scale(.8);
  transition: opacity .5s var(--ease) .06s, transform .5s var(--ease) .06s;
}
.shot-arrow svg { width: 15px; height: 15px; stroke: var(--paper); }
.shot:hover .shot-arrow, .shot:focus-visible .shot-arrow { opacity: 1; transform: none; }

.shot-meta {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  z-index: 2;
  padding: 18px 20px;
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: 14px;
  opacity: 0;
  transform: translateY(12px);
  transition: opacity .55s var(--ease) .04s, transform .55s var(--ease) .04s;
}
.shot:hover .shot-meta, .shot:focus-visible .shot-meta { opacity: 1; transform: none; }
.shot-cat {
  display: block;
  font-size: 0.59375rem;
  font-weight: 600;
  letter-spacing: .24em;
  text-transform: uppercase;
  color: var(--sky-core);
  margin-bottom: 7px;
}
.shot-meta h3 {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(0.875rem, 1.35vw, 1.1875rem);
  line-height: 1.15;
  color: var(--paper);
}
.shot-year { font-size: 0.6875rem; letter-spacing: .12em; color: rgba(242, 247, 253, .6); white-space: nowrap; }

/* touch devices get no hover, so the caption stays legible there */
@media (hover: none) {
  .shot::after { opacity: 1; background: linear-gradient(to top, rgba(6, 6, 68, .84), transparent 58%); }
  .shot-meta { opacity: 1; transform: none; }
  .shot-arrow { display: none; }
}

.gallery-hint {
  text-align: center;
  color: var(--text-soft);
  font-size: 0.6875rem;
  letter-spacing: .18em;
  text-transform: uppercase;
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 12px;
  flex-wrap: wrap;
}
.gallery-hint kbd {
  border: 1px solid var(--border);
  border-radius: 5px;
  padding: .1875rem .4375rem;
  font-family: inherit;
  font-size: 0.625rem;
  color: var(--text);
}

/* ---------- lightbox ---------- */

.lightbox {
  position: fixed;
  inset: 0;
  z-index: 1000;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: clamp(14px, 4vw, 44px);
  background: rgba(6, 6, 68, .88);
  backdrop-filter: blur(18px) saturate(120%);
  opacity: 0;
  visibility: hidden;
  /* visibility is discrete, so it is delayed only on the way OUT and flips instantly on the
     way IN -- a symmetric transition leaves the dialog unfocusable for 450ms and swallows
     the focus move. */
  transition: opacity .45s var(--ease), visibility 0s linear .45s;
}
.lightbox.open {
  opacity: 1;
  visibility: visible;
  transition: opacity .45s var(--ease), visibility 0s;
}
.lb-fig {
  max-width: min(1060px, 100%);
  width: 100%;
  /* At 400% zoom the figure outgrows the viewport and align-items:center would clip both
     ends. Scrolling the figure, not the backdrop, keeps .lb-close and the arrows pinned --
     .lightbox has a backdrop-filter and is their containing block. */
  max-height: 100%;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  gap: 18px;
  transform: scale(.96) translateY(14px);
  transition: transform .55s var(--ease);
}
.lightbox.open .lb-fig { transform: none; }
.lb-frame {
  height: clamp(280px, 66vh, 720px);
  flex: 0 0 auto;
  border-radius: 14px;
  overflow: hidden;
  background: var(--surface);
  border: 1px solid var(--border);
  display: flex;
  align-items: center;
  justify-content: center;
}
.lb-frame img, .lb-frame .placeholder-block {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
  border-radius: 0;
  border: 0;
}
.lb-cap { display: flex; justify-content: space-between; align-items: flex-end; gap: 18px; }
.lb-cap h3 {
  font-family: var(--font-display);
  font-weight: 700;
  font-size: clamp(1.0625rem, 2.3vw, 1.5625rem);
  color: var(--paper);
}
.lb-count { font-size: 0.75rem; letter-spacing: .2em; color: rgba(242, 247, 253, .7); white-space: nowrap; }
.lb-btn {
  position: absolute;
  z-index: 2;
  width: 48px; height: 48px;
  border-radius: 50%;
  display: grid;
  place-items: center;
  cursor: pointer;
  border: 1px solid rgba(242, 247, 253, .22);
  background: rgba(6, 6, 68, .55);
  backdrop-filter: blur(10px);
  color: var(--paper);
  transition: background .3s var(--ease), border-color .3s var(--ease),
              transform .3s var(--ease);
}
.lb-btn svg { width: 18px; height: 18px; stroke: currentColor; }
.lb-btn:hover { background: var(--turban); border-color: var(--turban); }
.lb-btn:focus-visible { outline: 2px solid var(--paper); outline-offset: 2px; }
.lb-close { top: clamp(14px, 3vw, 30px); right: clamp(14px, 3vw, 30px); }
.lb-prev { left: clamp(10px, 2.2vw, 30px); top: 50%; margin-top: -24px; }
.lb-next { right: clamp(10px, 2.2vw, 30px); top: 50%; margin-top: -24px; }

/* Measured: with four tracks the narrowest tile bottoms out at 209px just before this
   boundary. Two tracks hold to 560, where one takes over and the spans stop meaning
   anything. */
@media (max-width: 1023px) {
  .gallery { grid-template-columns: repeat(2, 1fr); grid-auto-rows: clamp(190px, 34vw, 260px); }
  .gallery-head { flex-direction: column; align-items: flex-start; }
}
@media (max-width: 560px) {
  .gallery { grid-template-columns: 1fr; grid-auto-rows: clamp(220px, 58vw, 300px); }
  .shot-2x2, .shot-1x2, .shot-2x1 { grid-column: auto; grid-row: auto; }
  .lb-prev { left: 8px; }
  .lb-next { right: 8px; }
  .lb-btn { width: 42px; height: 42px; }
  .lb-prev, .lb-next { margin-top: -21px; }
  .lb-cap { flex-direction: column; align-items: flex-start; gap: 8px; }
}

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

.timeline { list-style: none; }
.timeline-item {
  display: flex;
  gap: 20px;
  align-items: flex-start;
  padding: 20px 0;
  border-top: 1px solid var(--border);
}
.timeline-item:first-child { border-top: none; }
.timeline-body h3 { font-size: 1.1875rem; font-weight: 700; }
.timeline-body .role {
  margin-top: 6px;
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--text-soft);
}

/* Each role expands to its bullets. <details>/<summary> carries the toggle, keyboard
   handling and state natively. The mark is a "+" that rotates 45deg into an "x" -- one
   pseudo-element, no SVG. */
.timeline-body { flex: 1; min-width: 0; }
.timeline-body > summary {
  position: relative;
  /* clears the mark; the box is 1.75rem and 20px is the timeline's own gap */
  padding-right: calc(1.75rem + 20px);
  cursor: pointer;
  list-style: none;
}
/* Safari still paints the disclosure triangle without this, list-style alone
   does not reach it */
.timeline-body > summary::-webkit-details-marker { display: none; }
.timeline-body > summary:focus-visible { outline: 2px solid var(--turban); outline-offset: 4px; }
.timeline-body > summary::after {
  content: "+";
  position: absolute;
  top: 50%;
  right: 0;
  display: grid;
  place-items: center;
  width: 1.75rem;
  height: 1.75rem;
  border: 1px solid var(--border);
  border-radius: 8px;
  font-family: var(--font-mono);
  font-size: 1rem;
  line-height: 1;
  color: var(--text-soft);
  transform: translateY(-50%);
  transition: transform .3s var(--ease), color .2s ease, border-color .2s ease;
}
.timeline-body[open] > summary::after { transform: translateY(-50%) rotate(45deg); }
.timeline-body > summary:hover::after {
  color: var(--accent-ink);
  border-color: var(--accent-ink);
}
/* .role is a <span> here, not the <p> it is elsewhere -- <summary> takes
   phrasing content plus one heading, and a <p> inside it is invalid */
.timeline-body > summary .role,
.timeline-dates { display: block; }
.timeline-dates {
  margin-top: 5px;
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: .06em;
  color: var(--text-soft);
}
.timeline-detail { list-style: none; margin-top: 16px; }
.timeline-detail li {
  position: relative;
  padding: .375rem 0 .375rem 20px;
  max-width: 68ch;
  font-size: 0.9375rem;
  line-height: 1.55;
  color: var(--text-soft);
}
/* the hero eyebrow's dash, at list scale */
.timeline-detail li::before {
  content: "";
  position: absolute;
  top: 1em;
  left: 0;
  width: 10px;
  height: 2px;
  background: var(--turban);
}

/* The certificate inside the open <details>, linking to the file it shows so the browser's
   own image view is the lightbox -- no JS on a page that loads neither See DESIGN.md, Color
   palette. */
.cert-shot {
  display: inline-block;
  max-width: 100%;
  margin-top: 16px;
  border: 1px solid var(--border);
  border-radius: 10px;
  overflow: hidden;
  background: #fff;
  transition: border-color .2s ease;
}
.cert-shot img {
  display: block;
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 320px;
}
.cert-shot:hover { border-color: var(--accent-ink); }
.cert-shot:focus-visible { outline: 2px solid var(--turban); outline-offset: 3px; }

/* Measured: the role line holds at two lines to 420px, three at 390 and four at 360, where
   the centred toggle stops sitting beside the title. Below that the mark gives back 32px
   and the toggle pins top. */
@media (max-width: 419px) {
  .timeline-item { gap: 14px; }
  .timeline-mark { width: 4.5rem; height: 2.25rem; padding: 4px; }
  .timeline-body > summary { padding-right: calc(1.75rem + 12px); }
  .timeline-body > summary::after { top: 0; transform: none; }
  .timeline-body[open] > summary::after { transform: rotate(45deg); }
}

/* ---------- buttons ---------- */

.btn {
  display: inline-block;
  padding: .75rem clamp(1.125rem, 2vw, 1.375rem);
  border-radius: 6px;
  font-weight: 600;
  font-size: 0.90625rem;
  text-decoration: none;
  transition: background .2s ease, color .2s ease, border-color .2s ease;
}
.btn-primary {
  background: var(--turban);
  color: var(--paper);
}
.btn-secondary {
  background: transparent;
  color: var(--feature-text);
  border: 1px solid var(--feature-border);
}
.btn:focus-visible { outline: 2px solid var(--turban); outline-offset: 3px; }

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

/* Full-bleed navy. The inner wrapper is the same .section-inner every section uses, so the
   footer's copy lands on the same edges as the copy above it. */
.site-footer {
  background: var(--feature-bg);
  color: var(--feature-text);
  padding: var(--section-pad) 0;
}
/* Nested auto-fit, not one flat grid: the brand block and a nav column have minimums nearly
   3x apart and one grid can carry only one floor. See DESIGN.md, Layout conventions. */
/* max-content, not 1fr: 1fr split the row into equal tracks and left both blocks adrift
   inside oversized shares, putting a second gap between the nav's own two See DESIGN.md,
   Layout conventions. */
.footer-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), max-content));
  justify-content: space-between;
  gap: clamp(26px, 3vw, 40px);
  margin-top: clamp(30px, 3.4vw, 44px);
  padding-top: clamp(26px, 3vw, 40px);
  border-top: 1px solid rgba(242, 247, 253, .12);
}
.footer-nav {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(100px, 100%), 1fr));
  gap: clamp(18px, 2vw, 32px);
}
/* Heading left, actions right while both clear 320px. Same max-content + justify-content
   reasoning as .footer-grid: the actions size to their content and ride the leftover space
   right. */
.footer-hero {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), max-content));
  justify-content: space-between;
  gap: clamp(20px, 2.4vw, 40px);
  align-items: end;
}
.site-footer .eyebrow { color: var(--sky-core); }
.site-footer h2 { color: var(--feature-text); margin-bottom: 16px; }
.site-footer .lead { color: var(--feature-soft); margin-bottom: 0; }
/* .row's 26px top margin spaces it under hero copy; here the grid gap does */
.footer-hero .row { margin-top: 0; }

/* Exactly ONE button here -- the resume -- and everything below descends in weight. Four
   equal pills wrapped 2+2 read as a broken grid; two stacked pills of different widths read
   as a mistake. */
.footer-mail {
  margin-top: 18px;
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 0.9375rem;
  font-weight: 600;
}
.footer-mail-icon { width: 17px; height: 17px; flex: none; color: var(--feature-soft); }
.footer-mail a {
  /* 24px floor, as .footer-links a beside it; measured 162x21 */
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  color: var(--feature-text);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  /* the address is the longest unbreakable string in the footer */
  overflow-wrap: anywhere;
}
/* Used on TWO grounds: the themed Open-to-work band and the always-navy footer. Base is
   themed tokens, footer overrides. Styling it for the footer alone made "Get in touch"
   invisible on the band. */
.copy-btn {
  flex: none;
  display: grid;
  place-items: center;
  width: 30px; height: 30px;
  border-radius: 6px;
  border: 1px solid var(--border);
  background: none;
  color: var(--text-soft);
  cursor: pointer;
  transition: background .2s ease, color .2s ease, border-color .2s ease;
}
.copy-btn svg { width: 15px; height: 15px; }
.copy-btn:hover { background: var(--bg-alt); color: var(--text); border-color: var(--text-soft); }
.copy-btn:focus-visible { outline: 2px solid var(--turban); outline-offset: 3px; }
.copy-status {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--accent-ink);
}
.site-footer .copy-btn { border-color: rgba(255, 255, 255, .28); color: var(--feature-soft); }
.site-footer .copy-btn:hover {
  background: rgba(255, 255, 255, .1);
  color: var(--feature-text);
  border-color: var(--feature-text);
}
.site-footer .copy-btn:focus-visible { outline-color: var(--paper); }
.site-footer .copy-status { color: var(--sky-core); }

.footer-links {
  margin-top: 12px;
  display: flex;
  flex-wrap: wrap;
  gap: 6px 20px;
  font-size: 0.8125rem;
  font-weight: 600;
}
/* inline-flex + min-height and text-decoration, not padding-bottom and a border: a 19px
   line of type is under the 24px touch minimum, and a border-bottom peels off the word as
   the box grows. */
.footer-links a {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  color: var(--feature-soft);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  transition: color .2s ease;
}
.footer-links a:hover { color: var(--feature-text); }
.footer-links a:focus-visible { outline: 2px solid var(--paper); outline-offset: 4px; }
.footer-logo {
  display: block;
  width: auto;
  height: 2.5rem;
  opacity: .92;
}
.footer-brand p {
  font-size: 0.875rem;
  line-height: 1.6;
  color: var(--feature-soft);
  max-width: 34ch;
  margin: 16px 0 0;
}
/* padding and font-size stay .btn's: overriding them to 10px/14px rendered a
   37px pill, under the 44px touch target every other button here meets */
.footer-cta {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  border-radius: 999px;
}
.footer-cta svg { width: 16px; height: 16px; }
.footer-col h3 {
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--feature-soft);
  margin-bottom: 16px;
}
.footer-col ul { list-style: none; display: grid; gap: 11px; }
/* 24px floor, as .footer-links a. See DESIGN.md, Color palette. */
.footer-col a {
  display: inline-flex;
  align-items: center;
  min-height: 24px;
  font-size: 0.90625rem;
  color: var(--feature-soft);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  text-decoration-color: transparent;
  transition: color .2s ease, text-decoration-color .2s ease;
}
.footer-col a:focus-visible { outline: 2px solid var(--paper); outline-offset: 3px; }
.footer-bottom {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 8px 20px;
  margin-top: clamp(26px, 3vw, 36px);
  padding-top: clamp(18px, 2vw, 24px);
  border-top: 1px solid rgba(242, 247, 253, .12);
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: .08em;
  color: var(--feature-soft);
}

/* ---------- responsive ---------- */

/* mouse/trackpad only: keeps hover styles from "sticking" after a tap
   on touchscreens (phones, tablets, touch laptops) */
@media (hover: hover) and (pointer: fine) {
  /* the nav links lost their underline for the sliding marker, so hover is a
     colour change now and the marker itself is moved by js/main.js */
  .primary-nav a:hover { color: var(--text); }
  .nav-cta:hover { background: var(--turban-hover); transform: translateY(-1px); }
  .nav-cta:hover svg { transform: translate(2px, -2px); }
  .nav-overlay ul a:hover { color: var(--accent-ink); }
  .nav-overlay-foot a:hover { color: var(--accent-ink); }
  .row a:not(.btn):hover { text-decoration-color: var(--turban); }
  /* Was translateY(-4px) plus a shadow. On the ruled grid a lifting card pulls away from
     its own hairlines and drops a shadow across its neighbours; the tint does the same job. */
  .card:hover { background: var(--bg-alt); }
  .card-cta:hover .card-cta-arrow {
    background: var(--accent-ink);
    border-color: var(--accent-ink);
    color: var(--bg);
    transform: translateX(3px);
  }
  .project-foot a:hover { color: var(--accent-ink); text-decoration-color: var(--accent-ink); }
  .btn-primary:hover { background: var(--turban-hover); }
  .btn-secondary:hover { border-color: var(--feature-text); }
  .footer-col a:hover { color: var(--feature-text); text-decoration-color: var(--turban); }
}

/* Stacked hero, raised from 880px to 1100px: between 881 and 1100 the side-by-side copy
   landed on the subject and the column squeezed to ~335px. The height clause catches phones
   in landscape. */
/* The chip is no longer hidden below 1400px. See DESIGN.md. */

/* The bar collapses on WIDTH alone. Whether the row fits is a purely horizontal question;
   tying it to the hero's condition cost a wide, short window its nav for no reason. */
@media (max-width: 1100px) {
  /* The bar keeps only the brand and the burger. Two columns, not three -- an empty 1fr
     would stretch the row's middle. */
  .header-inner { grid-template-columns: auto auto; justify-content: space-between; }
  .nav-toggle { display: block; }
  .primary-nav, .nav-status, .nav-cta { display: none; }
}

/* ---------- hero mode B: the split ----------
   Second of the three: tablets both ways, and every laptop zoom step from ~115% down. See
   DESIGN.md, Hero modes. */
@media (min-aspect-ratio: 1/1) and (min-width: 700px) {
  /* The rails flank the grid here in BOTH orientations, so the gutter is not
     the landscape-only reservation it is for the stacked hero. */
  :root {
    --chip: clamp(2.25rem, 7vh, 3.25rem);
    --rail-gap: clamp(8px, 1.6vh, 14px);
    --rail-gutter: calc(clamp(6px, 2vw, 18px) + var(--chip) + 14px);

    /* 62svh. The 68vw fit cap still binds on a portrait tablet and holds him near 28% there
       -- unavoidable, since the copy owns half the width. Where the cap does not bind, the
       dial governs. */
    --subject-h: 62svh;
    --frame-fit: 68vw;
    --hero-mode: "B";
  }

  .hero {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    align-items: center;
    column-gap: var(--gap);
    /* No bottom padding: the stage is bottom-anchored and its mask has no bottom fade here,
       so any gap under it exposes the sprite's baked backdrop as a hard edge. */
    padding: calc(var(--header-h) + 16px)
             calc(var(--side-pad) + var(--rail-gutter, 0px)) 0;
  }
  .copy {
    margin: 0;
    max-width: none;
    padding: 0;
    text-align: left;
  }
  /* put back what mode C centred */
  .hero .copy .eyebrow::before { display: inline-block; margin: 0 10px 0 0; }
  .copy .row { justify-content: flex-start; }

  .stage {
    justify-self: start;
    /* Bottom-anchored, as mode A does it: floating the sprite mid-column exposes its
       unfaded bottom edge as a hard horizontal seam across the gradient, very visible at
       768x1024. */
    align-self: end;
  }
}

/* ---------- hero mode A: the dominant portrait ----------
   Last of the three, so it wins wherever it matches and needs no clause excluding the two
   above. See DESIGN.md, Hero modes. */
@media (min-width: 1101px) and (min-height: 500px) {
  :root {
    /* 84svh: holds 1280x800 and 1440x900 exactly where they were (84.0%) and pulls a short
       1366x640 laptop back into line from 90.2%. On very large screens --frame-max binds
       first. */
    --subject-h: 84svh;
    --frame-fit: 96vw;
    --hero-mode: "A";
  }
  .hero {
    display: flex;
    /* Mode B is written above and applies here until overridden -- the one cost of the
       ordering. `padding` is a SHORTHAND in B, so a padding-top alone leaves its 154px sides
       and shoves the portrait off centre. */
    padding: 0;
    align-items: normal;
    column-gap: normal;
  }
  .stage {
    /* align-self back to centre: mode B pins it to `end` for its grid row, and
       in this flex column that would left-align the portrait. */
    align-self: center;
    /* .copy leaves the flow here, so the stage is the only flex item and needs its own auto
       top margin to bottom-anchor. In mode C that job belongs to .copy's `margin: auto`. */
    margin: auto 0 0;
    /* The bottom-anchored crown climbs toward the bar on a short viewport, and the bar is
       transparent at the top. See DESIGN.md, Hero modes. */
    transform: translateY(clamp(
      0px,
      calc(var(--hero-top) - (100svh - var(--subject-h))),
      24px));
  }
  .copy {
    position: absolute;
    /* clamp() covers normal widths; the calc() takes over once the portrait reaches the
       copy, holding it 170px outside the stage's left edge so the two keep one relationship
       at every viewport. */
    left: max(clamp(20px, 5.5vw, 84px), calc(50vw - var(--stage-w) / 2 - 170px));
    top: 50%;
    margin: 0;
    padding: 0;
    transform: translateY(-58%);
    /* was a flat 335px, which forced the eyebrow to wrap mid-phrase and broke
       the h1 into four short lines on anything under ~1300px */
    max-width: clamp(335px, 30vw, 440px);
    text-align: left;
  }
  .hero .copy .eyebrow::before { display: inline-block; margin: 0 10px 0 0; }
  .copy .row { justify-content: flex-start; }

  /* The one place the tech stack swaps presentation. */
  .tech-rail { display: none; }
  .tech-orbit { display: block; }
}

/* Wide but short -- laptops once browser chrome is subtracted, landscape tablets. .copy is
   centred with a -58% bias, so here it rides up under the fixed header; anchor it below the
   header instead. */
@media (min-width: 1101px) and (min-height: 500px) and (max-height: 760px) {
  .copy {
    /* --hero-top, not its own offset. These were 8px and 24px, never reconciled, so the
       eyebrow sat 16px above the tech panel's top border. They are the only two elements
       pinned under the bar. */
    top: var(--hero-top);
    transform: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .status-dot::after { animation: none; }
  .nav-status::before { animation: none; }
  /* the marker still moves -- it has to, it marks position -- but it jumps */
  .nav-marker { transition: none; }
  .nav-toggle span { transition: none; }
  /* the bar still swaps between transparent and filled -- that is state, not
     decoration -- but it swaps instantly */
  .site-header { transition: none; }
  /* the overlay hides its links behind a transform, so this must resolve to
     "visible", not merely "faster" -- same rule as the gallery reveal */
  .nav-overlay ul a { opacity: 1; transform: none; transition: none; transition-delay: 0s !important; }
  /* the gallery reveal is the only thing here that hides content behind
     motion, so it must resolve to "visible", not merely "faster" */
  .gallery.js-reveal .shot { opacity: 1; transform: none; transition: none; }
  .shot-media, .shot:hover .shot-media { transform: none; }
  .hero h1 em::after { animation: none; transform: scaleX(1); }
  .stage canvas { transition: none; }
  html { scroll-behavior: auto; }
  body { transition: none; }
}

/* ---------- services page (services.html) ----------
   Hairline-ruled cards on a 1px grid, mono micro-labels, one oversized display line, and a
   full-screen detail view over a three-tier rate card. See DESIGN.md, Layout conventions. */

.visually-hidden {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* The typography for these two now lives in the shared .eyebrow declaration at
   the top of the file -- one voice, one place. Only structure is left here. */
.svc-kicker {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 14px;
}
.svc-kicker--split { justify-content: space-between; }
.svc-kicker--rule { margin-top: 22px; }
.svc-rule { width: 32px; height: 1px; background: var(--border); }
.svc-kicker-note { opacity: .7; }
.svc-label { display: block; margin-bottom: 14px; }

/* pill: the reference's single most-repeated object */
.svc-pill {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  padding: .3125rem .6875rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: transparent;
  font-family: var(--font-mono);
  font-size: 0.625rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--text-soft);
  white-space: nowrap;
}
.svc-pill--more { background: var(--text); border-color: var(--text); color: var(--bg); }
/* --accent-ink, not --turban: this pill carries type, and red on the dark
   --surface measures 1.9:1 */
.svc-pill--accent { border-color: var(--accent-ink); color: var(--accent-ink); }
.svc-pills { display: flex; flex-wrap: wrap; gap: 7px; list-style: none; }
.svc-pills--wide { margin-top: 16px; }

/* the display line, its tail set in the accent the way the reference sets its
   tail in italic — same rhythm, our palette */
/* On the site's own heading ramp, not a scale of its own: these two sizes are DESIGN.md's
   H1 and H2 tokens exactly. A fourth size put this line 14px above the hero h1 at 1440. */
.svc-display {
  font-family: var(--font-display);
  font-weight: 800;
  font-size: clamp(2.125rem, 4.6vw, 3.875rem);
  line-height: .92;
  letter-spacing: -.035em;
  text-wrap: balance;
}
.svc-display--sm { font-size: clamp(1.75rem, 3.4vw, 2.75rem); }
.svc-display em { font-style: normal; color: var(--accent-ink); }

/* ---------- intro ---------- */

section.svc-intro { padding: clamp(28px, 4vw, 56px) 0 clamp(24px, 3vw, 40px); }
.svc-intro-row {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-end;
  justify-content: space-between;
  gap: clamp(28px, 4vw, 64px);
}
/* the headline takes the room; the note keeps a column of its own and drops
   under the headline when there is no longer space for both */
.svc-intro-lead { flex: 1 1 min(520px, 100%); }
.svc-intro-side { flex: 0 1 clamp(280px, 26vw, 360px); }
.svc-intro-lead .svc-kicker { margin-bottom: clamp(20px, 2.6vw, 34px); }

/* ---------- filter bar ---------- */

/* .filters and .chip come from the gallery; only the trailing meta is new */
.svc-filters-meta {
  display: flex;
  align-items: center;
  gap: 14px;
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: 0.625rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.svc-filters-sep { width: 1px; height: 12px; background: var(--border); }
/* below this the meta only pushes the chips into a second row for nothing */
@media (max-width: 820px) { .svc-filters-meta { display: none; } }

/* ---------- service grid ----------
   The 1px gap over a --border ground draws the hairlines: one line per boundary, never two,
   at whatever column count auto-fit lands on. See DESIGN.md, Layout conventions. */
/* The band, not the grid. The element stays in the selector because `section:not(.hero)` is
   (0,1,1) and outranks a bare class -- without it the listing takes the site-wide 96px
   rhythm. */
section.svc-listing { padding: 0; }
.svc-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr));
  gap: 1px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}
/* Draws every hairline without a --border ground: the 1px spread sits in the gap and overlaps
   its neighbour's, and a row that runs out of cards leaves --surface, not border colour.
   Five services never divide evenly into 2, 3 or 4 columns, so there is always such a cell. */
.svc-card { box-shadow: 0 0 0 1px var(--border); }
.svc-card {
  position: relative;
  display: flex;
  flex-direction: column;
  min-height: 27rem;
  padding: clamp(22px, 2.4vw, 34px);
  background: var(--surface);
  transition: background .3s var(--ease);
}
/* min-height reserves the two-line case so every card's title starts from the same baseline
   whether or not its own tag row wraps -- "Intelligence" wraps at widths where "Web" and
   "Media" do not. */
.svc-card-top {
  display: flex;
  align-items: center;
  gap: 10px;
  flex-wrap: wrap;
  min-height: calc(2 * 1.75rem + 10px);
}
.svc-num {
  display: grid;
  place-items: center;
  min-width: 1.75rem;
  min-height: 1.75rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  font-family: var(--font-mono);
  font-size: 0.625rem;
  letter-spacing: .08em;
  color: var(--text-soft);
}
.svc-tag {
  padding: .25rem .5625rem;
  border-radius: 999px;
  background: var(--bg-alt);
  font-family: var(--font-mono);
  font-size: 0.5625rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.svc-price {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: .04em;
  color: var(--text);
}
.svc-price s { color: var(--text-soft); }
.svc-arrow {
  display: grid;
  place-items: center;
  width: 1.75rem; height: 1.75rem;
  flex: 0 0 auto;
  border: 1px solid var(--border);
  border-radius: 999px;
  color: var(--text-soft);
  transition: background .35s var(--ease), color .35s var(--ease),
              transform .35s var(--ease), border-color .35s var(--ease);
}
.svc-arrow svg { width: 13px; height: 13px; }
.svc-card-title { margin-top: clamp(34px, 4vw, 54px); }
.svc-card-foot { margin-top: auto; padding-top: 30px; }
.svc-card-meta {
  display: flex;
  align-items: center;
  gap: 9px;
  margin-top: 18px;
  font-family: var(--font-mono);
  font-size: 0.625rem;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.svc-dot { width: 8px; height: 8px; border-radius: 50%; flex: 0 0 auto; }

/* One real button covering the card: the card is not a link and the pills are not targets,
   so one control carries it all -- one tab stop, one accessible name, the hit area a
   pointer expects. */
.svc-card-btn {
  position: absolute;
  inset: 0;
  border: 0;
  background: none;
  cursor: pointer;
}
.svc-card:focus-within { outline: 2px solid var(--turban); outline-offset: -2px; }
.svc-card-btn:focus-visible { outline: none; }

/* ---------- custom-scope block ---------- */

/* The section owns the vertical rhythm; the row inside its .section-inner owns the two-
   column split. They were one element before this block carried its own .section-inner. */
section.svc-custom { padding: clamp(48px, 7vw, 96px) 0; }
.svc-custom-row {
  display: flex;
  flex-wrap: wrap;
  /* top-aligned: bottom-alignment stranded the heading level with the last
     line of a paragraph three times its height */
  align-items: flex-start;
  justify-content: space-between;
  gap: clamp(24px, 4vw, 56px);
}
.svc-custom-side { flex: 1 1 min(420px, 100%); max-width: 460px; }
.svc-custom-side .row { margin-top: 22px; align-items: center; }

/* ---------- detail view (<dialog>) ----------
   A native dialog opened with showModal(): focus trapping, Esc-to-close, inerting the page
   and returning focus are all the platform's, so none of it is in js/services.js. */

.svc-detail {
  width: 100%;
  max-width: none;
  height: 100%;
  max-height: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: var(--bg);
  color: var(--text);
  overflow-y: auto;
}
.svc-detail::backdrop { background: rgba(6, 6, 68, .7); }
.svc-detail-inner {
  /* Wider than --content-width on purpose: that token aligns a section's copy with the
     section above, and this is a full-screen overlay with nothing to align to. At 1440 the
     ration left no room for three tiers. */
  max-width: min(1600px, 100%);
  margin: 0 auto;
  padding: 0 var(--side-pad);
}
/* sticky inside the dialog's own scroll container, not the page's */
.svc-detail-bar {
  position: sticky;
  top: 0;
  z-index: 5;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 12px 20px;
  /* the bar spans the full dialog width while its contents keep --side-pad */
  margin: 0 calc(var(--side-pad) * -1);
  padding: 14px var(--side-pad);
  background: var(--header-bg);
  backdrop-filter: blur(12px);
  border-bottom: 1px solid var(--border);
  /* Zoom: at 400% the back button, pill and CTA wrap to three rows and the bar would take
     the screen. Cap it and scroll it, as .filters does. */
  max-height: 50svh;
  overflow-y: auto;
}
.svc-detail-bar-right { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.svc-back {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  padding: 6px 4px;
  background: none;
  border: 0;
  cursor: pointer;
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--text);
}
.svc-back-icon {
  display: grid;
  place-items: center;
  width: 2rem; height: 2rem;
  border: 1px solid var(--border);
  border-radius: 999px;
  transition: background .25s ease, color .25s ease, border-color .25s ease;
}
.svc-back-icon svg { width: 14px; height: 14px; }
.svc-back:focus-visible { outline: 2px solid var(--turban); outline-offset: 3px; }

/* 5/7, the reference's proportion. The side column is the brief and the main column the
   evidence, so the brief stays put while the evidence scrolls past it. */
.svc-detail-body {
  display: grid;
  /* minmax(0, ...), not a bare fr: an fr track's automatic minimum is min-content, so one
     wide child stretches the track past the dialog and every line of text overflows with
     it. */
  grid-template-columns: minmax(0, 5fr) minmax(0, 7fr);
  gap: 0 clamp(32px, 4vw, 64px);
  align-items: start;
}
.svc-detail-side {
  position: sticky;
  top: 5.25rem;
  /* its own scroll, so a long brief is never clipped by the sticky box */
  max-height: calc(100svh - 6.5rem);
  overflow-y: auto;
  scrollbar-width: thin;
  padding: clamp(32px, 4vw, 64px) 0;
}
.svc-detail-main {
  border-left: 1px solid var(--border);
  padding: clamp(32px, 4vw, 64px) 0 clamp(48px, 6vw, 96px) clamp(32px, 4vw, 64px);
}
.svc-tagline {
  margin: 18px 0 20px;
  font-family: var(--font-display);
  font-size: clamp(1.0625rem, 1.6vw, 1.3125rem);
  line-height: 1.25;
  color: var(--accent-ink);
  text-wrap: balance;
}
.svc-detail-side .lead { margin-bottom: 30px; max-width: 44ch; }

.svc-panel {
  margin-top: 30px;
  padding: clamp(20px, 2vw, 26px);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--surface);
}
.svc-process .svc-label { margin-bottom: 0; }
.svc-step { display: flex; gap: 14px; margin-top: 18px; }
.svc-step-k {
  display: grid;
  place-items: center;
  flex: 0 0 auto;
  min-width: 1.5rem; min-height: 1.5rem;
  border-radius: 999px;
  background: var(--text);
  color: var(--bg);
  font-family: var(--font-mono);
  font-size: 0.5625rem;
}
.svc-step-t { font-weight: 600; font-size: 0.8125rem; }
.svc-step-d { margin-top: 5px; font-size: 0.75rem; line-height: 1.45; color: var(--text-soft); }
.svc-next-wrap { margin-top: 30px; }
.svc-next { color: var(--text); border-color: var(--border); cursor: pointer; font-family: inherit; }
/* .btn-secondary:hover sets border-color:var(--feature-text) at (0,2,0) and outranks the
   (0,1,0) rule above, painting this border the same rgb(242,247,253) as the dialog -- the
   control vanishes on hover. */
.svc-next:hover { border-color: var(--text-soft); }

/* ---------- preview of the deliverable ---------- */

.svc-demo {
  margin-top: 14px;
  border: 1px solid var(--border);
  border-radius: var(--radius-lg);
  background: var(--surface);
  overflow: hidden;
}
.svc-demo-chrome {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 12px 18px;
  border-bottom: 1px solid var(--border);
}
.svc-demo-lights { display: flex; gap: 7px; }
.svc-demo-lights span {
  width: 10px; height: 10px;
  border-radius: 50%;
  background: var(--border);
}
.svc-demo-open {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: 0.625rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--text);
  /* 24px floor, as .footer-links a. Measured 119x15, and it is the ONLY way to open the
     live demo, so 2.5.8's equivalent-control exception does not apply. Underline, not
     border-bottom. */
  min-height: 24px;
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  text-decoration-color: var(--border);
  transition: color .2s ease, text-decoration-color .2s ease;
}
.svc-demo-open svg { width: 12px; height: 12px; }
.svc-demo-open:focus-visible { outline: 2px solid var(--turban); outline-offset: 3px; }

/* One frame visible at a time. 16:9 is the shape the stats row and the rate
   card below were laid out against, so every frame keeps it whatever it holds. */
.svc-demo-frame {
  position: relative;
  aspect-ratio: 16 / 9;
  overflow: hidden;
  background: var(--bg-alt);
}
.svc-demo-frame[hidden] { display: none; }
.svc-demo-frame img { display: block; width: 100%; height: 100%; object-fit: cover; }
/* ---------- before/after wipe (product photography demo only) ----------
   The frame stays 16:9 for the stats row and rate card below, and the widget sits inside it
   at the PAIR's own aspect -- fitting it to the frame's width would See DESIGN.md, Print. */
/* The only frame that does NOT keep 16:9. Seven of the eight pairs are ~2:3 posters, and
   inside a 16:9 frame the widget is 260px on a laptop and 137px on a phone -- too small to
   drag or read. */
.svc-demo-frame--compare {
  aspect-ratio: auto;
  display: flex;
  justify-content: center;
}
.svc-compare {
  --pos: 50%;
  position: relative;
  /* Height leads and width follows from --ar, until max-width takes over on a narrow frame
     and the height follows instead -- one rule covers 520x367 at 1440 and 345x488 at 390. */
  height: min(70vh, 520px);
  width: auto;
  max-width: 100%;
  aspect-ratio: var(--ar, 2 / 3);
  margin-inline: auto;
  overflow: hidden;
  isolation: isolate;
}
.svc-compare img {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  /* the pair is cropped to one aspect at build time, so cover never disagrees
     between the two layers and the wipe stays registered */
}
/* the retouched shot is the one that gets revealed, so it is the clipped layer */
.svc-compare-after { clip-path: inset(0 0 0 var(--pos)); }

.svc-compare-line {
  position: absolute;
  top: 0;
  bottom: 0;
  left: var(--pos);
  width: 2px;
  margin-left: -1px;
  background: rgba(255, 255, 255, .9);
  box-shadow: 0 0 0 1px rgba(4, 47, 105, .28);
  pointer-events: none;
}
.svc-compare-tag {
  position: absolute;
  bottom: 10px;
  z-index: 2;
  padding: .25rem .5rem;
  border-radius: 999px;
  background: rgba(6, 6, 68, .58);
  color: #fff;
  font-family: var(--font-mono);
  font-size: 0.5625rem;
  letter-spacing: .12em;
  text-transform: uppercase;
  pointer-events: none;
}
.svc-compare-tag--before { left: 10px; }
.svc-compare-tag--after { right: 10px; }

.svc-compare-range {
  position: absolute;
  inset: 0;
  z-index: 3;
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
  background: transparent;
  border: 0;
  appearance: none;
  -webkit-appearance: none;
  cursor: ew-resize;
}
.svc-compare-range:focus-visible { outline: 2px solid var(--turban); outline-offset: -2px; }
/* the two thumb pseudo-elements cannot be combined into one rule -- an
   unrecognised selector invalidates the whole list in both engines */
.svc-compare-range::-webkit-slider-thumb {
  -webkit-appearance: none;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, .95);
  background: rgba(6, 6, 68, .42);
  box-shadow: 0 2px 10px rgba(4, 47, 105, .35);
  cursor: ew-resize;
}
.svc-compare-range::-moz-range-thumb {
  width: 34px;
  height: 34px;
  border-radius: 50%;
  border: 2px solid rgba(255, 255, 255, .95);
  background: rgba(6, 6, 68, .42);
  box-shadow: 0 2px 10px rgba(4, 47, 105, .35);
  cursor: ew-resize;
}
.svc-compare-range::-moz-range-track { background: transparent; border: 0; }

/* A site frame renders at desktop width and is scaled down: at the ~700px the dialog gives
   it, a live site would switch to its own mobile layout. --demo-scale is set per frame by
   the ResizeObserver in services.js. */
.svc-demo-frame iframe {
  position: absolute;
  top: 0; left: 0;
  width: 1440px;
  height: 810px;
  border: 0;
  transform: scale(var(--demo-scale, .5));
  transform-origin: 0 0;
}
.svc-demo-soon {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 14px;
  min-height: 15rem;
  padding: clamp(24px, 4vw, 44px);
  text-align: center;
  background: var(--bg-alt);
}
.svc-demo-soon p {
  max-width: 42ch;
  font-size: 0.875rem;
  line-height: 1.55;
  color: var(--text-soft);
}
.svc-demo-nav {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 18px;
  border-top: 1px solid var(--border);
}
.svc-demo-btn {
  display: grid;
  place-items: center;
  width: 1.75rem; height: 1.75rem;
  flex: 0 0 auto;
  border: 1px solid var(--border);
  border-radius: 999px;
  background: transparent;
  color: var(--text-soft);
  cursor: pointer;
  transition: background .2s ease, color .2s ease, border-color .2s ease;
}
.svc-demo-btn svg { width: 13px; height: 13px; }
.svc-demo-btn:focus-visible { outline: 2px solid var(--turban); outline-offset: 2px; }
.svc-demo-dots { display: flex; gap: 7px; }
.svc-demo-dot {
  width: 7px; height: 7px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: var(--border);
  cursor: pointer;
  transition: background .2s ease, transform .2s ease;
}
.svc-demo-dot[aria-selected="true"] { background: var(--accent-ink); transform: scale(1.25); }
.svc-demo-dot:focus-visible { outline: 2px solid var(--turban); outline-offset: 3px; }
.svc-demo-count {
  margin-left: auto;
  font-family: var(--font-mono);
  font-size: 0.625rem;
  letter-spacing: .12em;
  color: var(--text-soft);
}
.svc-stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(140px, 100%), 1fr));
  gap: 1px;
  background: var(--surface);
  border-top: 1px solid var(--border);
}
.svc-stat { padding: 16px 18px; background: var(--surface); box-shadow: 0 0 0 1px var(--border); }
.svc-stat-k {
  font-family: var(--font-mono);
  font-size: 0.5625rem;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.svc-stat-v { margin-top: 6px; font-family: var(--font-display); font-size: 1.0625rem; }

/* ---------- rate card ---------- */

.svc-ratecard-head { margin-top: clamp(40px, 5vw, 72px); }
.svc-ratecard-head + .svc-kicker { margin-top: 8px; }
/* the same hairline-grid device as .svc-grid, so the two read as one system.
   220px is where a tier's price line and its longest feature stop fitting. */
.svc-tiers {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr));
  gap: 1px;
  margin-top: 20px;
  background: var(--surface);
  border: 1px solid var(--border);
  border-radius: var(--radius);
  overflow: hidden;
}
.svc-tier { box-shadow: 0 0 0 1px var(--border); }
.svc-tier {
  position: relative;
  display: flex;
  flex-direction: column;
  padding: clamp(16px, 1.6vw, 24px);
  background: var(--surface);
}
/* the middle tier inverts, as in the reference — it is the one being pointed
   at, and inverting says so without introducing a second accent colour */
.svc-tier--popular { background: var(--feature-bg); color: var(--feature-text); }
.svc-tier-badge {
  position: absolute;
  top: clamp(16px, 1.6vw, 24px);
  right: clamp(16px, 1.6vw, 24px);
  padding: .25rem .5rem;
  border-radius: 999px;
  background: var(--turban);
  color: var(--paper);
  font-family: var(--font-mono);
  font-size: 0.5rem;
  letter-spacing: .12em;
  text-transform: uppercase;
}
.svc-tier-name {
  font-family: var(--font-mono);
  font-size: 0.5625rem;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--text-soft);
}
.svc-tier--popular .svc-tier-name { color: var(--feature-soft); }
/* The list price, struck, above the live one. Both come from the same authored number via
   the DISCOUNT dial in js/services.js, so they cannot drift the way two hand-typed prices
   would. */
.svc-tier-was {
  margin-top: 12px;
  font-family: var(--font-mono);
  font-size: 0.75rem;
  color: var(--text-soft);
}
.svc-tier--popular .svc-tier-was { color: var(--feature-soft); }
.svc-tier-price {
  margin-top: 2px;
  font-family: var(--font-display);
  font-weight: 700;
  /* the longest figure on the page is a seven-character lakh amount
     (Rs 2,40,000); this ramp is what keeps it on one line inside a 190px tier */
  font-size: clamp(1.375rem, 1.9vw, 1.875rem);
  line-height: 1;
  letter-spacing: -.02em;
}
/* the second currency, deliberately quiet: it is the same price, not a second
   product. Its own line, so the display ramp above stays sized for INR alone */
.svc-tier-usd {
  margin-top: 4px;
  font-family: var(--font-mono);
  font-size: 0.75rem;
  color: var(--text-soft);
}
.svc-tier--popular .svc-tier-usd { color: var(--feature-soft); }
.svc-tier-desc { margin-top: 8px; font-size: 0.75rem; color: var(--text-soft); }
.svc-tier--popular .svc-tier-desc { color: var(--feature-soft); }
/* The list absorbs the slack so every tier's button sits on the same baseline -- three CTAs
   at three heights read as three unrelated cards rather than one comparison. */
.svc-tier-list {
  list-style: none;
  margin-top: 22px;
  display: grid;
  align-content: start;
  gap: 10px;
  flex: 1 1 auto;
}
.svc-tier-list li {
  position: relative;
  padding-left: 16px;
  font-size: 0.78125rem;
  line-height: 1.45;
  color: var(--text-soft);
}
.svc-tier-list li::before {
  content: "";
  position: absolute;
  left: 0; top: .5em;
  width: 4px; height: 4px;
  border-radius: 50%;
  background: currentColor;
}
.svc-tier--popular .svc-tier-list li { color: var(--feature-soft); }
.svc-tier-cta { margin-top: 26px; text-align: center; }

/* inverted-surface button: on --feature-bg the primary red drops too low, so
   the fill becomes paper and the label takes the navy */
.btn-onDark { background: var(--paper); color: var(--ink); }
.btn-onDark:focus-visible { outline: 2px solid var(--paper); outline-offset: 3px; }

.svc-two {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
  gap: 16px;
  margin-top: clamp(32px, 4vw, 56px);
}
.svc-two .svc-panel { margin-top: 0; }
.svc-panel--invert {
  background: var(--feature-bg);
  border-color: var(--feature-bg);
  color: var(--feature-text);
}
.svc-panel--invert .svc-label { color: var(--feature-soft); }
.svc-invert-lead {
  font-family: var(--font-display);
  font-size: 1.0625rem;
  line-height: 1.3;
  margin-bottom: 22px;
}
.svc-ticks { list-style: none; display: grid; gap: 11px; }
.svc-ticks li {
  position: relative;
  padding-left: 18px;
  font-size: 0.8125rem;
  line-height: 1.5;
  color: var(--text-soft);
}
.svc-ticks li::before {
  content: "";
  position: absolute;
  left: 0; top: .55em;
  width: 5px; height: 5px;
  border-radius: 50%;
  background: var(--accent-ink);
}

/* ---------- responsive ---------- */

/* Measured, not conventional: the boundary is where the main column stops holding three tiers
   above their 180px floor. See DESIGN.md. */
@media (max-width: 1279px) {
  .svc-detail-body { grid-template-columns: minmax(0, 1fr); }
  .svc-detail-side {
    position: static;
    max-height: none;
    overflow: visible;
    padding-bottom: clamp(24px, 3vw, 40px);
  }
  .svc-detail-main {
    border-left: 0;
    border-top: 1px solid var(--border);
    padding-left: 0;
  }
}

@media (hover: hover) and (pointer: fine) {
  .svc-card:hover { background: var(--bg-alt); }
  .svc-card:hover .svc-arrow {
    background: var(--text);
    border-color: var(--text);
    color: var(--bg);
    transform: rotate(45deg);
  }
  .svc-back:hover .svc-back-icon {
    background: var(--text);
    border-color: var(--text);
    color: var(--bg);
  }
  .btn-onDark:hover { background: var(--feature-text); }
}

@media (prefers-reduced-motion: reduce) {
  .svc-card, .svc-arrow, .svc-back-icon { transition: none; }
  .svc-card:hover .svc-arrow { transform: none; }
  .svc-demo-open:hover { color: var(--accent-ink); text-decoration-color: var(--accent-ink); }
  .svc-demo-btn:hover { background: var(--text); border-color: var(--text); color: var(--bg); }
}

/* .print-only elements are the ATS section headings -- see the print block. */
.print-only { display: none; }

/* ============================================================
   PRINT -- the page becomes an ATS-readable CV. There is no resume.pdf on purpose: two
   documents drift and the PDF is always the stale one. See DESIGN.md, Print.
   ============================================================ */
@media print {
  /* dark theme prints white-on-white without this */
  :root, :root:not([data-theme="light"]) {
    --bg: #fff; --bg-alt: #fff; --surface: #fff;
    --text: #000; --text-soft: #222; --border: #999;
    --hero-ink: #000; --hero-ink-soft: #222;
    --feature-bg: #fff; --feature-text: #000; --feature-soft: #222;
    --accent-ink: #000;
  }
  @page { margin: 12mm 12mm; }
  html, body { background: #fff !important; color: #000 !important;
               font-size: 10pt; }

  /* A resume is set in a standard face: Arial at normal tracking, upright, black. This
     deliberately outranks the whole brand type system. See DESIGN.md, Print. */
  #main, .print-head, #main *, .print-head * {
    font-family: Arial, "Helvetica Neue", Helvetica, sans-serif !important;
    letter-spacing: normal !important;
    font-style: normal !important;
    color: #000 !important;
  }
  /* the roles and dates are mono + uppercase on screen; a CV wants them as
     they were authored */
  .role, .timeline-dates { text-transform: none !important; }

  /* navigation, decoration, and anything a parser cannot read */
  .site-header, .nav-overlay, .skip-link, .stage, .tech-orbit, .tech-rail,
  .services, .open-to-actions, .copy-btn, .copy-status,
  .footer-hero .row, .footer-nav, .footer-bottom,
  .footer-logo, .card-cta, .wink, .wink-tap, .svc-detail, .lightbox, .filters,
  .gallery-hint, .card-icon, .cert-shot, .timeline-mark, .card-link,
  .status-dot, .hero .copy .row, .card-meta, .project-foot,
  /* .proof is a screen affordance: js/main.js force-opens every <details> for the print
     job, so without this the project cards would dump two JSONL blocks into the CV's text
     stream. */
  .proof,
  .timeline-body > summary::after { display: none !important; }

  /* the hero headline is a slogan, not a CV title -- the masthead replaces it */
  .hero, .hero .copy { display: none !important; }

  .print-only { display: block !important; }
  .print-head { display: block !important; margin-bottom: 6mm; }
  .print-name { font-family: var(--font-display); font-size: 20pt;
                font-weight: 700; line-height: 1.1; }
  .print-title { margin-top: 1pt; font-size: 10.5pt; font-weight: 600; }
  .print-meta { margin-top: 1pt; font-size: 9pt; color: #222; }

  /* every section is one linear block */
  section, .section-inner { padding: 0 !important; margin: 0 !important;
                            max-width: none !important; background: #fff !important; }
  section { margin-bottom: 4.5mm !important; }
  /* overflow:visible is not cosmetic: these containers clip on screen for the hairline-grid
     trick, and with the clip left on, a bold "A" at left:0 loses its overhanging stem --
     "gentic AI & LLMs". Do not put the clip back. */
  .card-grid, .facts, .footer-grid, .timeline {
    display: block !important; grid-template-columns: 1fr !important;
    background: none !important; border: none !important; gap: 0 !important;
    overflow: visible !important; }

  /* the ATS heading; the decorative one beside it goes */
  /* max-width:none matters -- h2 carries a 640px screen measure, which would stop the
     section rule half way across the page. */
  .print-only {
    font-family: var(--font-body); font-size: 11pt; font-weight: 700;
    max-width: none !important; text-transform: uppercase; letter-spacing: .08em;
    border-bottom: 1pt solid #000; padding-bottom: 1mm; margin-bottom: 2.5mm; }
  section > .section-inner > .eyebrow,
  section > .section-inner > h2:not(.print-only),
  .section-head { display: none !important; }

  .lead, p, li { font-size: 9.5pt; line-height: 1.35; }
  .lead + .lead { margin-top: 1.5mm; }

  /* --- Skills: eight groups, each one line of comma-separated keywords.
     This is also the best shape for keyword matching. --- */
  /* .card is display:flex on screen, and a flex child cannot be inline: `display:inline` on
     the h3 would compute back to block and put the group name on its own line. */
  .skills .card { display: block !important;
                  border: none !important; box-shadow: none !important;
                  padding: 0 !important; margin-bottom: 1.5mm; }
  .skills .card h3 { display: inline; font-family: var(--font-body);
                     font-size: 9.5pt !important; font-weight: 700; }
  .skills .card h3::after { content: ": "; }
  .skills .card h3 em { font-size: inherit !important; font-style: normal; }
  .skills .card ul { display: inline; }
  .skills .card li { display: inline; font-size: 9.5pt; }
  .skills .card li:not(:last-child)::after { content: ", "; }

  /* --- Experience: the bullets are the whole point, keep every one --- */
  .timeline-item { border: none !important; box-shadow: none !important;
                   padding: 0 !important; margin-bottom: 3mm; }
  .timeline-body > summary { padding: 0 !important; cursor: auto; }
  .timeline-body h3 { font-size: 10pt !important; font-weight: 700; display: inline; }
  .timeline-body .role { font-size: 9pt; font-weight: 600; }
  .timeline-dates { font-size: 9pt; color: #222; }
  /* Standard resume bullets. The screen design draws its own dash via li::before, so that
     must be switched OFF -- leaving it on and adding a disc prints two markers per line. */
  .timeline-detail { margin-top: 1mm !important; padding-left: 4.5mm !important; }
  .timeline-detail li { list-style: disc outside !important; margin-top: 0.8mm;
                        padding-left: 0 !important; }
  .timeline-detail li::before { display: none !important; }

  /* --- Certifications: name, issuer and date only. The descriptions and the
     seven verify URLs were most of two pages; they stay on the site. --- */
  .certifications .timeline-detail { display: none !important; }
  .certifications .timeline-item { margin-bottom: 0.6mm; }
  /* one line per certificate, not three: name, issuer, date all inline. Seven
     certs at three lines each was half a page of whitespace. */
  .certifications .timeline-body > summary { display: block !important; }
  .certifications .timeline-body h3 { font-size: 9.5pt !important; display: inline; }
  .certifications .timeline-body h3::after { content: " — "; font-weight: 400; }
  .certifications .timeline-body .role { display: inline; font-family: var(--font-body);
      font-size: 9.5pt; font-weight: 400; text-transform: none; letter-spacing: 0; }
  .certifications .timeline-body .role::after { content: ", "; }
  .certifications .timeline-dates { display: inline; font-family: var(--font-body);
      font-size: 9.5pt; letter-spacing: 0; }

  /* --- Projects: title and one line each --- */
  .projects .card { display: block !important;
                    border: none !important; box-shadow: none !important;
                    padding: 0 !important; margin-bottom: 2mm; }
  .projects .card h3 { font-size: 10pt !important; display: inline; }
  .projects .card h3::after { content: " — "; }
  .projects .card p { display: inline; font-size: 9.5pt; }

  /* --- Target roles: the two facts a recruiter screens on. The rest of the
     band duplicates the masthead. --- */
  .facts > div { padding: 0 !important; margin-bottom: 1mm; background: none !important;
                 border: none !important; }
  .facts dt { display: inline; font-family: var(--font-body); font-size: 9.5pt;
              font-weight: 700; text-transform: none; letter-spacing: 0; color: #000; }
  .facts dt::after { content: ": "; }
  .facts dd { display: inline; margin: 0 !important; font-size: 9.5pt; }
  /* Roles only. Experience, work model, location and contact are all already
     in the masthead, and Core stack repeats the Skills section. */
  .facts > div:nth-child(n+2) { display: none !important; }

  /* an entry split across a page break is what makes a CV look careless */
  .timeline-item, .card, .facts > div { break-inside: avoid; }
  .print-only { break-after: avoid; }

  a { color: #000 !important; text-decoration: none; }
  .site-footer { display: none !important; }
}
