/* ==========================================================================
   Ahmed Imad Tarq — portfolio
   Palette: slate-teal neutrals · oxblood accent · brass highlight
   Type:    Bricolage Grotesque (display) · IBM Plex Sans (body) · IBM Plex Mono (meta)
   ========================================================================== */

/* ---------- fonts (self-hosted, subset by tools/subset-fonts.py) ----------

   Each font-weight descriptor below states the range its file can ACTUALLY
   produce after axis trimming. Declaring wider would not unlock those weights;
   it would make the browser synthesise them, which looks like a smeared fake
   bold. If you change a weight in this stylesheet, change the matching range in
   tools/subset-fonts.py and re-run it.

   Bricolage keeps its opsz axis on purpose. Browsers apply optical sizing
   automatically, and this face uses it hard — advance widths differ by ~11% on
   average between opsz 12 and 96 — while the site sets it from 18px
   (.timeline-body h3) to 83px (.hero-name). Pinning opsz would save 28 KB and
   render the small headings with display-grade spacing. */

@font-face {
  font-family: "Bricolage Grotesque";
  font-style: normal;
  font-weight: 600 800;
  /* no font-stretch descriptor: this face's only axes are opsz and wght
     (verified against the shipped woff2 fvar table). Declaring a width range
     it does not have would make the face fail to match if anything ever set
     font-stretch. */
  font-display: swap;
  src: url("fonts/bricolage-grotesque-var.subset.woff2") format("woff2");
}

@font-face {
  font-family: "IBM Plex Sans";
  font-style: normal;
  font-weight: 400 600;
  font-display: swap;
  src: url("fonts/ibm-plex-sans-var.subset.woff2") format("woff2");
}

@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/ibm-plex-mono-400.subset.woff2") format("woff2");
}

@font-face {
  font-family: "IBM Plex Mono";
  font-style: normal;
  font-weight: 500;
  font-display: swap;
  src: url("fonts/ibm-plex-mono-500.subset.woff2") format("woff2");
}

/* Arabic, for /ar/ only. Declaring these here costs the English page nothing:
   a @font-face is not fetched until a glyph on the page actually needs it, and
   nothing in the English page ever will.

   Two weights rather than four because Google Fonts ships this family as static
   faces, not a variable font — see tools/fetch-arabic-font.py. CSS weight
   matching sends 500 down to 400 and 600/800 up to 700, so nothing is
   synthesised. These carry no Latin glyphs; the stack in [dir="rtl"] below
   falls through to IBM Plex Sans for those. */

@font-face {
  font-family: "IBM Plex Sans Arabic";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("fonts/ibm-plex-sans-arabic-400.subset.woff2") format("woff2");
}

@font-face {
  font-family: "IBM Plex Sans Arabic";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("fonts/ibm-plex-sans-arabic-700.subset.woff2") format("woff2");
}

/* ---------- tokens ---------- */

:root {
  /* light theme (porcelain with a green-grey bias) */
  --ground: #f4f5f2;
  --surface: #fbfcfa;
  --surface-2: #ecefe9;
  --line: #d7dcd3;
  --ink: #1e2427;
  --ink-soft: #4c5852;
  --ink-mute: #5c6963;
  --accent: #9e2b3c;          /* oxblood */
  --accent-ink: #ffffff;      /* text on accent */
  --accent-soft: rgba(158, 43, 60, 0.08);
  --brass: #776014;
  --brass-soft: rgba(178, 143, 38, 0.14);
  --ok: #2e7d4f;
  --shadow: 0 1px 2px rgba(30, 36, 39, 0.05), 0 8px 24px rgba(30, 36, 39, 0.06);

  --font-display: "Bricolage Grotesque", "Segoe UI", system-ui, sans-serif;
  --font-body: "IBM Plex Sans", "Segoe UI", system-ui, sans-serif;
  --font-mono: "IBM Plex Mono", "Cascadia Code", Consolas, monospace;

  --container: 68rem;
  --radius: 6px;

  color-scheme: light;
}

@media (prefers-color-scheme: dark) {
  :root {
    --ground: #101517;
    --surface: #171d20;
    --surface-2: #1c2427;
    --line: #2b3538;
    --ink: #e8e6e1;
    --ink-soft: #aab4ae;
    --ink-mute: #86928c;
    --accent: #d97b87;        /* oxblood, lifted for dark ground */
    --accent-ink: #16090c;
    --accent-soft: rgba(217, 123, 135, 0.12);
    --brass: #cfa93f;
    --brass-soft: rgba(207, 169, 63, 0.14);
    --ok: #5cb884;
    --shadow: inset 0 1px 0 rgba(232, 230, 225, 0.05), 0 1px 0 rgba(0, 0, 0, 0.4);

    color-scheme: dark;
  }
}

:root[data-theme="dark"] {
  --ground: #101517;
  --surface: #171d20;
  --surface-2: #1c2427;
  --line: #2b3538;
  --ink: #e8e6e1;
  --ink-soft: #aab4ae;
  --ink-mute: #86928c;
  --accent: #d97b87;
  --accent-ink: #16090c;
  --accent-soft: rgba(217, 123, 135, 0.12);
  --brass: #cfa93f;
  --brass-soft: rgba(207, 169, 63, 0.14);
  --ok: #5cb884;
  --shadow: inset 0 1px 0 rgba(232, 230, 225, 0.05), 0 1px 0 rgba(0, 0, 0, 0.4);

  color-scheme: dark;
}

