/* =============================================================
   Odoo developer portfolio
   Vanilla CSS, no framework. Tokens in :root, dark mode swaps
   only the token values. Layout is a hairline "spec sheet".
   ============================================================= */

*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
img, svg { display: block; max-width: 100%; }
button, input, textarea, select { font: inherit; color: inherit; }
a { color: inherit; text-decoration: none; }
table { border-collapse: collapse; width: 100%; }

/* ---------- tokens: light ---------- */
/* Blueprint and vermilion. Palette: #EEF2F6 paper, #FFFFFF, #0C2340 navy,
   #5A7290 steel, #E2543A vermilion.

   Two of those cannot be used as small text: vermilion is 3.77:1 on white and
   white-on-vermilion is the same, both under WCAG AA. So the accent is split --
   --accent is the fill/graphic colour, --accent-text is a darkened vermilion
   for type, and --accent-ink is what goes ON a vermilion fill. Every pair in
   here clears 4.5:1. */
:root {
  --bg:          #EEF2F6;
  --bg-alt:      #E2E9F1;
  --surface:     #FFFFFF;
  --ink:         #0C2340;   /* 14.0:1 on --bg */
  --ink-soft:    #3C5570;   /*  6.9:1 */
  --ink-faint:   #4F647E;   /*  5.4:1 on --bg, 5.0:1 on --bg-alt (the alt band is
                               the tighter constraint; palette steel is 4.34 there) */
  --line:        #D5DEE8;
  --line-firm:   #B7C5D5;
  --accent:        #E2543A;   /* vermilion: fills, dots, rules -- never small text */
  --accent-text:   #B0412D;   /*  5.1:1 on --bg; use for links and labels */
  --accent-ink:    #04121F;   /*  5.0:1 on the vermilion fill (white is 3.77) */
  --accent-wash:   #FBE7E3;
  --steel:        #5A7290;   /* palette steel: graphics, and code strings on white */
  --amber:       #8A5A2B;

  --r:    8px;
  --r-sm: 5px;
  --shadow:    0 1px 2px rgba(25, 19, 32, .04), 0 10px 26px -14px rgba(25, 19, 32, .16);
  --shadow-lg: 0 2px 5px rgba(25, 19, 32, .05), 0 26px 50px -24px rgba(25, 19, 32, .28);

  --f-display: "Bricolage Grotesque", ui-sans-serif, system-ui, sans-serif;
  --f-body:    "Manrope", ui-sans-serif, system-ui, -apple-system, sans-serif;
  --f-mono:    "IBM Plex Mono", ui-monospace, "SFMono-Regular", Menlo, monospace;

  --gutter: clamp(1.15rem, 4vw, 2.75rem);
  --band:   clamp(4rem, 8vw, 7rem);
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    color-scheme: dark;
    /* The palette navy is the page, not an accent. */
    --bg:        #0C2340;
    --bg-alt:    #102B4A;
    --surface:   #12304F;
    --ink:       #EEF2F6;
    --ink-soft:  #AEC0D4;
    --ink-faint: #8AA2BC;
    --line:      #1E3E63;
    --line-firm: #2D5480;
    --accent:      #FF6B4F;
    --accent-text: #FF7F66;
    --accent-ink:  #04121F;
    --accent-wash: #2C1A16;
    --steel:      #7E9BBC;
    --amber:     #D9A46A;
    --shadow:    0 1px 2px rgba(0,0,0,.45), 0 10px 26px -14px rgba(0,0,0,.7);
    --shadow-lg: 0 2px 5px rgba(0,0,0,.45), 0 26px 50px -24px rgba(0,0,0,.75);
  }
}

:root[data-theme="dark"] {
  color-scheme: dark;
  --bg:        #0C2340;
  --bg-alt:    #102B4A;
  --surface:   #12304F;
  --ink:       #EEF2F6;
  --ink-soft:  #AEC0D4;
  --ink-faint: #8AA2BC;
  --line:      #1E3E63;
  --line-firm: #2D5480;
  --accent:      #FF6B4F;
  --accent-text: #FF7F66;
  --accent-ink:  #04121F;
  --accent-wash: #2C1A16;
  --steel:      #7E9BBC;
  --amber:     #D9A46A;
  --shadow:    0 1px 2px rgba(0,0,0,.45), 0 10px 26px -14px rgba(0,0,0,.7);
  --shadow-lg: 0 2px 5px rgba(0,0,0,.45), 0 26px 50px -24px rgba(0,0,0,.75);
}

/* ---------- base ---------- */
html { scroll-behavior: smooth; -webkit-text-size-adjust: 100%; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  background: var(--bg);
  color: var(--ink);
  font-family: var(--f-body);
  font-size: 1rem;
  line-height: 1.62;
  -webkit-font-smoothing: antialiased;
  overflow-x: hidden;
}

h1, h2, h3, h4 { font-family: var(--f-display); font-weight: 700; line-height: 1.1; letter-spacing: -.025em; }
h1 { font-size: clamp(2.4rem, 6.2vw, 4.4rem); letter-spacing: -.04em; }
h2 { font-size: clamp(1.75rem, 3.6vw, 2.7rem); }
h3 { font-size: clamp(1.05rem, 1.5vw, 1.22rem); font-weight: 600; letter-spacing: -.015em; }

::selection { background: var(--accent); color: var(--accent-ink); }
:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; border-radius: 3px; }

.wrap { width: 100%; max-width: 1180px; margin-inline: auto; padding-inline: var(--gutter); }

.skip {
  position: absolute; top: 0; left: 50%; z-index: 200;
  transform: translate(-50%, -130%);
  background: var(--accent); color: var(--accent-ink);
  padding: .6rem 1.1rem; border-radius: 0 0 var(--r-sm) var(--r-sm);
  font-size: .85rem; font-weight: 700;
}
.skip:focus { transform: translate(-50%, 0); }

/* ---------- shared ---------- */
.band { padding-block: var(--band); }
.band--alt { background: var(--bg-alt); border-block: 1px solid var(--line); }

