/* ============================================================================
   ahachi.dev — Design Tokens
   Adapted from aliabdaal.com's system (Recoleta/Elza → Fraunces/Inter)
   for a warm, friendly, recruiter-facing engineering portfolio.

   USAGE
   -----
   1. Load the Google Fonts <link> in <head>, then this file, then style.css:
        <link rel="stylesheet" href="tokens.css">
        <link rel="stylesheet" href="style.css">
   2. Fonts are loaded via <link> in the HTML rather than @import here, so the
      browser can start fetching them in parallel with this stylesheet.
      The URL must include the SOFT and WONK axes or font-variation-settings
      below silently does nothing:
        family=Fraunces:opsz,wght,SOFT,WONK@9..144,600,0..100,0..1
   3. Rule of thumb: if a number isn't in this file, it shouldn't be in
      style.css. Add it here first.
   ========================================================================= */

/* ----------------------------------------------------------------------------
   Metric-matched fallback for Fraunces.

   Georgia renders this heading 29% wider than Fraunces at the same size, so
   while the webfont is loading the French <h1> wraps to three lines and then
   snaps to two — shoving the rest of the hero up by 78px. That single reflow
   was the site's entire CLS (0.107; the English page, whose heading fits two
   lines either way, measured 0.008).

   Times is only 6.2% wider than Fraunces, so scaling it to 93.8% makes the
   fallback occupy the same space and the swap becomes invisible. If none of
   the local() faces resolve, this @font-face is simply skipped and the stack
   falls through to Georgia as before.
   ------------------------------------------------------------------------- */
@font-face {
  font-family: 'Fraunces fallback';
  src: local('Times New Roman'), local('Liberation Serif'), local('Tinos'), local('Times');
  size-adjust: 93.8%;
  font-weight: 600;
  font-style: normal;
}

/* Same trick for the body face: Inter runs ~8% wider than Arial/Helvetica, so
   the hero paragraph gains a line when Inter arrives and pushes the buttons
   down. Arial, Helvetica and Liberation Sans share metrics, so one value
   covers Windows, macOS and Linux. */
@font-face {
  font-family: 'Inter fallback';
  src: local('Arial'), local('Helvetica Neue'), local('Helvetica'), local('Liberation Sans');
  size-adjust: 107.9%;
}