:root[data-theme="light"] {
  --ground: #f4f5f2;
  --surface: #fbfcfa;
  --surface-2: #ecefe9;
  --line: #d7dcd3;
  --ink: #1e2427;
  --ink-soft: #4c5852;
  --ink-mute: #5c6963;
  --accent: #9e2b3c;
  --accent-ink: #ffffff;
  --accent-soft: rgba(158, 43, 60, 0.08);
  --brass: #776014;
  --brass-soft: rgba(178, 143, 38, 0.14);
  --ok: #2e7d4f;
  --shadow: 0 1px 2px rgba(30, 36, 39, 0.05), 0 8px 24px rgba(30, 36, 39, 0.06);

  color-scheme: light;
}

/* ---------- reset & base ---------- */

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

html {
  scroll-behavior: smooth;
  scroll-padding-top: 5rem;
}

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--font-body);
  font-size: 1rem;
  line-height: 1.65;
  -webkit-font-smoothing: antialiased;
  text-rendering: optimizeLegibility;
}

img {
  max-width: 100%;
  height: auto;
  display: block;
}

h1, h2, h3 {
  font-family: var(--font-display);
  line-height: 1.15;
  text-wrap: balance;
  margin: 0;
}

p {
  margin: 0;
}

a {
  color: var(--accent);
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
}

a:hover {
  text-decoration-thickness: 2px;
}

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

.container {
  max-width: var(--container);
  margin-inline: auto;
  padding-inline: clamp(1.25rem, 4vw, 2.5rem);
}

/* The UA sheet's [hidden] { display: none } is (0,0,1) and loses to any class
   that sets display — .btn is inline-flex, so <button class="btn" hidden> would
   render. script.js relies on `hidden` to keep the copy button out of a no-JS
   page, so it has to actually hide. */
[hidden] {
  display: none !important;
}

/* Available to screen readers, absent from the page. Not display:none, which
   removes it from the accessibility tree along with everything else. */
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  white-space: nowrap;
  clip: rect(0 0 0 0);
  clip-path: inset(50%);
}

/* logical, not `left`: in the RTL page this has to fly out past the right edge */
.skip-link {
  position: absolute;
  inset-inline-start: -999px;
  top: 0;
  background: var(--accent);
  color: var(--accent-ink);
  padding: 0.5rem 1rem;
  z-index: 100;
}

.skip-link:focus {
  inset-inline-start: 0.5rem;
  top: 0.5rem;
}

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

.site-header {
  position: sticky;
  top: 0;
  z-index: 50;
  background: var(--ground);
  border-bottom: 1px solid var(--line);
}

@supports (background: color-mix(in srgb, red 50%, blue)) {
  .site-header {
    background: color-mix(in srgb, var(--ground) 88%, transparent);
    -webkit-backdrop-filter: blur(10px);
    backdrop-filter: blur(10px);
  }
}

.nav {
  max-width: var(--container);
  margin-inline: auto;
  padding: 0.75rem clamp(1.25rem, 4vw, 2.5rem);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
}

.nav-brand {
  font-family: var(--font-mono);
  font-size: 0.9rem;
  font-weight: 500;
  color: var(--ink);
  text-decoration: none;
  letter-spacing: 0.01em;
  white-space: nowrap;
}

.brand-mark {
  color: var(--accent);
}

.nav-menu {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: clamp(0.75rem, 2vw, 1.5rem);
  /* .nav now has a third visible child on desktop (the theme toggle, moved out
     of this menu). Pushing the menu right keeps the layout as
     brand | links theme instead of letting the links drift to the centre.
     Below 800px .nav-menu is position:absolute with left:0/right:0, where an
     auto margin resolves to 0 — so this rule is inert there. */
  margin-inline-start: auto;
}

.nav-link {
  font-size: 0.9rem;
  font-weight: 500;
  color: var(--ink-soft);
  text-decoration: none;
  padding: 0.25rem 0;
  border-bottom: 2px solid transparent;
}

.nav-link:hover,
.nav-link.is-active {
  color: var(--ink);
  border-bottom-color: var(--accent);
}

/* The label is written in the language it switches TO — that is the one string
   a reader who cannot read the current page is still able to recognise.
   It deliberately does NOT name the Arabic font: the Arabic word here would
   otherwise pull a 31 KB face onto the English page for a single word. The
   generic fallback in the body stack renders it with a system Arabic font at
   no cost. */
.lang-switch {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  line-height: 1.4;
  color: var(--ink-soft);
  text-decoration: none;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.3rem 0.6rem;
  white-space: nowrap;
}

.lang-switch:hover {
  color: var(--ink);
  border-color: var(--ink-mute);
}

.theme-toggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.1rem;
  height: 2.1rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  color: var(--ink-soft);
  cursor: pointer;
}

.theme-toggle:hover {
  color: var(--ink);
  border-color: var(--ink-mute);
}

/* show the icon of the theme you'd switch TO */
.icon-moon { display: none; }
.icon-sun  { display: block; }

[data-resolved-theme="light"] .icon-moon { display: block; }
[data-resolved-theme="light"] .icon-sun  { display: none; }

.nav-toggle {
  display: none;
  flex-direction: column;
  justify-content: center;
  gap: 5px;
  width: 2.4rem;
  height: 2.4rem;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  cursor: pointer;
  padding: 0 0.55rem;
}