.tag {
  display: flex; align-items: center; gap: .7rem;
  font-family: var(--f-mono);
  font-size: .72rem; letter-spacing: .16em; text-transform: uppercase;
  color: var(--ink-faint);
}
.tag b { color: var(--accent-text); font-weight: 500; }
.tag::after { content: ""; flex: 1; height: 1px; background: var(--line); }

.head { max-width: 46rem; margin-bottom: clamp(2.2rem, 4vw, 3.4rem); }
.head h2 { margin: 1rem 0 .85rem; }
.head p { color: var(--ink-soft); max-width: 40rem; }

.btn {
  display: inline-flex; align-items: center; justify-content: center; gap: .5rem;
  padding: .8rem 1.5rem;
  border: 1px solid transparent; border-radius: var(--r-sm);
  font-size: .92rem; font-weight: 700; letter-spacing: -.005em;
  cursor: pointer;
  transition: transform .16s ease, background .16s ease, border-color .16s ease, color .16s ease;
}
.btn:hover { transform: translateY(-2px); }
.btn:active { transform: none; }
.btn--fill { background: var(--accent); color: var(--accent-ink); }
.btn--fill:hover { filter: brightness(1.08); }
.btn--line { border-color: var(--line-firm); }
.btn--line:hover { border-color: var(--ink); background: var(--surface); }
.btn--wide { width: 100%; }
.btn[disabled] { opacity: .55; cursor: progress; transform: none; }

.panel {
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--r);
}

.chip {
  display: inline-block;
  font-family: var(--f-mono); font-size: .72rem; line-height: 1;
  padding: .42rem .6rem;
  border: 1px solid var(--line); border-radius: var(--r-sm);
  color: var(--ink-soft); background: var(--surface);
  white-space: nowrap;
  transition: color .16s ease, border-color .16s ease;
}
.stags .chip:hover, .case-stack .chip:hover { color: var(--accent-text); border-color: var(--accent); }

/* Shared icon tile -- process steps and Odoo app cards. Same shape language as
   .round (the theme toggle): grid-centred, --r-sm, --line border, svg sized by
   a descendant rule. Tinted rather than transparent because it sits on
   --surface inside a card. Size is set by whoever uses it. */
.ico {
  display: grid; place-items: center;
  width: 2.5rem; height: 2.5rem; flex: none;
  background: var(--bg-alt); border: 1px solid var(--line); border-radius: var(--r-sm);
  color: var(--accent-text);
  transition: border-color .18s ease;
}
.ico svg { width: 1.15rem; height: 1.15rem; }

.loading { font-family: var(--f-mono); font-size: .85rem; color: var(--ink-faint); }

/* ---------- nav ---------- */
.nav {
  position: sticky; top: 0; z-index: 100;
  background: var(--bg);
  background: color-mix(in srgb, var(--bg) 84%, transparent);
  backdrop-filter: saturate(150%) blur(14px);
  border-bottom: 1px solid transparent;
  transition: border-color .22s ease;
}
.nav.stuck { border-bottom-color: var(--line); }
.nav .wrap { display: flex; align-items: center; gap: 1.1rem; min-height: 66px; }

.brand { font-family: var(--f-mono); font-size: .93rem; font-weight: 500; margin-right: auto; }
.brand b { color: var(--accent-text); font-weight: 500; }

.nav-links { display: flex; align-items: center; gap: .2rem; }
.nav-links a {
  position: relative;
  padding: .45rem .68rem; border-radius: var(--r-sm);
  font-size: .88rem; font-weight: 500; color: var(--ink-soft);
  transition: color .16s ease, background .16s ease;
}
/* wipes in from the left on hover and on the scrollspy's active link */
.nav-links a:not(.cta)::after {
  content: ""; position: absolute; left: .68rem; right: .68rem; bottom: .22rem;
  height: 1.5px; background: currentColor; border-radius: 2px;
  transform: scaleX(0); transform-origin: left;
  transition: transform .24s cubic-bezier(.22, .61, .36, 1);
}
.nav-links a:not(.cta):hover::after,
.nav-links a:not(.cta).on::after { transform: scaleX(1); }
.nav-links a:hover { color: var(--ink); background: var(--bg-alt); }
/* :not(.cta) matters -- the CTA's href is #contact, so the scrollspy marks it
   .on too, and this rule out-specifies `.nav-links .cta`. Without the guard the
   button renders vermilion-on-vermilion and the label vanishes while the
   visitor is reading the contact section. */
.nav-links a:not(.cta).on { color: var(--accent-text); }
.nav-links .cta {
  margin-left: .45rem; padding: .5rem 1rem;
  background: var(--accent); color: var(--accent-ink); font-weight: 700;
}
.nav-links .cta:hover { background: var(--accent); filter: brightness(1.08); color: var(--accent-ink); }

.round {
  display: grid; place-items: center;
  width: 37px; height: 37px;
  background: transparent; border: 1px solid var(--line); border-radius: var(--r-sm);
  color: var(--ink-soft); cursor: pointer;
  transition: color .16s ease, border-color .16s ease;
}
.round:hover { color: var(--ink); border-color: var(--line-firm); }
.round svg { width: 17px; height: 17px; }

/* theme icon: show the theme you would switch TO */
.i-moon { display: block; }
.i-sun  { display: none; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) .i-moon { display: none; }
  :root:not([data-theme="light"]) .i-sun  { display: block; }
}
:root[data-theme="dark"]  .i-moon { display: none; }
:root[data-theme="dark"]  .i-sun  { display: block; }
:root[data-theme="light"] .i-moon { display: block; }
:root[data-theme="light"] .i-sun  { display: none; }