:root {

  /* =========================================================================
     1. COLOUR
     ---------------------------------------------------------------------
     THE RULE: accent colours are SURFACES. Text on them is always --ink-1.
     Never use --surface-* as a text colour. For coloured text, use --on-*.
     Contrast ratios below are measured against #FFFFFF.
     ====================================================================== */

  /* --- Warm background ramp (the source of the "friendly" feeling) ------ */
  --bg-1: #FFFFFF;    /* cards, elevated surfaces                         */
  --bg-2: #FAF7F4;    /* page default, hero                               */
  --bg-3: #F3EDE9;    /* alternating sections, contact block              */
  --bg-4: #E9E1DB;    /* deepest — badges, subtle fills                   */

  /* --- Ink ramp (purple-tinted, sits WITH the warm bg, not against) ----
     Ratios below are against --bg-3 (#F3EDE9), the DARKEST surface any of
     these actually sit on — not against white. Measuring against white is
     what lets muted text quietly fail: the original #76737C is 4.7:1 on
     white but only 4.0:1 on --bg-3, which is where the footer and the
     section eyebrows live.                                                */
  --ink-1: #1B1624;   /* 15.1:1 — headings, body, text on accent surfaces */
  --ink-2: #54505B;   /*  6.8:1 — secondary text, labels                  */
  --ink-3: #68656D;   /*  4.9:1 — muted text. AA on bg-1 / bg-2 / bg-3.   */
  --ink-4: #8D8A91;   /*  2.9:1 — BORDERS + DECORATION ONLY. Never text.  */

  /* --- Lines ----------------------------------------------------------- */
  --line:        #E5DCD5;   /* default hairline border                    */
  --line-strong: #D4C7BE;   /* emphasised border, active states           */

  /* --- ACCENT SURFACES (backgrounds only — put --ink-1 on top) --------- */
  --surface-sky:      #5DCDF1;   /* ink on top =  9.7:1 ✅ primary CTA    */
  --surface-coral:    #FD976D;   /* ink on top =  8.3:1 ✅ highlights     */
  --surface-amber:    #FDD46B;   /* ink on top = 12.5:1 ✅ badges         */
  --surface-violet:   #C9B1FB;   /* ink on top =  9.4:1 ✅               */
  --surface-green:    #79D287;   /* ink on top =  9.6:1 ✅ availability   */

  /* --- Tinted surfaces (soft fills for pills, tags, chips) ------------- */
  --tint-sky:    #E6F7FD;
  --tint-coral:  #FFEDE5;
  --tint-amber:  #FFF6DC;
  --tint-green:  #E6F6EA;

  /* --- ACCENT TEXT (darkened so they actually pass AA) ------------------
     Use these ANY time an accent colour must be text.                   */
  --on-sky:    #0B5E7A;   /* 7.3:1 ✅ links, active nav, project links   */
  --on-coral:  #B04A22;   /* 5.5:1 ✅ warm emphasis                     */
  --on-amber:  #8A5A00;   /* 5.9:1 ✅ status badges, meta               */
  --on-green:  #2E7D4F;   /* 5.1:1 ✅ "available" text                  */
  --on-violet: #6247AA;   /* 7.0:1 ✅                                    */

  /* --- Semantic roles (use these in components, not raw colours) ------- */
  --brand:          var(--surface-sky);
  --brand-text:     var(--on-sky);
  --page-bg:        var(--bg-2);
  --text:           var(--ink-1);
  --text-muted:     var(--ink-2);
  --text-subtle:    var(--ink-3);

  /* Text sitting ON an accent surface. Deliberately NOT --ink-1: the accent
     surfaces stay bright in dark mode, so their label has to stay dark in
     both themes or the primary button turns into white-on-cyan.            */
  --on-accent:      #1B1624;

  /* The highest fill in the stack — inline <code>. In light mode that is the
     lightest surface, in dark mode the lightest is --bg-4, so the chip needs
     its own name rather than hard-coding --bg-1.                           */
  --fill-raised:    var(--bg-1);

  /* Translucent sticky header. Tokenised because the dark theme needs its
     own value and rgba() can't be derived from --bg-2.                     */
  --header-bg:      rgba(250, 247, 244, 0.85);

  /* Image treatment. Light mode leaves every asset alone; the dark theme
     overrides these (see "DARK THEME" below).                              */
  --logo-filter:     none;
  --media-filter:    none;
  --portrait-filter: none;


  /* =========================================================================
     2. TYPOGRAPHY
     ====================================================================== */

  --font-heading: 'Fraunces', 'Fraunces fallback', Georgia, 'Times New Roman', serif;
  --font-body:    'Inter', 'Inter fallback', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
  --font-mono:    'JetBrains Mono', ui-monospace, 'SF Mono', Menlo, monospace;

  /* Fluid type scale — five sizes, that's all you need */
  --text-xs:  clamp(0.8125rem, 0.78rem + 0.15vw, 0.875rem);
  --text-sm:  clamp(1rem,      0.97rem + 0.15vw, 1.125rem);
  --text-md:  clamp(1.125rem,  1.05rem + 0.35vw, 1.375rem);
  --text-lg:  clamp(1.5rem,    1.1rem  + 1.8vw,  2.625rem);
  --text-xl:  clamp(2rem,      1.3rem  + 3.2vw,  4rem);
  --text-2xl: clamp(2.5rem,    1.8rem  + 3.4vw,  5.25rem);

  /* Weights — body defaults to 500, NOT 400. This is deliberate:
     on warm off-white, 400 reads thin and washed out.                    */
  --weight-regular:  400;
  --weight-medium:   500;   /* ← body default */
  --weight-semibold: 600;   /* ← the ONLY heading weight */
  --weight-bold:     700;

  /* Line heights — named, six values */
  --lh-one:    1;
  --lh-tiny:   1.1;
  --lh-small:  1.25;   /* headings */
  --lh-medium: 1.4;
  --lh-normal: 1.6;    /* body */
  --lh-large:  2;

  --tracking-tight:  -0.03em;   /* headings — Ali's exact value */
  --tracking-normal: 0;
  --tracking-wide:   0.02em;    /* small caps labels */


  /* =========================================================================
     3. SPACING — eight fluid steps. No fixed px in your layout.
     ====================================================================== */

  --space-2xs: clamp(0.25rem, 0.75vw, 0.5rem);
  --space-xs:  clamp(0.5rem,  1.5vw,  1rem);
  --space-sm:  clamp(0.75rem, 2vw,    1.5rem);
  --space-md:  clamp(1rem,    2.5vw,  2rem);
  --space-lg:  clamp(1.25rem, 4vw,    3rem);
  --space-xl:  clamp(1.5rem,  6.5vw,  4rem);
  --space-2xl: clamp(2rem,    7.5vw,  6rem);
  --space-3xl: clamp(2rem,    8vw,    7.5rem);

  --section-y: var(--space-2xl);   /* vertical rhythm between sections */


  /* =========================================================================
     4. RADII — named by intent, not by pixel value
     ====================================================================== */

  --radius-xs:     3px;
  --radius-sm:     8px;
  --radius-md:     16px;
  --radius-lg:     clamp(1.25rem, 2.5vw, 2rem);   /* cards, photos */
  --radius-round:  9999px;                         /* buttons, pills */
  --radius-circle: 50%;


  /* =========================================================================
     5. SHADOWS — WARM-TINTED, never black.
        Black shadows on a warm background look like dirt.
     ====================================================================== */

  --shadow-xs:      0 1px 2px    rgba(180, 165, 152, 0.14);
  --shadow-sm:      0 2px 8px   -2px rgba(180, 165, 152, 0.22);
  --shadow-md:      0 12px 28px -8px rgba(190, 175, 162, 0.32);
  --shadow-lg:      0 28px 48px -12px rgba(200, 186, 172, 0.42);
  --shadow-ambient: 0 50px 80px -15px rgba(224, 216, 208, 0.65);  /* Ali's exact */


  /* =========================================================================
     6. MOTION — different durations for different jobs.
        (Replaces `transition: all 0.3s ease` everywhere.)
     ====================================================================== */

  --ease:      cubic-bezier(0.2, 0, 0.2, 1);
  --ease-out:  cubic-bezier(0.16, 1, 0.3, 1);

  --dur-fast:  120ms;   /* hover / focus feedback */
  --dur-base:  200ms;   /* colour + shadow changes */
  --dur-slow:  320ms;   /* layout / reveal */


  /* =========================================================================
     7. LAYOUT
     ====================================================================== */

  --width-prose: 700px;    /* reading column — never wider */
  --width-wide:  1200px;   /* full sections */
  --gutter:      var(--space-lg);
  --header-h:    64px;
}