.nav-toggle-bar {
  height: 2px;
  width: 100%;
  background: var(--ink);
  transition: transform 0.2s ease, opacity 0.2s ease;
}

/* ---------- scroll progress ---------- */

/* Zero JavaScript: a scroll-driven animation, so the browser advances it off
   the main thread and there is no scroll listener to throttle. Browsers without
   scroll timelines simply get no bar — it is decoration, and scaleX(0) leaves
   it invisible rather than stuck at full width.
   .site-header is position:sticky, which makes it the containing block. */
.scroll-progress {
  position: absolute;
  inset-inline: 0;
  bottom: -1px;
  height: 2px;
  transform: scaleX(0);
  transform-origin: 0 50%;
  background: var(--accent);
  pointer-events: none;
}

@keyframes scroll-progress-grow {
  to { transform: scaleX(1); }
}

@supports (animation-timeline: scroll()) {
  .scroll-progress {
    animation: scroll-progress-grow linear;
    animation-timeline: scroll(root block);
  }
}

/* ---------- hero ---------- */

.hero {
  padding-block: clamp(3rem, 9vh, 6.5rem) clamp(2.5rem, 6vh, 4rem);
  border-bottom: 1px solid var(--line);
  overflow: hidden;
}

.hero-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr);
  gap: clamp(2rem, 5vw, 4rem);
  align-items: center;
}

.status-line {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  letter-spacing: 0.04em;
  color: var(--ink-soft);
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: 0.35rem 0.9rem;
  background: var(--surface);
}

.status-dot {
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: var(--ok);
  flex-shrink: 0;
}

.hero-name {
  font-size: clamp(2.9rem, 8vw, 5.2rem);
  font-weight: 800;
  letter-spacing: -0.02em;
  margin-top: 1.4rem;
}

.hero-role {
  font-family: var(--font-mono);
  font-size: clamp(0.95rem, 2vw, 1.1rem);
  color: var(--accent);
  margin-top: 0.9rem;
  letter-spacing: 0.01em;
}

.role-divider {
  color: var(--ink-mute);
}

.hero-lede {
  margin-top: 1.4rem;
  max-width: 34rem;
  font-size: 1.06rem;
  color: var(--ink-soft);
}

.hero-lede strong {
  color: var(--ink);
  font-weight: 600;
}

.hero-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 0.75rem;
  margin-top: 1.8rem;
}

.hero-links {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 1.25rem;
  padding: 0;
  margin: 1.6rem 0 0;
}

.hero-links a {
  display: inline-flex;
  align-items: center;
  gap: 0.45rem;
  font-size: 0.9rem;
  font-weight: 500;
  color: var(--ink-soft);
  text-decoration: none;
}

.hero-links a:hover {
  color: var(--accent);
}

/* portrait */

.hero-portrait {
  justify-self: center;
}

.portrait-frame {
  margin: 0;
  position: relative;
  max-width: 21rem;
}

.portrait-frame::before {
  content: "";
  position: absolute;
  inset: 0 0 auto;
  aspect-ratio: 1 / 1;
  transform: translate(14px, 14px);
  border: 1px solid var(--accent);
  border-radius: var(--radius);
  pointer-events: none;
}

/* <picture> is display:inline by default; wrapping a display:block <img> in an
   inline box leaves a stray line-height gap under the portrait and shifts the
   ::before frame off its corner. */
.portrait-frame picture {
  display: block;
}

.portrait-frame img {
  position: relative;
  border-radius: var(--radius);
  border: 1px solid var(--line);
  filter: saturate(0.8) contrast(1.05);
}

.portrait-caption {
  position: relative;
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.05em;
  color: var(--ink-mute);
  margin-top: 1.35rem;
}

/* stat strip */

.stat-strip {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 1px;
  background: var(--line);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  overflow: hidden;
  margin: clamp(2.5rem, 6vh, 4rem) 0 0;
}

.stat {
  background: var(--surface);
  padding: 1.1rem 1.25rem 1.2rem;
  display: flex;
  flex-direction: column-reverse;
  /* column-reverse packs to the BOTTOM by default, so the one label that wraps
     ("Payment & delivery integrations") pushed its number a line above the
     other three. flex-end is the top in column-reverse: numbers align, each
     label stays tucked under its own. */
  justify-content: flex-end;
  gap: 0.15rem;
}

.stat dd {
  margin: 0;
  font-family: var(--font-display);
  font-size: clamp(1.7rem, 3.5vw, 2.4rem);
  font-weight: 700;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  line-height: 1.1;
}

.stat dt {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-mute);
}

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

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.5rem;
  font-family: var(--font-body);
  font-size: 0.95rem;
  font-weight: 600;
  padding: 0.65rem 1.35rem;
  border-radius: var(--radius);
  text-decoration: none;
  border: 1px solid transparent;
  transition: transform 0.15s ease, background-color 0.15s ease, border-color 0.15s ease;
}

.btn:active {
  transform: translateY(1px);
}

.btn-primary {
  background: var(--accent);
  color: var(--accent-ink);
}

@supports (background: color-mix(in srgb, red 50%, blue)) {
  .btn-primary:hover {
    background: color-mix(in srgb, var(--accent) 88%, var(--ink) 12%);
  }
}