.burger {
  display: none; position: relative;
  width: 37px; height: 37px;
  background: transparent; border: 1px solid var(--line); border-radius: var(--r-sm);
  cursor: pointer;
}
.burger span {
  position: absolute; left: 50%; width: 15px; height: 1.5px;
  background: var(--ink); border-radius: 2px; transform: translateX(-50%);
  transition: transform .2s ease, opacity .16s ease;
}
.burger span:nth-child(1) { top: 12px; }
.burger span:nth-child(2) { top: 17px; }
.burger span:nth-child(3) { top: 22px; }
.burger[aria-expanded="true"] span:nth-child(1) { transform: translateX(-50%) translateY(5px) rotate(45deg); }
.burger[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.burger[aria-expanded="true"] span:nth-child(3) { transform: translateX(-50%) translateY(-5px) rotate(-45deg); }

.drawer { display: none; background: var(--bg); border-bottom: 1px solid var(--line); }
.drawer.open { display: block; }
.drawer a {
  display: block; padding: .9rem var(--gutter);
  border-top: 1px solid var(--line);
  font-size: .95rem; color: var(--ink-soft);
}
.drawer a:first-child { border-top: 0; }
.drawer a:hover { color: var(--ink); background: var(--bg-alt); }

/* ---------- hero ---------- */
.hero {
  position: relative; isolation: isolate;
  padding-block: clamp(3rem, 7vw, 5.5rem) clamp(2.5rem, 5vw, 4rem);
  border-bottom: 1px solid var(--line);
}
.hero::before {
  content: ""; position: absolute; z-index: -1; pointer-events: none;
  /* horizontal inset stays at 0: a negative one widens the scroll box and
     gives the page a horizontal scrollbar on narrow viewports */
  inset: -18% 0 auto 0; height: 72%;
  /* Steel, not vermilion: an accent-tinted wash puts a warm cast over a
     palette whose whole point is that it is cool. */
  background: radial-gradient(58% 100% at 20% 0%,
    color-mix(in srgb, var(--steel) 20%, transparent), transparent 68%);
}
.hero-top { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); gap: clamp(1.5rem, 4vw, 3.5rem); align-items: start; }

.live {
  display: inline-flex; align-items: center; gap: .5rem;
  padding: .38rem .8rem .38rem .6rem;
  background: var(--accent-wash); color: var(--accent-text);
  border-radius: 100px; font-size: .79rem; font-weight: 700;
}
.live i {
  width: 7px; height: 7px; border-radius: 50%; background: currentColor;
  animation: pulse 2.4s cubic-bezier(.4,0,.2,1) infinite;
}
@keyframes pulse {
  0%   { box-shadow: 0 0 0 0 currentColor; }
  70%  { box-shadow: 0 0 0 7px transparent; }
  100% { box-shadow: 0 0 0 0 transparent; }
}
@media (prefers-reduced-motion: reduce) { .live i { animation: none; } }

.hero h1 { margin: 1.3rem 0 1.2rem; }
.hero h1 em { font-style: normal; color: var(--accent); }   /* display size: 3.36:1 clears the 3:1 large-text bar */
.hero .lead { font-size: clamp(1rem, 1.4vw, 1.14rem); color: var(--ink-soft); max-width: 36rem; }
.hero-cta { display: flex; flex-wrap: wrap; gap: .7rem; margin-top: 2rem; }

/* ---------- hero profile card ----------
   Deliberately dark in both themes: it reads as an editor window, so it
   keeps its own palette rather than following the page tokens. Values
   below mirror the dark-theme token set, so it stays on-brand. */