/* ============================================================================
   1b. DARK THEME
   ----------------------------------------------------------------------------
   Everything above is the light theme AND the fallback. This block re-points
   the colour tokens with light-dark(); no component below knows dark mode
   exists.

   THE FIVE RULES THIS BLOCK FOLLOWS
   ---------------------------------
   1. NO PURE BLACK, NO PURE WHITE. The page is #131019, the brightest ink is
      #EAE7F1. #FFF on #000 is 21:1 — that pairing is what makes dark themes
      buzz and smear (halation), badly so for readers with astigmatism.
      Nothing here exceeds 15.4:1, and nothing sits below AA.

   2. THE HUE COMES FROM THE INK, NOT FROM THE PAPER. The light theme's warmth
      lives in the *background* ramp (#FAF7F4 …). Darkening a warm hue gives
      brown, and brown reads as dirt. So the dark surfaces are built from the
      other half of the palette — --ink-1 #1B1624, a deep violet-black that is
      already the brand's dark colour. Saturation is held at ~22% so it reads
      as a tinted neutral, never as "the purple website".

   3. ELEVATION INVERTS, HIERARCHY DOESN'T. In light mode --bg-1 is the
      LIGHTEST (cards float above the page). Elevated still means "nearer the
      light", so in dark mode --bg-1 is the lightest DARK surface, not the
      darkest. Lightness order flips to bg-2 < bg-3 < bg-1 < bg-4, which keeps
      every card floating above its section instead of sinking into it.

   4. SHADOWS ARE REPLACED, NOT RECOLOURED. A shadow is nearly invisible on a
      dark surface, so depth is carried by (a) the surface step from rule 3 and
      (b) --line becoming a white ALPHA hairline — edge lighting, the way a
      photographer's hair light separates a subject from a dark ground. The
      shadows stay, at ~3× the alpha, only to ground the biggest objects.

   5. ACCENTS GET DESATURATED ~22 POINTS. A colour that reads as friendly on
      #FAF7F4 vibrates on #131019. Surfaces lose 22 points of saturation and 4
      of lightness; accent TEXT is re-picked light instead of dark, landing at
      7.4–9.5:1 rather than the 21:1 a naive invert would give.

   Ratios in the comments are measured against --bg-1, the LIGHTEST surface any
   of these sit on — the mirror of the light theme, which measures against
   --bg-3, its darkest. Measuring against the page colour is what lets muted
   text quietly fail on the one surface where it matters.
   ========================================================================= */