.btn-ghost {
  border-color: var(--line);
  color: var(--ink);
  background: var(--surface);
}

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

/* copy-to-clipboard: the label swaps rather than the button resizing, so the
   contact row does not reflow mid-click */
.btn-copy .copy-idle,
.btn-copy .copy-done {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
}

.btn-copy .copy-done,
.btn-copy.is-copied .copy-idle {
  display: none;
}

.btn-copy.is-copied .copy-done {
  display: inline-flex;
}

.btn-copy.is-copied {
  border-color: var(--ok);
  color: var(--ok);
}

/* ---------- sections ---------- */

.section {
  padding-block: clamp(3.5rem, 9vh, 6rem);
}

.section-alt {
  background: var(--surface);
  border-block: 1px solid var(--line);
}

.section-eyebrow {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  letter-spacing: 0.04em;
  color: var(--accent);
  margin-bottom: 0.6rem;
}

.section-title {
  font-size: clamp(1.8rem, 4.5vw, 2.6rem);
  font-weight: 700;
  letter-spacing: -0.015em;
  margin-bottom: clamp(1.75rem, 4vh, 2.75rem);
}

/* ---------- about ---------- */

.about-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.5fr) minmax(0, 1fr);
  gap: clamp(2rem, 5vw, 4rem);
  align-items: start;
}

.about-copy {
  display: flex;
  flex-direction: column;
  gap: 1.1rem;
  max-width: 40rem;
  color: var(--ink-soft);
}

.about-facts {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  padding: 1.4rem 1.5rem;
  box-shadow: var(--shadow);
}

.facts-title {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-mute);
  margin-bottom: 1rem;
}

.facts-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.9rem;
}

.facts-list li {
  display: flex;
  flex-direction: column;
  gap: 0.1rem;
  font-size: 0.92rem;
}

.fact-key {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-mute);
}

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

.timeline {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
}

.timeline-item {
  display: grid;
  grid-template-columns: 13rem minmax(0, 1fr);
  gap: 1.5rem;
  padding-block: 1.6rem;
  border-top: 1px solid var(--line);
  position: relative;
}

.timeline-item:last-child {
  border-bottom: 1px solid var(--line);
}

.timeline-meta {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 0.5rem;
}

.timeline-date {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  color: var(--ink-mute);
  font-variant-numeric: tabular-nums;
}

.tag {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  padding: 0.15rem 0.55rem;
  border-radius: 3px;
  border: 1px solid var(--line);
  color: var(--ink-soft);
}

.tag-current {
  border-color: transparent;
  background: var(--accent-soft);
  color: var(--accent);
}

.timeline-body h3 {
  font-size: 1.15rem;
  font-weight: 600;
  margin-bottom: 0.45rem;
}

.timeline-body .org {
  color: var(--accent);
}

.timeline-body .org a {
  color: inherit;
  text-decoration: none;
}

.timeline-body .org a:hover {
  text-decoration: underline;
}

.timeline-body p {
  color: var(--ink-soft);
  max-width: 40rem;
}

/* ---------- education & training ---------- */

.education {
  margin-top: clamp(2rem, 4vh, 3rem);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  /* --ground, not --surface: this card sits inside .section-alt, whose own
     background is already --surface, and would otherwise vanish into it */
  background: var(--ground);
  padding: 1.5rem;
}

.education-title {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-mute);
  margin-bottom: 1.1rem;
}

.education-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
  gap: 1.5rem;
  align-items: start;
}

.education h4 {
  font-family: var(--font-display);
  font-size: 1.05rem;
  font-weight: 600;
  line-height: 1.2;
  margin: 0.35rem 0 0.3rem;
}

.education-date {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  color: var(--ink-mute);
  font-variant-numeric: tabular-nums;
}

.education-where {
  color: var(--ink-soft);
  font-size: 0.93rem;
}

.module-list {
  list-style: none;
  margin: 1rem 0 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr));
  gap: 0.85rem 1.5rem;
}

.module-list li {
  position: relative;
  padding-inline-start: 1.1rem;
  font-size: 0.88rem;
  color: var(--ink-soft);
}

.module-list li::before {
  content: "▸";
  position: absolute;
  inset-inline-start: 0;
  color: var(--ink-mute);
}

.module-list b {
  display: block;
  color: var(--ink);
  font-weight: 600;
}

/* ---------- projects ---------- */

.project {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1.6fr);
  gap: clamp(1.5rem, 4vw, 3rem);
  padding: clamp(1.5rem, 3.5vw, 2.5rem);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  box-shadow: var(--shadow);
}

.project + .project {
  margin-top: 1.5rem;
}

.project-name {
  font-size: clamp(1.6rem, 3.5vw, 2.1rem);
  font-weight: 700;
  letter-spacing: -0.01em;
}

.project-tagline {
  color: var(--ink-soft);
  margin-top: 0.5rem;
  max-width: 24rem;
}

.project-stack {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
  padding: 0;
  margin: 1.1rem 0 0;
}

.project-stack li {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.04em;
  color: var(--ink-soft);
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: 0.18rem 0.6rem;
  background: var(--ground);
}

.project-link {
  display: inline-block;
  margin-top: 1rem;
  font-family: var(--font-mono);
  font-size: 0.8rem;
  letter-spacing: 0.04em;
  color: var(--accent);
  text-decoration: none;
}