.idcard {
  --card-bg:     #0A1C33;
  --card-chrome: #0E2440;
  --card-line:   #1C3B5E;
  --card-punct:  #6B87A6;   /* 4.6:1 on --card-bg */
  margin-top: 3.2rem;   /* sits level with the headline, not the status pill */
  background: var(--card-bg);
  border: 1px solid var(--card-line);
  border-radius: var(--r);
  overflow: hidden;
  box-shadow: var(--shadow);
}
.idcard-bar {
  display: flex; align-items: center; gap: .7rem;
  padding: .6rem .85rem;
  background: var(--card-chrome);
  border-bottom: 1px solid var(--card-line);
}
.idcard-dots { display: flex; gap: .38rem; }
.idcard-dots i { width: .62rem; height: .62rem; border-radius: 50%; background: #4A4160; }
.idcard-dots i:nth-child(1) { background: #E05A54; }
.idcard-dots i:nth-child(2) { background: #E0B145; }
.idcard-dots i:nth-child(3) { background: #4FB06A; }
.idcard-file {
  font-family: var(--f-mono); font-size: .72rem; color: var(--card-punct);
}
.idcard-body {
  margin: 0; padding: 1.15rem 1.25rem;
  overflow-x: auto;
  font-family: var(--f-mono); font-size: .78rem; line-height: 1.85;
  color: var(--card-punct);
  tab-size: 2;
}
/* the shared highlighter emits these; restate them so the card keeps its
   own colours regardless of which theme the surrounding page is in */
.idcard-body .t-key { color: #FF8A73; }   /*  7.5:1 on --card-bg */
.idcard-body .t-str { color: #8FD0E8; }   /* 10.1:1 */
.idcard-body .t-num { color: #A8C4E0; }   /*  9.5:1 */
.idcard-body .t-kw  { color: #A8C4E0; font-weight: 400; }

.figures {
  display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: 1px; margin-top: clamp(2.2rem, 5vw, 3.4rem);
  background: var(--line); border: 1px solid var(--line); border-radius: var(--r);
  overflow: hidden;
}
/* The lift uses the `translate` property, not `transform`. These cards live in
   a .rv.stagger container whose settled rule sets `transform: none` at a higher
   specificity than any :hover here, which silently killed the lift. `translate`
   is a separate property, so the entrance animation and the hover compose
   instead of overwriting each other -- no specificity golf. */
.figure {
  background: var(--surface); padding: 1.15rem 1.2rem;
  transition: background .18s ease, translate .18s ease;
}
.figure:hover { background: var(--accent-wash); translate: 0 -2px; }
.figure b { display: block; font-family: var(--f-display); font-size: 1.6rem; letter-spacing: -.035em; }
.figure span { font-size: .78rem; color: var(--ink-faint); }

/* ---------- capabilities ---------- */
.caps { display: grid; grid-template-columns: repeat(auto-fit, minmax(285px, 1fr)); gap: 1rem; }
.cap {
  position: relative;               /* the ghost numeral anchors to the card */
  padding: 1.45rem 1.5rem 1.55rem;
  transition: border-color .18s ease, translate .18s ease, box-shadow .18s ease;
}
.cap:hover { border-color: var(--line-firm); translate: 0 -3px; box-shadow: var(--shadow); }
.cap:hover .ico { border-color: var(--line-firm); }
.cap-ico { margin-bottom: 1rem; }

/* Same treatment as the open process panel: icon tile at the top left, the
   numeral oversized and ghosted at the top right. Out of flow so a long title
   wraps under it instead of being squeezed into a narrow column beside it. */
.cap-n {
  position: absolute; top: 1rem; right: 1.35rem;
  font-family: var(--f-display); font-weight: 700;
  font-size: 3.1rem; line-height: 1; letter-spacing: -.03em;
  color: var(--ink); opacity: .1;
  pointer-events: none; user-select: none;
}
.cap h3 { margin-bottom: .5rem; }
.cap p { color: var(--ink-soft); font-size: .93rem; }
.cap ul { list-style: none; margin-top: 1rem; display: grid; gap: .4rem; }
.cap li { position: relative; padding-left: 1.05rem; font-size: .86rem; color: var(--ink-faint); }
.cap li::before {
  content: ""; position: absolute; left: 0; top: .6em;
  width: 5px; height: 5px; border-radius: 1px; background: var(--line-firm);
}

/* ---------- syntax tokens: article fences + hero profile card ---------- */
.t-com { color: var(--ink-faint); font-style: italic; }
.t-str { color: var(--steel); }
.t-kw  { color: var(--accent-text); font-weight: 500; }
.t-dec { color: var(--amber); }
.t-num { color: var(--amber); }
.t-tag { color: var(--accent-text); }
.t-att { color: var(--amber); }
.t-key { color: var(--accent-text); }

/* ---------- version matrix ---------- */
.matrix { border: 1px solid var(--line); border-radius: var(--r); overflow: hidden; background: var(--surface); }
.matrix th, .matrix td {
  text-align: left; padding: .85rem 1.1rem;
  border-bottom: 1px solid var(--line); font-size: .9rem;
}
.matrix tr:last-child td { border-bottom: 0; }
.matrix th {
  font-family: var(--f-mono); font-size: .72rem; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ink-faint);
  background: var(--bg-alt); font-weight: 400;
}
.matrix td:first-child { font-family: var(--f-mono); font-weight: 500; width: 8rem; }
.matrix td:last-child { color: var(--ink-soft); }
.matrix tbody tr { transition: background .16s ease; }
.matrix tbody tr:hover { background: var(--bg-alt); }
.lvl { display: inline-flex; align-items: center; gap: .45rem; font-size: .82rem; }
.lvl::before { content: ""; width: 8px; height: 8px; border-radius: 2px; background: var(--line-firm); }
.lvl[data-lvl="current"]   { color: var(--accent-text); }
.lvl[data-lvl="current"]::before { background: var(--accent); }
.lvl[data-lvl="supported"] { color: var(--steel); }
.lvl[data-lvl="supported"]::before { background: var(--steel); }
.lvl[data-lvl="legacy"]    { color: var(--ink-faint); }

/* ---------- work ---------- */
.filters { display: flex; flex-wrap: wrap; gap: .45rem; margin-bottom: 1.6rem; }
.filter {
  padding: .42rem .85rem;
  background: transparent; border: 1px solid var(--line); border-radius: 100px;
  font-family: var(--f-mono); font-size: .75rem; color: var(--ink-soft); cursor: pointer;
  transition: color .16s ease, border-color .16s ease, background .16s ease;
}
.filter:hover { border-color: var(--line-firm); color: var(--ink); }
.filter[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); }

/* ---------- selected work: scroll-stacked cases ----------
   Each card is sticky and pins a little lower than the one before it, so the
   next card slides over the last and the earlier ones stay visible as a deck of
   edges. --n is the card's index, set by paintCases on every paint -- which is
   what makes this survive a filter click changing the set.

   No JS drives the stack: it is position: sticky. Note that any
   overflow: hidden on an ancestor of .cases would silently kill it. */
.cases { display: grid; gap: 1rem; }

.case {
  padding: 0;                       /* the body carries the padding now */
  overflow: hidden;                 /* clip the media to the card radius */
  transition: border-color .18s ease, box-shadow .18s ease;
}
.case:hover { border-color: var(--line-firm); box-shadow: var(--shadow); }

@media (min-width: 861px) {
  /* No gap once stacking: a gap is a window onto the pinned card underneath,
     which shows a stray line of the previous card's text mid-slide. */
  .cases { gap: 0; }
  .case {
    display: grid;
    grid-template-columns: minmax(0, 0.8fr) minmax(0, 1.2fr);
    align-items: stretch;
    position: sticky;
    /* Clear the sticky nav, then step each card down so the previous ones show
       as a deck of edges. The step is deliberately small and tightly capped:
       this card is dark-media + light-body, so a wide strip reads as a two-tone
       stripe rather than a card edge, and 16 of them would be noise. */
    top: calc(84px + min(var(--n, 0) * 6px, 36px));
  }
  /* A hover lift on a sticky element fights the pin, so it goes on desktop. */
  .case:hover { transform: none; }
}

/* ---------- work: the same deck on phones, where there is room ------------
   A sticky card TALLER than the scrollport pins and then hides its own lower
   half -- the highlights and stack chips become unreachable. Measured: the
   tallest card is 761px and the nav takes 67px, so the deck needs ~836px.
   Phones at 844/896/926 have that; a 667px-tall screen does not, and there it
   stays the plain scrolling list. Hence the min-height gate: it is not a
   guess, it is the height the content actually requires. */
@media (max-width: 860px) and (min-height: 840px) {
  .cases { gap: 0; }
  .case {
    position: sticky;
    /* Smaller nav and less room than desktop, so a tighter step and a lower
       cap -- the effect is the deck of edges, not the size of the offset. */
    top: calc(72px + min(var(--n, 0) * 4px, 24px));
  }
  .case:hover { transform: none; }
}

/* The image is absolutely positioned, not height:100%. In normal flow (mobile,
   where the card is not a grid) the panel has no definite height, so height:100%
   resolves to auto -- the image then renders at its intrinsic 4:3 and, since
   max-height applies to the panel and not the image, spills out and paints over
   the card heading below. inset:0 makes it fill exactly the panel box instead. */
.case-media { position: relative; overflow: hidden; background: var(--bg-alt); min-height: 220px; }
.case-media img {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  object-fit: cover; display: block;
}
.case-body { padding: clamp(1.35rem, 3vw, 1.9rem); min-width: 0; }

.case-top { display: flex; align-items: baseline; gap: .7rem; flex-wrap: wrap; margin-bottom: .6rem; }
.case-top h3 { margin-right: auto; }
/* Client name accented, engagement type in the body colour -- their pattern:
   AURORA (accent) TECH WEBSITE REDESIGN (plain). */
.case-client { color: var(--accent-text); }
.case-kind-of { color: var(--ink); font-weight: 400; }
.case-v { font-family: var(--f-mono); font-size: .73rem; color: var(--accent-text); }
.case-y { font-family: var(--f-mono); font-size: .73rem; color: var(--ink-faint); }
.case-body > p { color: var(--ink-soft); font-size: .94rem; max-width: 58rem; }
.case-cols { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 300px); gap: clamp(1.2rem, 3vw, 2.5rem); margin-top: 1.15rem; }
.case-hl { list-style: none; display: grid; gap: .5rem; }
.case-hl li { position: relative; padding-left: 1.3rem; font-size: .88rem; color: var(--ink-soft); }
.case-hl li::before {
  content: "\2192"; position: absolute; left: 0; top: 0;
  color: var(--accent-text); font-family: var(--f-mono);
}
.case-stack { display: flex; flex-wrap: wrap; gap: .4rem; align-content: start; padding-left: clamp(1.2rem, 3vw, 2.5rem); border-left: 1px solid var(--line); }
.case-sector { font-size: .78rem; color: var(--ink-faint); font-family: var(--f-mono); }
/* marks work that is mine rather than a client engagement -- the distinction
   has to survive being skim-read, so it sits next to the sector, not in prose */
.case-kind {
  display: inline-block; margin-left: .6rem;
  padding: .1rem .48rem;
  border: 1px solid var(--line-firm); border-radius: 100px;
  font-size: .68rem; color: var(--ink-soft);
}

/* ---------- modules ---------- */
/* Cards, not a hairline table: same .panel ground and the same lift as .cap,
   with the process section's icon tile. */
.mods { display: grid; grid-template-columns: repeat(auto-fit, minmax(255px, 1fr)); gap: 1rem; }
.mod {
  display: flex; flex-direction: column; align-items: flex-start;
  padding: 1.3rem 1.35rem 1.4rem;
  transition: border-color .18s ease, translate .18s ease, box-shadow .18s ease;
}
.mod:hover { border-color: var(--line-firm); translate: 0 -3px; box-shadow: var(--shadow); }
.mod:hover .ico { border-color: var(--line-firm); }
.mod-ico { margin-bottom: 1rem; }
.mod h3 { font-size: 1.02rem; }
.mod p { font-size: .87rem; color: var(--ink-soft); margin: .4rem 0 1rem; }
/* auto pushes the chip to the card floor, so a short summary does not leave the
   version floating mid-card while its neighbours sit low. */
.mod .chip { margin-top: auto; }

/* ---------- technology stack ---------- */
.stack { display: grid; gap: 0; }
.sgroup {
  display: grid; grid-template-columns: minmax(0, 10rem) minmax(0, 1fr);
  gap: clamp(.8rem, 3vw, 2rem);
  padding: 1.15rem 0;
  border-top: 1px solid var(--line);
  align-items: start;
}
.stack .sgroup:last-child { border-bottom: 1px solid var(--line); }
.sgroup > b {
  font-family: var(--f-mono); font-size: .78rem; font-weight: 500;
  color: var(--ink-faint); padding-top: .18rem;
  transition: color .16s ease;
}
.sgroup:hover > b { color: var(--accent-text); }
.stags { display: flex; flex-wrap: wrap; gap: .4rem; }

@media (max-width: 640px) {
  .sgroup { grid-template-columns: minmax(0, 1fr); gap: .6rem; }
}

/* ---------- process ---------- */
/* Narrow screens keep the plain stacked list: a horizontal accordion needs
   width, and every panel is open here so nothing is reachable only by hover. */
.steps { display: grid; gap: 0; }
.step {
  display: grid; grid-template-columns: 3.5rem minmax(0, 1fr);
  gap: clamp(1rem, 3vw, 2rem);
  padding: 1.4rem 0;
  border-top: 1px solid var(--line);
}
.steps .step:last-child { border-bottom: 1px solid var(--line); }
.step-head { display: flex; flex-direction: column; align-items: flex-start; gap: .5rem; padding-top: .15rem; }
.step-n { font-family: var(--f-mono); font-size: .78rem; color: var(--accent-text); }
/* The oversized numeral belongs to the horizontal card only -- in the stacked
   list the rows are not cards and it would just be noise. */
.step-fig { display: none; }

/* ---------- process: horizontal accordion ----------
   Panels collapse to a numbered rail; the active one expands. Collapsed content
   is clipped by overflow, never display:none, so all five steps stay in the
   accessibility tree no matter which is open. */
@media (min-width: 861px) {
  .steps { display: flex; gap: .55rem; align-items: stretch; }

  /* --- the pin ---
     Height is the viewport plus one screenful-ish per extra panel, so there is
     scroll distance for the accordion to advance through while the row itself
     stays still. --steps comes from main.js. */
  .steps-pin { height: calc(100vh + (var(--steps, 5) - 1) * 26vh); }
  .steps-stick {
    position: sticky;
    top: 84px;                        /* clears the sticky nav */
    /* Full pinned viewport holding the heading and the row as one centred
       block. Must stay shorter than .steps-pin or there is nothing to stick
       within, so this is a min-height on a frame that is already exactly that
       tall -- it only grows if the heading wraps on a short, wide screen, and
       the pin is taller than a viewport either way. */
    min-height: calc(100vh - 84px);
    display: flex; flex-direction: column; justify-content: center;
    gap: clamp(1.75rem, 5vh, 3rem);
    padding-block: 2rem;
  }
  /* the flex gap above owns the spacing now */
  .steps-stick > .head { margin-bottom: 0; }

  .step {
    /* reset the stacked-list rules */
    display: flex; flex-direction: column; justify-content: space-between;
    grid-template-columns: none; gap: .9rem;
    border-top: 0;
    padding: 1.05rem 1.05rem;

    position: relative;             /* the ghost numeral anchors to the card */
    flex: 0 0 4.85rem;              /* collapsed rail: padding + the icon tile */
    min-width: 0; min-height: 12.5rem;
    background: var(--surface);
    border: 1px solid var(--line);
    border-radius: var(--r);
    overflow: hidden;
    cursor: pointer;
    transition: flex-basis .52s cubic-bezier(.22, .61, .36, 1),
                background .28s ease, border-color .28s ease;
  }
  .steps .step:last-child { border-bottom: 1px solid var(--line); }

  .step.on {
    /* Sized to the 21rem measure plus padding, NOT flex: 1 1 auto -- growing
       into the leftover row width gave a 807px card carrying 336px of text,
       so two thirds of the open panel was empty. Fixed basis also means the
       row's total width never changes as panels swap. */
    flex: 0 0 25rem;
    border-color: var(--line-firm);
  }

  .step-head { padding-top: 0; gap: .6rem; }
  .step-ico { width: 2.75rem; height: 2.75rem; }
  .step-ico svg { width: 1.25rem; height: 1.25rem; }

  /* Collapsed, the rail is icon over number and that number is the only
     wayfinding there. Open, the ghost numeral below takes the job and this one
     fades out -- it stays in the accessibility tree either way.

     Out of flow for the same reason as .step-fig: in flow it reserved its line
     box on every card including the open one where it is invisible, which cost
     the whole row ~40px of height. Absolute puts it under the icon tile
     without charging for it. */
  .step-n {
    position: absolute;
    top: calc(1.05rem + 2.75rem + .5rem);   /* padding + tile + gap */
    left: 1.05rem;
    font-size: .88rem;
    transition: opacity .2s ease;
  }
  .step.on .step-n { opacity: 0; }

  /* Out of flow, so a collapsed rail clips it instead of reserving width for
     it -- the card's overflow: hidden does the work. */
  .step-fig {
    display: block;
    position: absolute; top: .5rem; right: 1.1rem;
    font-family: var(--f-display); font-weight: 700;
    font-size: 3.5rem; line-height: 1; letter-spacing: -.03em;
    color: var(--ink); opacity: 0;
    pointer-events: none; user-select: none;
    transition: opacity .3s ease;
  }
  .step.on .step-fig { opacity: .12; transition-delay: .14s; }

  .step-in {
    /* A fixed measure keeps the text from reflowing while the panel animates --
       a percentage width would re-wrap every frame.

       max-width MUST NOT be a percentage here: in a collapsed panel 100% is
       ~40px, which clamps this measure away and wraps the paragraph into a
       sliver hundreds of pixels tall, dragging the whole row's height with it.
       The panel clips with overflow: hidden, so letting it exceed is the point. */
    width: 21rem; max-width: none;
    opacity: 0;
    transition: opacity .26s ease;
  }
  .step.on .step-in { opacity: 1; transition-delay: .14s; }
  .step:not(.on) .step-in { pointer-events: none; }

  .step h3 { margin-bottom: .5rem; }
  .step-in p { color: var(--ink-soft); font-size: .9rem; }
}
.step h3 { margin-bottom: .35rem; }
.step p { color: var(--ink-soft); font-size: .93rem; max-width: 46rem; }

/* ---------- writing ---------- */
.posts { display: grid; grid-template-columns: repeat(auto-fit, minmax(275px, 1fr)); gap: 1rem; }
.post { display: flex; flex-direction: column; overflow: hidden; transition: border-color .18s ease, translate .18s ease, box-shadow .18s ease; }
.post:hover { border-color: var(--line-firm); translate: 0 -3px; box-shadow: var(--shadow); }
.post-top { aspect-ratio: 16 / 7; display: grid; place-items: center; border-bottom: 1px solid var(--line); }
.post-top svg { width: 28px; height: 28px; color: #fff; opacity: .9; }
/* Names match the palette, so an article's `accent:` reads as what it draws. */
.post-top[data-accent="blueprint"] { background: linear-gradient(135deg, #08192E, #1E4674); }
.post-top[data-accent="steel"]     { background: linear-gradient(135deg, #2E4257, #5A7290); }
.post-top[data-accent="vermilion"] { background: linear-gradient(135deg, #8E3323, #E2543A); }
.post-in { padding: 1.2rem 1.3rem 1.35rem; display: flex; flex-direction: column; flex: 1; }
.post-tag { font-family: var(--f-mono); font-size: .71rem; color: var(--accent-text); margin-bottom: .55rem; }
.post h3 { font-size: 1.04rem; margin-bottom: .45rem; }
.post p { color: var(--ink-soft); font-size: .89rem; flex: 1; }
.post-foot {
  display: flex; align-items: center; gap: .55rem;
  margin-top: 1.1rem; padding-top: .85rem; border-top: 1px solid var(--line);
  font-family: var(--f-mono); font-size: .72rem; color: var(--ink-faint);
}
.post-foot i { width: 3px; height: 3px; border-radius: 50%; background: var(--line-firm); }

/* ---------- contact ---------- */
.contact { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr); gap: clamp(2rem, 5vw, 4rem); align-items: start; }
.contact-side p { color: var(--ink-soft); margin-top: 1rem; }
.rows { display: grid; margin-top: 2rem; }
.row {
  display: flex; align-items: center; justify-content: space-between; gap: 1rem;
  padding: .85rem 0; border-top: 1px solid var(--line); font-size: .92rem;
  transition: padding-left .18s ease, color .18s ease;
}
.rows .row:last-child { border-bottom: 1px solid var(--line); }
.row:hover { padding-left: .5rem; color: var(--accent-text); }
.row span:last-child { font-family: var(--f-mono); font-size: .74rem; color: var(--ink-faint); }

.form { padding: clamp(1.35rem, 3vw, 1.85rem); }
.f { margin-bottom: 1rem; }
.f label { display: block; margin-bottom: .38rem; font-size: .81rem; font-weight: 600; color: var(--ink-soft); }
.f input, .f textarea, .f select {
  width: 100%; padding: .7rem .85rem;
  background: var(--bg); border: 1px solid var(--line); border-radius: var(--r-sm);
  font-size: .92rem;
  transition: border-color .16s ease, background .16s ease;
}
.f textarea { resize: vertical; min-height: 112px; }
.f input:focus, .f textarea:focus, .f select:focus { outline: none; border-color: var(--accent); background: var(--surface); }
.f2 { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
.note { margin-top: .85rem; font-size: .77rem; color: var(--ink-faint); text-align: center; }
/* the note doubles as the form's status line; --bad is its failure state */
.note--bad { color: var(--ink); }
.note--bad a { color: var(--accent-text); text-decoration: underline; text-underline-offset: 3px; }
.pot { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }

/* ---------- footer ---------- */
.foot { border-top: 1px solid var(--line); padding-block: 2.3rem; }
.foot .wrap { display: flex; flex-wrap: wrap; gap: 1rem 1.5rem; align-items: center; }
.foot p { font-size: .81rem; color: var(--ink-faint); margin-right: auto; }
.foot nav { display: flex; gap: 1.1rem; }
.foot nav a { font-size: .81rem; color: var(--ink-soft); }
.foot nav a:hover { color: var(--accent-text); }
.foot small { display: block; width: 100%; font-size: .74rem; color: var(--ink-faint); }

/* ---------- article ---------- */
.article { padding-block: clamp(2.4rem, 6vw, 4.2rem) var(--band); }
.article-in { max-width: 44rem; margin-inline: auto; }
.back { font-family: var(--f-mono); font-size: .78rem; color: var(--ink-faint); }
.back:hover { color: var(--accent-text); }
.article-head { margin: 1.5rem 0 2.3rem; padding-bottom: 1.7rem; border-bottom: 1px solid var(--line); }
.article-head h1 { font-size: clamp(1.9rem, 4.4vw, 2.85rem); margin-bottom: 1rem; }
.article-head .post-foot { border: 0; padding: 0; margin-top: 1rem; }
.article-body { font-size: 1.02rem; }
.article-body > * + * { margin-top: 1.1rem; }
.article-body h2 { font-size: 1.4rem; margin-top: 2.5rem; }
.article-body h3 { font-size: 1.1rem; margin-top: 1.9rem; }
.article-body p, .article-body li { color: var(--ink-soft); }
.article-body strong { color: var(--ink); font-weight: 700; }
.article-body ul, .article-body ol { padding-left: 1.3rem; display: grid; gap: .45rem; }
.article-body code {
  font-family: var(--f-mono); font-size: .86em;
  background: var(--bg-alt); border: 1px solid var(--line);
  padding: .12em .36em; border-radius: 4px; color: var(--ink);
}
.article-body pre {
  background: var(--surface); border: 1px solid var(--line);
  border-radius: var(--r); padding: 1.05rem 1.15rem; overflow-x: auto;
}
.article-body pre code { background: none; border: 0; padding: 0; font-size: .83rem; line-height: 1.72; }
.article-body blockquote { margin-left: 0; padding: .2rem 0 .2rem 1.2rem; border-left: 2px solid var(--accent); }
.article-body blockquote p { color: var(--ink); font-style: italic; }
.article-body hr { border: 0; border-top: 1px solid var(--line); margin-block: 2.1rem; }
.article-body a { color: var(--accent-text); text-decoration: underline; text-underline-offset: 3px; }

/* ---------- reveal (gated on .js so no-JS shows content) ---------- */
.js .rv { opacity: 0; transform: translateY(14px); transition: opacity .55s ease, transform .55s ease; }
.js .rv.in { opacity: 1; transform: none; }

/* A .stagger container is only a trigger -- it does not fade itself, or the
   children would fade twice and read muddy. Children cascade off --i, which
   js/main.js sets once per container. */
.js .rv.stagger { opacity: 1; transform: none; transition: none; }
.js .rv.stagger > * {
  opacity: 0; transform: translateY(12px);
  transition: opacity .5s ease, transform .5s ease;
}
.js .rv.stagger.in > * {
  opacity: 1; transform: none;
  transition-delay: calc(var(--i, 0) * 60ms);
}

/* ---------- per-character heading reveal ----------
   Each letter rises, un-zooms and un-blurs, staggered. js/main.js does the
   splitting; everything below is the motion.

   A [data-split] element is only a trigger -- like .stagger, it does not fade
   itself, because the characters carry the movement. Without this exemption the
   heading would fade as a block AND resolve per letter, which reads muddy. */
.js .rv[data-split] { opacity: 1; transform: none; transition: none; }

.js [data-split] .word { display: inline-block; white-space: nowrap; }
.js [data-split] .char { display: inline-block; opacity: 0; }

@keyframes char-in {
  from { opacity: 0; transform: translateY(0.5em) scale(1.35); filter: blur(6px); }
  to   { opacity: 1; transform: none;                         filter: blur(0); }
}
.js [data-split].in .char {
  /* 'both' matters: 'backwards' alone would revert each letter to the base
     opacity:0 the moment its animation finished. */
  animation: char-in .78s cubic-bezier(.16, .84, .34, 1) both;
  /* --stagger is set per heading by main.js: a fixed step would make a
     46-character heading take 2.7s to resolve. */
  animation-delay: calc(var(--c, 0) * var(--stagger, 42ms));
  will-change: transform, filter, opacity;
}
/* A blur filter holds a compositing layer open, so it is dropped once the
   letter lands. `animation: none` is load-bearing: an animation's filled
   values outrank a plain declaration, so without clearing the animation the
   filter would stay at blur(0) rather than none. */
.js [data-split] .char.done {
  animation: none;
  opacity: 1;
  filter: none;
  transform: none;
  will-change: auto;
}

/* ---------- hero entrance ----------
   Runs on load rather than on scroll: the hero is above the fold, so an
   IntersectionObserver would fire instantly anyway. 'backwards' fill holds
   each element hidden through its own delay. */
@keyframes rise {
  from { opacity: 0; transform: translateY(18px); }
  to   { opacity: 1; transform: none; }
}
.js .hero .live,
.js .hero .lead,
.js .hero .hero-cta,
.js .hero .idcard { animation: rise .68s cubic-bezier(.22, .61, .36, 1) backwards; }
.js .hero .live     { animation-delay: .06s; }
.js .hero .lead     { animation-delay: .22s; }
.js .hero .hero-cta { animation-delay: .30s; }
.js .hero .idcard   { animation-delay: .16s; }

/* ---------- scroll progress ---------- */
.progress {
  position: fixed; top: 0; left: 0; right: 0; height: 2px;
  z-index: 200; pointer-events: none; background: transparent;
}
.progress i {
  display: block; height: 100%; width: 0;
  background: var(--accent);
  transform-origin: left;
}

@media (prefers-reduced-motion: reduce) {
  .js .rv, .js .rv.stagger > * { opacity: 1; transform: none; transition: none; }
  .js .rv.stagger.in > * { transition-delay: 0s; }
  .js .hero .live, .js .hero .lead,
  .js .hero .hero-cta, .js .hero .idcard { animation: none; }
  /* main.js skips splitting entirely under reduced motion; this covers a
     mid-session preference change, when the spans already exist. */
  .js [data-split] .char { opacity: 1; filter: none; animation: none; }
  .btn:hover { transform: none; }
  .cap:hover, .post:hover, .case:hover, .figure:hover, .mod:hover { translate: none; }
  /* The stack is scroll-driven motion, so it is opt-out too. */
  .case { position: static; }
  /* No pin, and the accordion reverts to the plain stacked list. Unpinning
     alone is not enough: the horizontal layout would survive with four of five
     panels held at opacity 0 and no scroll to advance them, so the content
     would only be reachable by hovering. */
  .steps-pin { height: auto; }
  .steps-stick { position: static; min-height: 0; display: block; padding-block: 0; }
  /* display: block drops the flex gap, so the heading takes its margin back */
  .steps-stick > .head { margin-bottom: clamp(2.2rem, 4vw, 3.4rem); }
  .steps { display: grid; gap: 0; }
  .step {
    display: grid; grid-template-columns: 3.5rem minmax(0, 1fr);
    gap: clamp(1rem, 3vw, 2rem);
    flex: auto;
    min-height: 0; padding: 1.4rem 0;
    border: 0; border-top: 1px solid var(--line); border-radius: 0;
    background: none; cursor: auto; transition: none;
  }
  .steps .step:last-child { border-bottom: 1px solid var(--line); }
  .step-head { padding-top: .15rem; gap: .5rem; }
  .step-ico { width: 2.5rem; height: 2.5rem; }
  .step-ico svg { width: 1.15rem; height: 1.15rem; }
  /* Undo both accordion tricks: the number goes back into the flow (it is a
     grid column here, not an overlay on a card), and .step.on must not blank
     it -- there is no ghost numeral in the stacked list to take over from it.
     That rule outranks a bare .step-n, so it has to be reset by name. */
  .step-n { position: static; font-size: .78rem; transition: none; }
  .step .step-n, .step.on .step-n { opacity: 1; }
  .step-fig { display: none; }
  .step-in { width: auto; opacity: 1; transition: none; }
  .nav-links a::after { transition: none; }
}

/* ---------- responsive ---------- */
@media (max-width: 960px) {
  .hero-top, .contact { grid-template-columns: minmax(0, 1fr); }
  .idcard { margin-top: 0; }
  .case-cols { grid-template-columns: minmax(0, 1fr); }
  .case-stack { padding-left: 0; padding-top: 1.1rem; border-left: 0; border-top: 1px solid var(--line); }
  .case-media { min-height: 170px; max-height: 220px; }
}
@media (max-width: 820px) {
  .nav-links { display: none; }
  .burger { display: block; }
}
@media (max-width: 640px) {
  .matrix td:first-child { width: auto; }
  .matrix th:nth-child(3), .matrix td:nth-child(3) { display: none; }
  .step { grid-template-columns: minmax(0, 1fr); gap: .3rem; }
  /* single column now, so icon and number sit side by side above the title */
  .step-head { flex-direction: row; align-items: center; gap: .6rem; padding-bottom: .3rem; }
}
@media (max-width: 520px) {
  .f2 { grid-template-columns: minmax(0, 1fr); }
  .foot .wrap { flex-direction: column; align-items: flex-start; }
}

@media print {
  .nav, .drawer, .hero-cta, .form, .foot nav, .skip, .filters { display: none !important; }
  body { background: #fff; color: #000; }
}