@supports (color: light-dark(white, black)) {

  /* Which scheme is live. `light dark` follows the OS; the two [data-theme]
     rules are the visitor's explicit override, set on <html> by the inline
     script in <head> before this file is even parsed — so there is no flash
     of the wrong theme. color-scheme must be on the ROOT, not on body, or the
     browser leaves scrollbars, form controls and the canvas in light mode. */
  :root                     { color-scheme: light dark; }
  :root[data-theme='light'] { color-scheme: light; }
  :root[data-theme='dark']  { color-scheme: dark; }

  /* Paper is always paper. */
  @media print { :root { color-scheme: light; } }

  :root {

    /* --- Surfaces — see rule 3. Lightness order: bg-2 < bg-3 < bg-1 < bg-4 */
    --bg-1: light-dark(#FFFFFF, #241F30);   /* cards, trust bar, skills      */
    --bg-2: light-dark(#FAF7F4, #131019);   /* page default, hero — deepest  */
    --bg-3: light-dark(#F3EDE9, #1B1725);   /* alternating sections, footer  */
    --bg-4: light-dark(#E9E1DB, #2F2940);   /* raised fills — highest        */

    /* --- Ink — ratios against --bg-1 (the lightest dark surface) --------- */
    --ink-1: light-dark(#1B1624, #EAE7F1);  /* 13.1:1 — off-white, not #FFF  */
    --ink-2: light-dark(#54505B, #ADA7BA);  /*  6.9:1 — secondary text       */
    --ink-3: light-dark(#68656D, #9A94A7);  /*  5.5:1 — muted. AA everywhere */
    --ink-4: light-dark(#8D8A91, #736C82);  /*  3.2:1 — DECORATION ONLY      */

    /* --- Lines become EDGE LIGHT (rule 4) --------------------------------
       White alpha rather than a fixed grey, so one value reads correctly on
       all four surface steps instead of only on the one it was picked for. */
    --line:        light-dark(#E5DCD5, rgba(255, 255, 255, 0.10));
    --line-strong: light-dark(#D4C7BE, rgba(255, 255, 255, 0.22));

    /* --- Accent SURFACES — saturation −22, lightness −4 (rule 5) ---------
       --on-accent (#1B1624) on top measures 7.3–10.8:1 on every one.      */
    --surface-sky:    light-dark(#5DCDF1, #5EBAD9);   /*  8.0:1 ✅ */
    --surface-coral:  light-dark(#FD976D, #EA906C);   /*  7.3:1 ✅ */
    --surface-amber:  light-dark(#FDD46B, #EAC66C);   /* 10.8:1 ✅ */
    --surface-violet: light-dark(#C9B1FB, #BFA9EF);   /*  8.5:1 ✅ */
    --surface-green:  light-dark(#79D287, #7ABD84);   /*  8.0:1 ✅ */

    /* --- Tinted fills — the pale washes become deep, low-saturation ones.
       Inverting the light tints would give near-white chips.              */
    --tint-sky:   light-dark(#E6F7FD, #132F3D);
    --tint-coral: light-dark(#FFEDE5, #39231A);
    --tint-amber: light-dark(#FFF6DC, #382E15);
    --tint-green: light-dark(#E6F6EA, #16311E);

    /* --- Accent TEXT — light instead of dark, held to one lightness band
       (L≈68–78) so the five hues read as one family. First ratio is against
       --bg-1, second against the matching --tint-* they sit on.           */
    --on-sky:    light-dark(#0B5E7A, #81CBE4);   /* 8.8:1 · 7.7:1 ✅ */
    --on-coral:  light-dark(#B04A22, #E8A487);   /* 7.7:1 · 7.1:1 ✅ */
    --on-amber:  light-dark(#8A5A00, #E5C476);   /* 9.5:1 · 8.0:1 ✅ */
    --on-green:  light-dark(#2E7D4F, #8BD09A);   /* 8.8:1 · 7.8:1 ✅ */
    --on-violet: light-dark(#6247AA, #BBA5E9);   /* 7.4:1           ✅ */

    /* --- Surfaces that can't be derived --------------------------------- */
    --fill-raised: light-dark(#FFFFFF, #2F2940);
    --header-bg:   light-dark(rgba(250, 247, 244, 0.85), rgba(19, 16, 25, 0.82));

    /* --- Shadows (rule 4) — warm and faint on paper, near-black and ~3×
       the alpha here, because a 14%-black shadow on #131019 is nothing.  */
    --shadow-tint-xs:      light-dark(rgba(180, 165, 152, 0.14), rgba(0, 0, 0, 0.45));
    --shadow-tint-sm:      light-dark(rgba(180, 165, 152, 0.22), rgba(0, 0, 0, 0.50));
    --shadow-tint-md:      light-dark(rgba(190, 175, 162, 0.32), rgba(0, 0, 0, 0.55));
    --shadow-tint-lg:      light-dark(rgba(200, 186, 172, 0.42), rgba(0, 0, 0, 0.60));
    --shadow-tint-ambient: light-dark(rgba(224, 216, 208, 0.65), rgba(0, 0, 0, 0.70));

    --shadow-xs:      0 1px 2px         var(--shadow-tint-xs);
    --shadow-sm:      0 2px 8px    -2px var(--shadow-tint-sm);
    --shadow-md:      0 12px 28px  -8px var(--shadow-tint-md);
    --shadow-lg:      0 28px 48px -12px var(--shadow-tint-lg);
    --shadow-ambient: 0 50px 80px -15px var(--shadow-tint-ambient);
  }

  /* --- Image treatment -------------------------------------------------
     Filters aren't colours, so light-dark() can't carry them and the two
     dark cases have to be spelled out: the OS preference (unless the
     visitor has explicitly chosen light) and the explicit dark choice.

     The trust logos are the load-bearing case, and the brands keep their
     own colours here — flattening the row to white passes contrast easily
     but strips the section of the thing it exists to show.

     Left as authored on #131019 the row is broken, though. Judge a logo by
     its DARKEST large region, not its average: one bright glyph drags the
     average up while the wordmark beside it disappears.

         Enedis    78% of the mark is rgb(24,24,216)   1.94:1
         Novelis   67% of the mark is rgb(72,72,120)   2.22:1
         Oracle    #C74634                             3.90:1
         Atos      #0596FF                             6.11:1

     brightness() cannot fix that. It MULTIPLIES, so a saturated blue just
     clamps its blue channel and barely moves: blue carries 7.2% of
     luminance, and even pure #0000FF measures 2.19:1 here. Enedis caps at
     2.45:1 however hard you push. Worse, clamping shifts hue — Atos drifts
     205°→191° and visibly turns cyan.

     contrast() below 1 pulls toward mid-grey instead of multiplying, which
     LIFTS a dark colour and leaves hue untouched. Paired with a small
     brightness bump the whole row clears 3:1 with every hue intact:

         Enedis 3.27:1 (240°→240°)   Novelis 4.31:1 (220°→219°)
         Oracle 5.86:1 (7°→7°)       Atos    7.99:1 (205°→205°)

     It also softens them, which is what the trust bar wants — the logos
     read as present rather than shouting. That is the same move rule 5
     makes on the accents, applied to borrowed artwork.

     Screenshots are mostly white pages and would otherwise be the
     brightest thing on the screen, so they lose 12% brightness — handed
     back on hover for the project cards, the ones you're meant to look at.
     The portraits are dimmed far less: a face is not a glare source.     */
  @media (prefers-color-scheme: dark) {
    :root:not([data-theme='light']) {
      --logo-filter:     contrast(0.6) brightness(1.25);
      --media-filter:    brightness(0.88) contrast(1.03);
      --portrait-filter: brightness(0.94);
    }
  }
  :root[data-theme='dark'] {
    --logo-filter:     contrast(0.6) brightness(1.25);
    --media-filter:    brightness(0.88) contrast(1.03);
    --portrait-filter: brightness(0.94);
  }

  /* --- The switch itself -----------------------------------------------
     Armed by script.js for the ~250ms the flip takes, then removed: left
     on permanently it would slow down every hover in the page. Absent
     entirely for reduced-motion users, since this rule would otherwise
     out-specify the global transition-duration override at the bottom of
     this file. scrollbar-color is deliberately NOT in the list — a WebKit
     bug makes the scrollbar flicker whenever it transitions.             */
  @media (prefers-reduced-motion: no-preference) {
    .is-theming, .is-theming *, .is-theming *::before, .is-theming *::after {
      transition: background-color var(--dur-slow) var(--ease),
                  border-color     var(--dur-slow) var(--ease),
                  color            var(--dur-slow) var(--ease),
                  fill             var(--dur-slow) var(--ease),
                  box-shadow       var(--dur-slow) var(--ease),
                  filter           var(--dur-slow) var(--ease) !important;
    }
  }
}


/* ============================================================================
   BASE — apply the system
   ========================================================================= */

*, *::before, *::after { box-sizing: border-box; }

html {
  scroll-behavior: smooth;
  scroll-padding-top: calc(var(--header-h) + var(--space-md));
  -webkit-text-size-adjust: 100%;
}

body {
  margin: 0;
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);       /* ← 500, not 400 */
  line-height: var(--lh-normal);
  color: var(--text);
  background: var(--page-bg);
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

h1, h2, h3, h4 {
  font-family: var(--font-heading);
  font-weight: var(--weight-semibold);     /* ← the ONLY heading weight */
  font-variation-settings: 'SOFT' 60, 'WONK' 0, 'opsz' 96;  /* Recoleta-like */
  line-height: var(--lh-small);
  letter-spacing: var(--tracking-tight);
  color: var(--ink-1);
  margin: 0 0 var(--space-sm);
  text-wrap: balance;
}

h1 { font-size: var(--text-xl); }
h2 { font-size: var(--text-lg); }
h3 { font-size: var(--text-md); }
h4 { font-size: var(--text-sm); font-weight: var(--weight-semibold); }

p { margin: 0 0 var(--space-sm); max-width: var(--width-prose); text-wrap: pretty; }

img, svg { max-width: 100%; }
img { height: auto; }

/* Inline command / identifier names. White fill + hairline rather than a tinted
   fill, so it reads the same on every step of the background ramp. */
code {
  font-family: var(--font-mono);
  font-size: 0.875em;
  color: var(--ink-1);
  background: var(--fill-raised);
  border: 1px solid var(--line);
  border-radius: var(--radius-xs);
  padding: 0.08em 0.4em;
  white-space: nowrap;
}

/* The split-heading technique — neutral phrase + accented phrase.
   <h1>Ingénieur logiciel <em>full-stack</em></h1>                        */
h1 em, h2 em, h3 em {
  font-style: normal;
  color: var(--on-sky);
}


/* ============================================================================
   COMPONENTS
   ========================================================================= */

/* --- Layout ---------------------------------------------------------- */
.container { width: 100%; max-width: var(--width-wide); margin-inline: auto; padding-inline: var(--gutter); }
.prose     { max-width: var(--width-prose); }
section    { padding-block: var(--section-y); }

/* Alternate backgrounds at every section boundary */
.section--tint { background: var(--bg-3); }
.section--card { background: var(--bg-1); }


/* --- Buttons ---------------------------------------------------------
   Note the ASYMMETRIC padding (0.7 top / 0.6 bottom) — optical centering.
   Note the hover INVERTS rather than darkening.                        */
.btn {
  display: inline-flex; align-items: center; justify-content: center;
  gap: var(--space-2xs);
  padding: 0.7rem clamp(1.5rem, 1.75vw, 2.5rem) 0.6rem;
  border: 1px solid transparent;
  border-radius: var(--radius-round);
  font-family: var(--font-body);
  font-size: var(--text-sm);
  font-weight: var(--weight-medium);
  line-height: var(--lh-small);
  text-decoration: none;
  white-space: nowrap;
  cursor: pointer;
  transition: background-color var(--dur-base) var(--ease),
              color            var(--dur-base) var(--ease),
              border-color     var(--dur-base) var(--ease),
              box-shadow       var(--dur-base) var(--ease);
}

.btn--primary {
  background: var(--surface-sky);
  color: var(--on-accent);          /* ← ink on accent. 9.7:1 / 8.0:1 ✅ */
  box-shadow: var(--shadow-sm);
}
.btn--primary:hover,
.btn--primary:focus-visible {
  background: var(--ink-1);         /* ← full inversion */
  color: var(--bg-1);
  box-shadow: var(--shadow-md);
}

.btn--ghost {
  background: transparent;
  color: var(--ink-1);
  border-color: var(--line-strong);
}
.btn--ghost:hover,
.btn--ghost:focus-visible {
  background: var(--ink-1);
  color: var(--bg-1);
  border-color: var(--ink-1);
}

/* Compact variant — header CV button */
.btn--sm {
  padding: 0.45rem 1.1rem 0.35rem;
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
}


/* --- Availability badge ---------------------------------------------- */
.badge-available {
  display: inline-flex; align-items: center; gap: 0.5rem;
  padding: 0.4rem 0.9rem;
  background: var(--tint-green);
  color: var(--on-green);
  border-radius: var(--radius-round);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
}
.badge-available::before {
  content: ''; width: 8px; height: 8px; flex: none;
  border-radius: var(--radius-circle);
  background: var(--surface-green);
}


/* --- Cards ------------------------------------------------------------ */
.card {
  background: var(--bg-1);
  border: 1px solid var(--line);
  border-radius: var(--radius-lg);
  padding: var(--space-md);
  box-shadow: var(--shadow-sm);
  transition: box-shadow  var(--dur-base) var(--ease),
              transform   var(--dur-base) var(--ease-out),
              border-color var(--dur-base) var(--ease);
}
.card:hover {
  box-shadow: var(--shadow-lg);
  transform: translateY(-3px);       /* subtle — not -10px */
  border-color: var(--line-strong);
}

.card__media {
  aspect-ratio: 16 / 9;
  border-radius: var(--radius-md);
  overflow: hidden;
  margin: calc(var(--space-md) * -1) calc(var(--space-md) * -1) var(--space-md);
  border-bottom: 1px solid var(--line);
}
/* --media-filter is `none` in light mode; the dark theme dims screenshots so
   a mostly-white page capture isn't the brightest object on the screen. */
.card__media img {
  width: 100%; height: 100%; object-fit: cover; display: block;
  filter: var(--media-filter);
  transition: filter var(--dur-base) var(--ease);
}


/* --- Tech tags -------------------------------------------------------- */
.tag {
  display: inline-block;
  padding: 0.3rem 0.7rem;
  background: var(--bg-3);
  color: var(--ink-2);
  border: 1px solid var(--line);
  border-radius: var(--radius-sm);
  font-family: var(--font-mono);
  font-size: var(--text-xs);
  font-weight: var(--weight-regular);
  line-height: var(--lh-medium);
}
.tag--primary { background: var(--tint-sky); color: var(--on-sky); border-color: transparent; }
/* Says a role is ongoing without disclosing when it started. */
.tag--current { background: var(--tint-amber); color: var(--on-amber); border-color: transparent; }


/* --- Skill tiers ------------------------------------------------------ */
.skill-tier { margin-bottom: var(--space-lg); }
.skill-tier__label {
  font-family: var(--font-body);
  font-size: var(--text-xs);
  font-weight: var(--weight-semibold);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: var(--space-xs);
}
.skill-tier--1 .tag { font-size: var(--text-sm); padding: 0.45rem 1rem; background: var(--tint-sky);  color: var(--on-sky); border-color: transparent; }
.skill-tier--2 .tag { font-size: var(--text-xs); }
.skill-tier--3 .tag { font-size: var(--text-xs); color: var(--ink-3); background: transparent; }


/* --- Links ------------------------------------------------------------ */
a { color: var(--brand-text); text-underline-offset: 0.2em; }
a:hover { text-decoration: none; }


/* --- Focus: visible, and matched to the system ------------------------ */
:focus-visible {
  outline: 3px solid var(--surface-sky);
  outline-offset: 3px;
  border-radius: var(--radius-xs);
}


/* --- Skip link (first focusable element in <body>) -------------------- */
.skip-link {
  position: absolute; left: -9999px;
  padding: var(--space-xs) var(--space-sm);
  background: var(--ink-1); color: var(--bg-1);
  border-radius: var(--radius-sm); z-index: 999;
  text-decoration: none;
}
.skip-link:focus { left: var(--space-sm); top: var(--space-sm); }


/* ============================================================================
   ACCESSIBILITY — respect motion preferences.
   ========================================================================= */
@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}