.project-link:hover {
  text-decoration: underline;
}

.project-points {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 0.75rem;
}

.project-points li {
  position: relative;
  padding-inline-start: 1.3rem;
  color: var(--ink-soft);
  font-size: 0.95rem;
}

.project-points li::before {
  content: "▸";
  position: absolute;
  inset-inline-start: 0;
  top: 0;
  color: var(--ink-mute);
}

.project-points strong {
  color: var(--ink);
  font-weight: 600;
}

/* ---------- architecture diagrams ---------- */

/* .project is a two-column grid; a third child would otherwise drop into the
   narrow first column of a new row instead of spanning the card */
.project-extra {
  grid-column: 1 / -1;
}

.diagram-figure {
  margin: 1.6rem 0 0;
}

/* tabindex="0" in the markup plus this overflow makes the diagram operable
   without a pointer: a scrollable region that cannot be reached by keyboard
   fails WCAG 2.1.1, and on a phone these are always scrollable. */
.diagram-scroll {
  overflow-x: auto;
  /* belt and braces with the minmax(0, 1fr) tracks in the responsive block:
     a grid or flex child defaults to min-width:auto, which would let the
     620px diagram inside push this box — and the page — wider than the screen */
  min-width: 0;
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--ground);
  padding: 1rem;
}

.diagram {
  display: block;
  width: 100%;
  height: auto;
  /* Below roughly this width the 11px labels stop being readable. Scrolling the
     container is the honest trade — shrinking an already-small diagram to fit a
     375px phone produces something nobody can read at any zoom level. */
  min-width: 620px;
}

/* every colour comes from the theme tokens, so both palettes and the print
   override work with no duplicated SVG */
.diagram .box {
  fill: var(--surface);
  stroke: var(--line);
  stroke-width: 1;
}

.diagram .box-accent {
  fill: var(--accent-soft);
  stroke: var(--accent);
}

.diagram .box-muted {
  fill: var(--surface-2);
  stroke: var(--line);
}

.diagram text {
  font-family: var(--font-mono);
  font-size: 11px;
  fill: var(--ink-soft);
}

.diagram .t-title {
  font-size: 12px;
  font-weight: 500;
  fill: var(--ink);
}

.diagram .t-accent {
  fill: var(--accent);
}

.diagram .t-mute {
  font-size: 10px;
  fill: var(--ink-mute);
}

.diagram .flow {
  fill: none;
  stroke: var(--ink-mute);
  stroke-width: 1.25;
}

.diagram .flow-accent {
  stroke: var(--accent);
  stroke-dasharray: 4 3;
}

.diagram .arrow-head {
  fill: var(--ink-mute);
}

.diagram .arrow-head-accent {
  fill: var(--accent);
}

.diagram-caption {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.05em;
  color: var(--ink-mute);
  margin-top: 0.8rem;
}

/* ---------- case study disclosure ---------- */

.case {
  margin-top: 1.4rem;
  border-top: 1px solid var(--line);
  padding-top: 1.2rem;
}

.case > summary {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  letter-spacing: 0.04em;
  color: var(--accent);
  cursor: pointer;
  list-style: none;
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
}

/* Safari still paints the old-style disclosure triangle through this
   pseudo-element rather than ::marker */
.case > summary::-webkit-details-marker {
  display: none;
}

.case > summary::before {
  content: "+";
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.15rem;
  height: 1.15rem;
  border: 1px solid var(--line);
  border-radius: 3px;
  font-weight: 500;
}

.case[open] > summary::before {
  content: "–";
}

.case > summary:hover::before {
  border-color: var(--accent);
}

.case-body {
  display: flex;
  flex-direction: column;
  gap: 1rem;
  margin-top: 1.2rem;
}

.case-body h4 {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-mute);
  margin: 0 0 0.35rem;
}

.case-body p {
  color: var(--ink-soft);
  font-size: 0.95rem;
  max-width: 46rem;
}

/* ---------- skills ---------- */

.skills-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
  gap: 1.25rem;
}

.skill-group {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface-2);
  padding: 1.25rem 1.35rem 1.4rem;
}

.skill-group h3 {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-mute);
  margin-bottom: 0.9rem;
}

.chip-list {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

.chip-list li {
  font-size: 0.83rem;
  font-weight: 500;
  color: var(--ink-soft);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 3px;
  padding: 0.22rem 0.7rem;
}

/* ---------- achievements ---------- */

.awards-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
  gap: 1.25rem;
}

.award {
  border: 1px solid var(--line);
  border-radius: var(--radius);
  background: var(--surface);
  padding: 1.5rem 1.5rem 1.6rem;
  box-shadow: var(--shadow);
  display: flex;
  flex-direction: column;
  gap: 0.7rem;
  align-items: flex-start;
}

.award-place {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  font-weight: 500;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  padding: 0.2rem 0.65rem;
  border-radius: 3px;
  background: var(--accent-soft);
  color: var(--accent);
}

.award-place.award-first {
  background: var(--brass-soft);
  color: var(--brass);
}

.award h3 {
  font-size: 1.1rem;
  font-weight: 600;
}

.award p {
  color: var(--ink-soft);
  font-size: 0.93rem;
}

/* ---------- contact ---------- */

.section-contact {
  text-align: center;
  border-top: 1px solid var(--line);
  background:
    radial-gradient(ellipse 60% 55% at 50% 0%, var(--accent-soft), transparent 70%),
    var(--ground);
}

.section-contact .section-eyebrow,
.section-contact .section-title {
  text-align: center;
}

.availability {
  display: inline-flex;
  align-items: center;
  gap: 0.6rem;
  font-family: var(--font-mono);
  font-size: 0.8rem;
  letter-spacing: 0.04em;
  color: var(--ink-soft);
  border: 1px solid var(--line);
  border-radius: 3px;
  background: var(--surface);
  padding: 0.4rem 0.95rem;
  margin-bottom: 1.3rem;
}

.contact-lede {
  max-width: 32rem;
  margin-inline: auto;
  color: var(--ink-soft);
}

/* ---------- contact form ---------- */

.contact-form {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 1rem;
  max-width: 40rem;
  margin: 2.2rem auto 0;
  /* the section is centred; form labels and inputs are not */
  text-align: start;
}

.field {
  display: flex;
  flex-direction: column;
  gap: 0.35rem;
  /* an <input> carries an intrinsic min-content width from its `size` default,
     which a grid item's min-width:auto would honour over width:100% */
  min-width: 0;
}

.field-wide {
  grid-column: 1 / -1;
}

.contact-form label {
  font-family: var(--font-mono);
  font-size: 0.72rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-mute);
}

.contact-form input,
.contact-form textarea {
  font-family: var(--font-body);
  font-size: 0.95rem;
  line-height: 1.5;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: var(--radius);
  padding: 0.6rem 0.8rem;
  width: 100%;
}

.contact-form textarea {
  resize: vertical;
  min-height: 7rem;
}

.contact-form input:focus-visible,
.contact-form textarea:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: var(--accent);
}

.form-row {
  display: flex;
  align-items: center;
  gap: 1rem;
  flex-wrap: wrap;
}

.form-status {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  color: var(--ink-mute);
  margin: 0;
}

.form-status.is-ok {
  color: var(--ok);
}

.form-status.is-error {
  color: var(--accent);
}

/* Honeypot. Not display:none and not .visually-hidden: display:none is the
   first thing a scraper skips, and .visually-hidden stays in the accessibility
   tree, where a screen-reader user would meet a field they must not fill.
   aria-hidden + tabindex="-1" in the markup keep it away from real people. */
.honeypot {
  position: absolute;
  inset-inline-start: -9999px;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}

@media (max-width: 600px) {
  .contact-form {
    grid-template-columns: 1fr;
  }
}

.contact-actions {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 0.75rem;
  margin-top: 1.8rem;
}

.contact-links {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 1.5rem;
  margin: 1.6rem 0 0;
  padding: 0;
}

.contact-links a {
  font-family: var(--font-mono);
  font-size: 0.8rem;
  letter-spacing: 0.04em;
  color: var(--ink-soft);
}

.contact-links a:hover {
  color: var(--accent);
}

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

.site-footer {
  border-top: 1px solid var(--line);
  padding-block: 1.4rem;
  background: var(--surface);
}

.footer-row {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  gap: 0.75rem;
  font-family: var(--font-mono);
  font-size: 0.8rem;
  letter-spacing: 0.04em;
  color: var(--ink-mute);
}

.footer-status {
  display: inline-flex;
  align-items: center;
  gap: 0.5rem;
}

/* ---------- reveal animation ---------- */

/* hide-for-reveal only when JS is confirmed running (html.js set inline in <head>) */
.js .reveal {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity 0.55s ease, transform 0.55s ease;
}

.reveal.is-visible {
  opacity: 1;
  transform: none;
}

/* ---------- hero entrance ---------- */

/* The hero deliberately does NOT use .reveal. An element at opacity:0 is not
   an LCP candidate at all, so gating the <h1> on IntersectionObserver pushed
   Largest Contentful Paint out past script.js parsing and execution.
   This is pure CSS, runs at first paint, and animates TRANSFORM ONLY — adding
   opacity here would re-break LCP for exactly the same reason. */
@keyframes hero-rise {
  from { transform: translateY(14px); }
  to   { transform: none; }
}

@media (prefers-reduced-motion: no-preference) {
  .hero-copy > *,
  .hero-portrait {
    animation: hero-rise 0.5s cubic-bezier(0.2, 0.6, 0.2, 1) both;
  }

  /* total stagger capped at 250ms — the whole hero is settled before a
     reader could finish the first line */
  .hero-copy > :nth-child(2) { animation-delay: 50ms; }
  .hero-copy > :nth-child(3) { animation-delay: 100ms; }
  .hero-copy > :nth-child(4) { animation-delay: 150ms; }
  .hero-copy > :nth-child(5) { animation-delay: 200ms; }
  .hero-copy > :nth-child(6) { animation-delay: 250ms; }
  .hero-portrait { animation-delay: 100ms; }
}

/* Blanket rule, not a whitelist. This used to name .reveal, .btn and
   .nav-toggle-bar individually, which meant every animation added later
   silently escaped the user's reduced-motion preference. */
@media (prefers-reduced-motion: reduce) {
  html {
    scroll-behavior: auto;
  }

  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-delay: 0ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    transition-delay: 0ms !important;
    scroll-behavior: auto !important;
  }

  /* reveal targets must land visible, not merely finish their transition fast */
  .js .reveal {
    opacity: 1 !important;
    transform: none !important;
  }

  /* The blanket rule above is written for time-driven animations, where
     duration 0.01ms means "land on the end state immediately" — correct.
     Applied to the scroll-driven progress bar it means the entire timeline is
     consumed in the first 0.01ms of scroll, i.e. a bar pinned at 100% from the
     top of the page. That is worse than no bar, so remove it outright. */
  .scroll-progress {
    display: none;
  }
}

/* no-JS fallback: never hide content.
   transition:none matters — this class can be set late (see the script.js
   watchdog in index.html), and without it 40 elements would cross-fade at
   once, which reads as a glitch rather than a page. */
.no-observer .reveal {
  opacity: 1;
  transform: none;
  transition: none;
}

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

/* Every single-column track below is minmax(0, 1fr) rather than a bare 1fr,
   and that is load-bearing rather than pedantic.

   A `1fr` track has an automatic minimum of min-content: it refuses to shrink
   below the widest thing inside it. The architecture diagrams set
   `min-width: 620px` on purpose — their labels stop being readable below that,
   so their own container scrolls instead. With a bare `1fr` that 620px
   propagated all the way out and made the whole PAGE 620px wide, so on a
   390px phone the hero text, the stat labels and the nav all ran off the right
   edge. minmax(0, …) is what lets the track shrink and hands the overflow back
   to .diagram-scroll, where it belongs.

   The desktop rules further up already do this; only these mobile overrides
   had dropped it. */
@media (max-width: 900px) {
  .hero-grid {
    grid-template-columns: minmax(0, 1fr);
  }

  .hero-portrait {
    justify-self: start;
    order: -1;
  }

  .portrait-frame {
    max-width: 15rem;
  }

  .stat-strip {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }

  .about-grid {
    grid-template-columns: minmax(0, 1fr);
  }

  .project {
    grid-template-columns: minmax(0, 1fr);
  }

  .timeline-item {
    grid-template-columns: minmax(0, 1fr);
    gap: 0.6rem;
  }

  .timeline-meta {
    flex-direction: row;
    align-items: center;
  }
}

@media (max-width: 800px) {
  .nav-toggle {
    display: flex;
    /* .nav-menu leaves the flow here, so this is what groups the hamburger
       and the theme toggle together at the end of the header row */
    margin-inline-start: auto;
  }

  .nav-menu {
    position: absolute;
    top: 100%;
    inset-inline: 0;
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    background: var(--ground);
    border-bottom: 1px solid var(--line);
    padding: 0.5rem clamp(1.25rem, 4vw, 2.5rem) 1rem;
    display: none;
  }

  .nav-menu.is-open {
    display: flex;
  }

  .nav-link {
    padding: 0.7rem 0;
    border-bottom: 1px solid var(--line);
  }

  .nav-link:hover,
  .nav-link.is-active {
    border-bottom-color: var(--accent);
  }

  /* the theme toggle is a direct child of .nav now and stays in the header
     row at this breakpoint, so it no longer needs the stacked-in-the-dropdown
     treatment it used to have here */

  .nav-toggle[aria-expanded="true"] .nav-toggle-bar:first-child {
    transform: translateY(3.5px) rotate(45deg);
  }

  .nav-toggle[aria-expanded="true"] .nav-toggle-bar:last-child {
    transform: translateY(-3.5px) rotate(-45deg);
  }
}

@media (max-width: 480px) {
  .stat dd {
    font-size: 1.5rem;
  }

  .hero-actions .btn {
    flex: 1 1 100%;
  }
}

/* The nav row carries brand, hamburger, language switch and theme toggle. The
   brand is the only one that is not a control, so it is the one that gives way
   first — the "~/" mark alone still reads as the mark. */
@media (max-width: 430px) {
  .brand-name {
    display: none;
  }
}

/* ---------- Arabic / RTL ---------- */

/* Everything above this point is written in logical properties, so direction is
   handled by the layout engine rather than by a mirrored stylesheet. What
   remains here is the handful of things that are genuinely direction-dependent
   rather than merely side-dependent. */

[dir="rtl"] {
  /* Bricolage Grotesque has no Arabic coverage whatsoever, so Arabic headings
     fall to the Arabic face — and Bricolage is deliberately NOT kept as the
     Latin fallback behind it.

     Keeping it looked like the brand-preserving choice: a Latin run inside an
     Arabic heading ("ASP.NET Core", "PostgreSQL") would still be drawn in the
     display face. But the Arabic subset carries no Latin glyphs and no digits,
     so that fallback fires on every stat number too — which means the whole
     61 KB face downloads on every Arabic page load to set a handful of Latin
     words. Measured, it was the single largest asset on /ar/.

     IBM Plex Sans is already loaded, is the same superfamily as the Arabic
     face, and pairs with it more coherently than Bricolage does. Dropping
     Bricolage here takes about 20% off the Arabic page. */
  --font-display: "IBM Plex Sans Arabic", "IBM Plex Sans", "Segoe UI", system-ui, sans-serif;
  --font-body: "IBM Plex Sans Arabic", "IBM Plex Sans", "Segoe UI", system-ui, sans-serif;
  /* --font-mono is deliberately unchanged. It only ever sets route labels,
     dates and stack names, all of which stay Latin by design. */
}

/* Arabic has no letter case, so text-transform: uppercase is a no-op on it —
   but letter-spacing is worse than a no-op: it prises apart the joins that make
   a word a word. Both have to come off every label that uses them. */
[dir="rtl"] .stat dt,
[dir="rtl"] .facts-title,
[dir="rtl"] .fact-key,
[dir="rtl"] .skill-group h3,
[dir="rtl"] .education-title,
[dir="rtl"] .case-body h4,
[dir="rtl"] .contact-form label,
[dir="rtl"] .award-place,
[dir="rtl"] .tag {
  text-transform: none;
  letter-spacing: 0;
}

/* a bullet has to point the way the line runs */
[dir="rtl"] .project-points li::before,
[dir="rtl"] .module-list li::before {
  content: "◂";
}

/* inset:0 0 auto already flips, but a raw translate does not: without this the
   offset frame lands across the portrait instead of beside it */
[dir="rtl"] .portrait-frame::before {
  transform: translate(-14px, 14px);
}

/* fills from the edge the reader starts at */
[dir="rtl"] .scroll-progress {
  transform-origin: 100% 50%;
}

/* The architecture diagrams stay left-to-right. Their labels are Latin
   technical terms and their arrows encode a pipeline, not a reading order —
   mirroring them would reverse the direction of the thing being described. */
[dir="rtl"] .diagram-scroll,
[dir="rtl"] .diagram {
  direction: ltr;
}

/* The chip rows mix Arabic labels with Latin product names, and a chip like
   "ASP.NET Core (.NET 8)" or "Git / GitHub" carries bidi-neutral characters —
   parentheses, slashes, dots — that the algorithm would resolve against the
   paragraph direction and shunt to the wrong end. `plaintext` resolves each
   chip against its OWN first strong character instead, so the Latin ones read
   left-to-right and the Arabic ones right-to-left, while the row itself still
   flows RTL like the rest of the page. Setting dir="ltr" on the list would
   have fixed the chips and reversed the row. */
[dir="rtl"] .chip-list li,
[dir="rtl"] .project-stack li {
  unicode-bidi: plaintext;
}

/* the mono eyebrows read "GET /about" — a Latin phrase whose slash and spaces
   are bidi-neutral, so in an RTL paragraph the algorithm would reorder it to
   "about/ GET". The markup carries dir="ltr"; this keeps it left-aligned in a
   right-aligned column rather than centred on its own line. */
[dir="rtl"] .section-eyebrow[dir="ltr"],
[dir="rtl"] .footer-status[dir="ltr"] {
  text-align: right;
}

/* ---------- print ---------- */

/* Ctrl+P is a real screening ritual and this page used to print almost blank:
   any section the visitor had not scrolled to was still .reveal / opacity:0. */
@media print {
  /* the bug fix — unconditionally, whatever the observer did or did not do */
  .js .reveal,
  .reveal {
    opacity: 1 !important;
    transform: none !important;
  }

  /* Force the light palette. A bare :root is (0,1,0) and would LOSE to
     :root[data-theme="dark"] at (0,2,0) — a media query contributes no
     specificity — so a dark-theme visitor would print light-grey ink. */
  :root,
  :root[data-theme="dark"],
  :root[data-theme="light"] {
    --ground: #fff;
    --surface: #fff;
    --surface-2: #fff;
    --ink: #000;
    --ink-soft: #222;
    --ink-mute: #444;
    --line: #999;
    --accent: #7a1f2d;
    --accent-soft: transparent;
    --brass: #6b5410;
    --brass-soft: transparent;
    --shadow: none;

    color-scheme: light;
  }

  @page {
    margin: 14mm;
  }

  body {
    background: #fff;
    font-size: 10.5pt;
    line-height: 1.45;
  }

  /* navigation, theme switching and decoration have no meaning on paper.
     .site-header covers .scroll-progress and .contact-actions covers the
     copy-to-clipboard button, both of which are children of those. */
  .site-header,
  .site-footer,
  .skip-link,
  .hero-links,
  .hero-actions,
  .hero-portrait,
  .contact-actions,
  .contact-form,
  .nav-toggle,
  .theme-toggle {
    display: none !important;
  }

  /* the diagrams are the best thing on the printed page — but the scroll box
     that makes them work on a phone would guillotine them at the paper edge */
  .diagram-scroll {
    overflow: visible;
    padding: 0;
    border: 0;
  }

  .diagram {
    min-width: 0;
  }

  /* script.js opens every <details> on beforeprint; these keep the expanded
     case studies from being split across a page break */
  .case,
  .diagram-figure {
    break-inside: avoid;
  }

  /* the disclosure affordance is meaningless once it is unconditionally open */
  .case > summary::before {
    display: none;
  }

  .section-contact {
    background: none;
  }

  /* .stat-strip draws its dividers as gap:1px over a background colour, which
     printers strip — without real borders all four stats merge into one box */
  .stat-strip {
    background: none;
    border: 0;
    gap: 0;
  }

  .stat {
    border: 1px solid #999;
  }

  /* .project-link's visible label already IS the URL; a print href rule would
     render "sendyiq.com ↗ https://sendyiq.com" */
  .project-link span,
  .role-divider {
    display: none;
  }

  a {
    text-decoration: none;
  }

  .timeline-item,
  .project,
  .award,
  .skill-group,
  .stat {
    break-inside: avoid;
  }

  .section {
    padding-block: 1.2rem;
  }

  .section-title {
    break-after: avoid;
  }
}
