/* ============================================================
   ABI PORTFOLIO — MAIN STYLESHEET
   Font: Clash Display (display) + Satoshi (body) + JetBrains Mono (code)
   ============================================================ */

@import url('https://api.fontshare.com/v2/css?f[]=clash-display@500,600,700&f[]=satoshi@400,500&display=swap');
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500&display=swap');
/* Editorial serif for the display tier (protocol S3). Newsreader is used rather
   than Instrument Serif because Instrument ships regular+italic only, and the
   display tier needs 600/700. Newsreader is variable (200-800) with optical
   sizing, so the 94px hero wordmark still has weight to spare. Instrument Serif
   is kept as the fallback. */
@import url('https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,200..800;1,6..72,200..800&display=swap');

/* ======================== VARIABLES ======================== */
:root {
  --bg:        #f8f7f4;
  --bg-card:   #ffffff;
  --bg-dark:   #111110;
  --ink:       #1a1918;
  --ink-2:     #3d3b38;
  --ink-3:     #6f6b66;
  --ink-4:     #b0ada8;
  --gold:      #b8924a;
  --gold-lt:   #d4aa6a;
  /* Text-safe gold. --gold on paper/white is 2.7-2.9:1 and fails WCAG 1.4.3 for
     small text. --gold-ink is the same hue darkened to 5.0:1+ for any use where
     gold IS the text colour (issuer lines, project sub-labels). Structure keeps
     --gold. Do not use --gold for body or label text. */
  --gold-ink:  #8a6a24;
  --line:      #e5e3de;
  --line-2:    #cac8c2;

  --fd: 'Clash Display', sans-serif;
  --fb: 'Satoshi', sans-serif;
  --fm: 'JetBrains Mono', monospace;
  /* Editorial serif — the display tier. Identity/section titles only; Clash
     Display stays for card titles and the logo, where the geometric grotesque
     is doing a different job. */
  --fserif: 'Newsreader', Georgia, serif;

  --ease-s: cubic-bezier(.16,1,.3,1);
  --ease-i: cubic-bezier(.4,0,.2,1);

  --r2: 2px; --r4: 4px; --r8: 8px;

  /* Intrinsic ratio of the hero portrait (779x595 source) */
  --hero-media-ar: 779 / 595;
  /* Distance the decorative ring is offset behind the portrait */
  --hero-media-inset: 16px;

  --sh-sm: 0 2px 8px rgba(0,0,0,.06);
  --sh-md: 0 6px 24px rgba(0,0,0,.09);
  --sh-lg: 0 16px 56px rgba(0,0,0,.12);
}

/* ======================== RESET ======================== */
*, *::before, *::after { margin:0; padding:0; box-sizing:border-box; }
/* scroll-padding keeps anchor jumps and keyboard focus clear of the 60px fixed
   nav. Without it, tabbing to a target near the top parks it under the bar. */
html { scroll-behavior: smooth; -webkit-font-smoothing: antialiased; scroll-padding-top: 76px; }
body {
  background: var(--bg);
  color: var(--ink);
  font-family: var(--fb);
  overflow-x: hidden;
  cursor: auto;
}
img { display:block; width:100%; }
a { text-decoration: none; color: inherit; }
ul { list-style: none; }

/* ======================== NAV ======================== */
nav {
  position: fixed; top: 0; left: 0; right: 0; z-index: 100;
  display: flex; align-items: center; justify-content: space-between;
  height: 60px; padding: 0 48px;
  /* Opaque, not glass. A blur with no coloured substrate behind it reads as
     fog, costs a full-screen backdrop-filter on every scroll frame, and makes
     nav text contrast depend on whatever is scrolling underneath. */
  background: var(--bg);
  border-bottom: 1px solid transparent;
  transition: border-color .3s, box-shadow .3s;
}
nav.scrolled { border-bottom-color: var(--line); box-shadow: var(--sh-sm); }
.nav-logo {
  font-family: var(--fd); font-weight: 700; font-size: 18px;
  letter-spacing: -.02em; color: var(--ink);
}
.nav-logo em { color: var(--gold-ink); font-style: normal; }
.nav-links { display: flex; gap: 32px; }
.nav-links a {
  font-family: var(--fm); font-size: 11px; font-weight: 400;
  letter-spacing: .1em; text-transform: uppercase; color: var(--ink-3);
  position: relative; padding-bottom: 2px; transition: color .2s;
}
.nav-links a::after {
  content: ''; position: absolute; bottom: 0; left: 0;
  width: 0; height: 1px; background: var(--gold);
  transition: width .3s var(--ease-s);
}
.nav-links a:hover, .nav-links a.active { color: var(--ink); }
.nav-links a:hover::after, .nav-links a.active::after { width: 100%; }
.nav-links a:focus-visible { color: var(--ink); }
.nav-links a:focus-visible::after { width: 100%; }
.nav-btn {
  font-family: var(--fm); font-size: 11px; letter-spacing: .1em;
  text-transform: uppercase; color: var(--ink);
  border: 1.5px solid var(--ink); padding: 8px 18px; border-radius: var(--r2);
  transition: background .2s, color .2s, transform .2s var(--ease-s), box-shadow .2s;
}
.nav-btn:hover { background: var(--ink); color: #fff; }

/* ======================== SECTION BASE ======================== */
section {
  position: relative; z-index: 1;
  padding: 112px 48px;
  max-width: 1160px; margin: 0 auto;
}
.sec-label {
  display: inline-flex; align-items: center; gap: 14px;
  font-family: var(--fm); font-size: 11px; font-weight: 400;
  letter-spacing: .18em; text-transform: uppercase; color: var(--ink-4);
  margin-bottom: 56px;
}
.sec-label::before {
  content: ''; width: 28px; height: 1px; background: var(--gold); display: block;
}

/* ======================== REVEAL ANIMATIONS ========================
   Gated on .js. Without it every [data-reveal] element renders at full opacity
   and no transform, so the page is completely readable if the script fails,
   is blocked, or the IntersectionObserver never fires.

   Travel is 18px, not 40px, and duration is .55s. A 40px rise on ~30 elements
   is the generic fade-up this design should not have. */
.js [data-reveal] {
  opacity: 0;
  transform: translateY(18px);
  transition: opacity .55s var(--ease-s), transform .55s var(--ease-s);
}
.js [data-reveal].in { opacity: 1; transform: none; }
.js [data-reveal="left"]  { transform: translateX(-24px); }
.js [data-reveal="right"] { transform: translateX(24px); }
.js [data-reveal="scale"] { transform: scale(.97); }

[data-delay="1"] { transition-delay: .06s; }
[data-delay="2"] { transition-delay: .12s; }
[data-delay="3"] { transition-delay: .18s; }

/* ======================== HERO ======================== */
#hero {
  min-height: 100vh; display: flex; flex-direction: column;
  justify-content: flex-end; padding-top: 100px; padding-bottom: 80px;
  overflow: hidden; position: relative; border-bottom: 1px solid var(--line);
}

.hero-content { position: relative; z-index: 1; }

.hero-name {
  font-family: var(--fd); font-weight: 600;
  font-size: clamp(34px, 5.2vw, 72px);
  line-height: .9; letter-spacing: -.03em; color: var(--ink-2);
  margin-bottom: 24px; overflow: hidden;
}
.hero-name .word { display: inline-block; overflow: hidden; margin-right: .12em; }
.hero-name .word span {
  display: inline-block;
  animation: wordUp 1s var(--ease-s) both;
  opacity: 0;
}
.hero-name .word:nth-child(1) span { animation-delay: .05s; }
.hero-name .word:nth-child(2) span { animation-delay: .18s; }
.hero-name .word:nth-child(3) span { animation-delay: .31s; }
@keyframes wordUp {
  from { transform: translateY(105%); opacity: 0; }
  to   { transform: translateY(0);    opacity: 1; }
}

.hero-nickname {
  font-family: var(--fd); font-weight: 700;
  font-size: clamp(36px, 6.2vw, 72px);
  line-height: .95; letter-spacing: -.02em;
  text-transform: none; color: var(--ink);
  margin-bottom: 10px;
  opacity: 0; animation: fadeSlide .7s var(--ease-s) .55s both;
}
.hero-role {
  font-family: var(--fd); font-size: clamp(18px, 2.1vw, 30px);
  font-weight: 500; color: var(--ink-2);
  margin-bottom: 18px;
  opacity: 0; animation: fadeSlide .7s var(--ease-s) .7s both;
}
.hero-desc {
  font-family: var(--fb); font-size: clamp(15px, 1.45vw, 21px); line-height: 1.72;
  color: var(--ink-3); max-width: 620px; margin-bottom: 44px;
  opacity: 0; animation: fadeSlide .7s var(--ease-s) .85s both;
}
.hero-status-badge {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  font-family: var(--fm);
  font-size: 10px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: #5f5a54;
  background: #f2ede3;
  border: 1px solid #d7ccb7;
  border-radius: 999px;
  padding: 7px 12px;
  margin-bottom: 18px;
  opacity: 0;
  animation: fadeSlide .7s var(--ease-s) .92s both;
}
@keyframes fadeSlide {
  from { opacity: 0; transform: translateY(16px); }
  to   { opacity: 1; transform: translateY(0); }
}
.hero-ctas {
  display: flex; gap: 14px; flex-wrap: wrap;
  opacity: 0; animation: fadeSlide .7s var(--ease-s) 1s both;
}

/* Proof strip. Sits under the CTAs so the credibility signal lands before the
   scroll, but stays inside .hero-copy so the portrait's pinned top edge
   (align-self: start) is unaffected. Reuses the .proj-tag pill geometry.
   Label is inline with the list on desktop to keep the hero within one screen. */
.hero-proof {
  display: flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 6px 16px;
  margin-top: 24px;
  padding-top: 16px;
  border-top: 1px solid var(--line);
  max-width: 640px;
  opacity: 0;
  animation: fadeSlide .7s var(--ease-s) 1.2s both;
}
.hero-proof-label {
  font-family: var(--fm);
  font-size: 11px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: #6f6b66;
  white-space: nowrap;
}
.hero-proof-list {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.hero-proof-item {
  font-family: var(--fm);
  font-size: 11px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-2);
  background: var(--bg);
  border: 1px solid var(--line-2);
  border-radius: 999px;
  padding: 6px 12px;
  transition: border-color .2s, color .2s;
}
.hero-proof-item:hover { border-color: var(--ink); color: var(--ink); }
.hero-proof-award {
  color: var(--gold-ink);
  background: #fffaf0;
  border-color: #e2ca95;
}
.hero-proof-award:hover { border-color: var(--gold-ink); color: var(--gold-ink); }

/* At >=1024px #hero is an exact-fit min-height:100vh composition. Measured at
   1280px wide the copy column with this strip is ~917px tall, so on any viewport
   under ~940px it pushes the CTAs below the fold and drops the absolutely
   positioned scroll cue and stats out of view. The strip is supplementary — the
   same employers and the medal are stated in Experience and Honors — so it yields
   to the hero on short viewports rather than the other way round. Verified: with
   the strip hidden the hero returns to exactly 100vh at 1280x800. */
@media (min-width: 1024px) and (max-height: 939px) {
  .hero-proof { display: none; }
}
.btn-solid {
  font-family: var(--fm); font-size: 11px; letter-spacing: .1em; text-transform: uppercase;
  background: var(--ink); color: #fff; padding: 14px 30px; border-radius: var(--r2);
  transition: transform .2s var(--ease-s), box-shadow .2s, background .2s;
}
.btn-solid:hover { transform: translateY(-3px); box-shadow: 0 8px 24px rgba(0,0,0,.18); background: #000; }
.btn-outline {
  font-family: var(--fm); font-size: 11px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink); padding: 14px 30px; border-radius: var(--r2);
  border: 1.5px solid var(--line-2);
  transition: transform .2s var(--ease-s), border-color .2s, box-shadow .2s;
}
.btn-outline:hover { border-color: var(--ink); transform: translateY(-3px); box-shadow: var(--sh-sm); }

/* Hero stats */
.hero-stats {
  position: absolute; right: 48px; bottom: 48px;
  display: flex; flex-direction: column; gap: 20px; align-items: flex-end;
  opacity: 0; animation: fadeSlide .7s var(--ease-s) 1.1s both;
}
.h-stat .n {
  font-family: var(--fd); font-size: 32px; font-weight: 700;
  letter-spacing: -.04em; color: var(--ink); line-height: 1;
}
.h-stat .l {
  font-family: var(--fm); font-size: 9px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--ink-4); text-align: right;
}

/* ======================== ABOUT ======================== */
#about { border-top: 1px solid var(--line); }
.about-grid { display: grid; grid-template-columns: 1fr 440px; gap: 80px; align-items: start; }


/* Stack diagram */
.stack-card {
  background: var(--bg-card); border: 1.5px solid var(--line);
  border-radius: var(--r8); padding: 32px;
  position: relative;
}
.stack-title {
  font-family: var(--fm); font-size: 11px; letter-spacing: .14em;
  text-transform: uppercase; color: var(--ink-4); margin-bottom: 20px;
}
.stack-rows { display: flex; flex-direction: column; margin: 0; }
.stack-row {
  display: flex; align-items: baseline; gap: 16px;
  padding: 13px 0; border-top: 1px solid var(--line);
  font-family: var(--fb); font-size: 13px; color: var(--ink-2);
  transition: border-color .2s;
}
/* A definition list, not a stack of cards. The rows are hairlines inside one
   surface, so the block reads as a single spec sheet instead of five identical
   boxes — and there is no proficiency rating to defend, because self-assessed
   "Growing / Strong / Medium" labels are not verifiable by a reader. */
.stack-row:last-child { border-bottom: 1px solid var(--line); }
.stack-row:hover { border-color: var(--line-2); }
.stack-domain { font-family: var(--fd); font-size: 15px; font-weight: 600; color: var(--ink); flex-shrink: 0; min-width: 152px; }
.stack-tools { font-family: var(--fm); font-size: 11px; letter-spacing: .04em; color: var(--ink-3); margin: 0; min-width: 0; }
/* Each tool is an unbreakable item, so "Looker Studio" and "Knowledge Graphs" never
   split across lines. The middle dot is drawn in the gap before every item rather
   than typed between them: a typed dot can strand at the end or start of a wrapped
   line. Here the list clips on its own left edge, so the dot that would sit before
   the first item on each line falls outside the box and is never painted. The
   decorative dot gets an empty alt (`/ ''`) so screen readers read a plain list. */
.stack-list {
  list-style: none; margin: 0; padding: 0;
  display: flex; flex-wrap: wrap; gap: 3px 20px;
  overflow: hidden; line-height: 1.7;
}
.stack-list li { position: relative; white-space: nowrap; }
.stack-list li::before {
  content: '\00b7'; content: '\00b7' / '';
  position: absolute; left: -13px; color: var(--ink-4);
}
/* On a phone the 152px label column leaves the tool list ~100px, which puts every
   tool on its own line and clips the longer nowrap items. The label stacks above
   its tools instead, so the list gets the full row. */
@media (max-width: 600px) {
  .stack-row { flex-direction: column; align-items: stretch; gap: 6px; }
  .stack-domain { min-width: 0; }
}

/* ======================== EXPERIENCE ======================== */
#experience { border-top: 1px solid var(--line); }
.exp-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
.exp-card {
  background: var(--bg-card); border: 1.5px solid var(--line);
  border-radius: var(--r8); overflow: hidden; cursor: pointer;
  transition: border-color .2s;
  position: relative;
}
.exp-card:hover { border-color: var(--line-2); }
.exp-card.open  { border-color: var(--ink); }
.exp-head {
  padding: 26px 26px 20px;
  display: flex; justify-content: space-between; align-items: flex-start;
}
.exp-role    { font-family: var(--fd); font-size: 16px; font-weight: 600; color: var(--ink); margin-bottom: 6px; }
.exp-company { font-family: var(--fm); font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--gold-ink); margin-bottom: 5px; }
.exp-meta    { font-family: var(--fm); font-size: 11px; color: var(--ink-4); line-height: 1.7; }
.exp-toggle  {
  width: 28px; height: 28px; flex-shrink: 0;
  border: 1.5px solid var(--line); border-radius: var(--r4);
  display: flex; align-items: center; justify-content: center;
  font-size: 18px; color: var(--ink-3); font-weight: 300;
  transition: background .2s, color .2s, transform .35s var(--ease-s), border-color .2s;
}
.exp-card.open .exp-toggle { background: var(--ink); color: #fff; border-color: var(--ink); transform: rotate(45deg); }
.exp-preview { padding: 0 26px 20px; font-size: 13px; line-height: 1.7; color: var(--ink-3); }
/* 0fr -> 1fr animates to the body's real height, so open and close take the
   stated time for every role. max-height:800px finished early on short bodies
   and clipped long ones. Closing is quicker than opening. */
.exp-body { display: grid; grid-template-rows: 0fr; overflow: hidden; transition: grid-template-rows .3s var(--ease-s); }
.exp-body > .exp-body-inner { min-height: 0; overflow: hidden; transition: padding .3s var(--ease-s), border-color .3s; }
/* A grid item can't shrink below its own padding, so a closed body drops its
   vertical padding; otherwise ~47px of it stays visible under the trigger. */
.exp-card:not(.open) .exp-body > .exp-body-inner { padding-top: 0; padding-bottom: 0; border-top-color: transparent; }
.exp-card.open .exp-body > .exp-body-inner { transition-duration: .45s; }
.exp-card.open .exp-body { grid-template-rows: 1fr; transition-duration: .45s; }
.exp-body-inner { padding: 20px 26px 26px; border-top: 1px solid var(--line); }
.exp-body-label { font-family: var(--fm); font-size: 11px; letter-spacing: .14em; text-transform: uppercase; color: var(--ink-4); margin-bottom: 12px; }
.exp-list { display: flex; flex-direction: column; gap: 9px; }
.exp-list li { font-size: 13px; line-height: 1.65; color: var(--ink-2); display: flex; gap: 10px; }
.exp-list li::before { content: '›'; color: var(--gold); font-size: 16px; flex-shrink: 0; line-height: 1.4; }

/* ======================== PROJECTS ======================== */
#projects { border-top: 1px solid var(--line); }
/* Two equal columns, always. A bento variant was tried here (flagship span 6,
   then 3+3, then 2+2+2) and reverted: giving the flagship the full 1064px
   measure made it read as a different class of object from the other five
   rather than as the first of six. The flat 2-up grid also keeps every card the
   same size, which is what the CTA-alignment rule in .proj-body assumes. */
.proj-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
.proj-card {
  background: var(--bg-card); border: 1.5px solid var(--line);
  border-radius: var(--r8); overflow: hidden; cursor: pointer;
  transition: border-color .2s;
  position: relative;
  /* Flex column so .proj-body can FILL the leftover height below the image band.
     This is what puts the CTA on the card's bottom edge. See .proj-body. */
  display: flex; flex-direction: column;
}
/* The card does not translate. Six project cards lifting -6px each read as a
   template, and a full-width card that moves also drags the click target out
   from under the pointer mid-gesture. Feedback is the border going to ink plus
   the media easing — enough to read as openable without moving the layout. */
.proj-card:hover { border-color: var(--ink); }
.proj-img-slot {
  width: 100%; height: 200px; background: var(--bg);
  border-bottom: 1px solid var(--line);
  display: flex; align-items: center; justify-content: center;
  overflow: hidden; position: relative;
  flex-shrink: 0; /* .proj-card is a flex column; the media band keeps its height */
}
.proj-img-slot img { width: 100%; height: 100%; object-fit: cover; }
/* Shown only if the image fails to load: a plain plate with the project name,
   never authoring hints like "add image here". */
.proj-img-placeholder,
.cert-img-placeholder {
  display: none; padding: 24px; text-align: center;
  font-family: var(--fd); font-size: 17px; font-weight: 600; color: var(--ink-3);
}
/* Column layout with the CTA pushed to the bottom. Cards in a grid row are
   already the same height, so this lines the "View Technical Details" buttons
   up across the row instead of letting them float at ragged heights.

   `height: 100%` was WRONG here and hid the button entirely: a percentage
   height resolves against the card's full content box, which already contains
   the 200px .proj-img-slot, so the body became as tall as the card instead of
   filling the space beneath the image. It then spilled ~200px past the card,
   and .proj-card's overflow:hidden clipped the CTA out of existence — leaving
   [data-project-open], the only handler that opens the project modal, with
   nothing clickable. Use flex:1 on a flex-column card instead. */
.proj-body { padding: 24px; display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
.proj-num   { font-family: var(--fm); font-size: 11px; letter-spacing: .1em; color: var(--ink-4); margin-bottom: 12px; }
.proj-title { font-family: var(--fd); font-size: 19px; font-weight: 600; color: var(--ink); margin-bottom: 10px; line-height: 1.2; }
.proj-desc  { font-size: 13.5px; line-height: 1.72; color: var(--ink-3); margin-bottom: 18px; }

/* One verified headline number per project, set large, with its sample size
   beside it. Projects without a measured result simply have no figure — the
   slot is never filled with a made-up number. */
.proj-figure { display: flex; align-items: baseline; gap: 12px; margin-bottom: 14px; }
.proj-figure-n { font-family: var(--fd); font-size: 40px; font-weight: 700; letter-spacing: -.03em; line-height: 1; color: var(--ink); }
.proj-figure-l { font-size: 13px; line-height: 1.45; color: var(--ink-3); max-width: 240px; }
/* The measured number is the one thing on the card that gets its own entrance:
   uncovered left to right after the card lands, like a readout settling.
   Gated on .js like every reveal, so with no script it is simply there. */
.js .proj-card[data-reveal] .proj-figure-n { clip-path: inset(-.1em 100% -.1em -.1em); }
.js .proj-card[data-reveal].in .proj-figure-n {
  clip-path: inset(-.1em -.1em -.1em -.1em);
  transition: clip-path .7s var(--ease-s) .3s;
}
.proj-status { font-family: var(--fm); font-size: 11px; letter-spacing: .06em; color: var(--ink-3); margin-bottom: 16px; }
/* Plain text labels in a single run, not chips: the stack is context, the
   numbers above are the point. */
.proj-tags  { font-family: var(--fm); font-size: 11px; letter-spacing: .06em; color: var(--ink-3); margin-bottom: 16px; }

/* ======================== PROJECT MODAL ======================== */
.modal-overlay {
  position: fixed; inset: 0; z-index: 500;
  background: rgba(17,17,16,.6);
  display: flex; align-items: center; justify-content: center;
  padding: 24px; opacity: 0; pointer-events: none;
  transition: opacity .35s var(--ease-s);
}
.modal-overlay.open { opacity: 1; pointer-events: all; }
.modal-box {
  background: var(--bg-card); border-radius: var(--r8);
  width: 100%; max-width: 880px; max-height: 90vh;
  overflow-y: auto; box-shadow: var(--sh-lg);
  /* --modal-dx/--modal-dy point from the viewport centre to the card that
     opened the modal (set in openModal), so the detail grows out of its card
     and shrinks back into it on close. Closing is quicker than opening. */
  transform: translate(var(--modal-dx, 0px), var(--modal-dy, 32px)) scale(.6);
  transition: transform .25s var(--ease-s);
}
.modal-overlay.open .modal-box { transform: none; transition-duration: .45s; }
.modal-bar {
  position: sticky; top: 0; background: var(--bg-card);
  border-bottom: 1px solid var(--line);
  padding: 18px 32px; display: flex; justify-content: space-between; align-items: center;
  z-index: 10;
}
.modal-bar-label { font-family: var(--fm); font-size: 11px; letter-spacing: .16em; text-transform: uppercase; color: var(--ink-4); }
.modal-close {
  width: 32px; height: 32px; border: 1.5px solid var(--line); border-radius: var(--r4);
  background: none; cursor: pointer; display: flex; align-items: center; justify-content: center;
  font-size: 16px; color: var(--ink-3); transition: background .2s, color .2s;
}
.modal-close:hover { background: var(--ink); color: #fff; border-color: var(--ink); }
.modal-content { padding: 40px 32px 56px; }
.modal-title { font-family: var(--fd); font-size: 34px; font-weight: 700; letter-spacing: -.03em; margin-bottom: 6px; }
.modal-sub { font-family: var(--fm); font-size: 11px; letter-spacing: .12em; text-transform: uppercase; color: var(--gold-ink); margin-bottom: 32px; }
.modal-sh { font-family: var(--fm); font-size: 11px; letter-spacing: .14em; text-transform: uppercase; color: var(--ink-4); margin-top: 28px; margin-bottom: 10px; }
.modal-p { font-size: 14.5px; line-height: 1.8; color: var(--ink-2); }
.flow-wrap { margin-top: 28px; padding-top: 22px; border-top: 1px solid var(--line); }
.flow-label { font-family: var(--fm); font-size: 11px; letter-spacing: .14em; text-transform: uppercase; color: var(--ink-3); margin-bottom: 14px; }

/* ======================== CERTIFICATES ======================== */
#certificates { border-top: 1px solid var(--line); }
.cert-grid { display: grid; grid-template-columns: repeat(3,1fr); gap: 16px; }
.cert-card {
  background: var(--bg-card); border: 1.5px solid var(--line);
  border-radius: var(--r8); overflow: hidden;
  position: relative;
}
.cert-img-slot {
  width: 100%; height: 160px; background: var(--bg);
  border-bottom: 1px solid var(--line);
  display: flex; align-items: center; justify-content: center;
  overflow: hidden; position: relative;
}
.cert-img-slot img { width: 100%; height: 100%; object-fit: cover; }
.cert-body { padding: 20px; }
/* No "Verified" pills or badges: the page cannot verify anything itself. A
   credential is backed by its link, or it carries no button at all. */
.cert-issuer { font-family: var(--fm); font-size: 11px; letter-spacing: .1em; text-transform: uppercase; color: var(--gold-ink); margin-bottom: 7px; }
.cert-name   { font-family: var(--fd); font-size: 16px; font-weight: 600; color: var(--ink); margin-bottom: 6px; line-height: 1.3; }
.cert-desc   { font-size: 13px; color: var(--ink-3); margin-top: 7px; line-height: 1.6; }
.section-intro {
  max-width: 760px;
  margin: -24px 0 24px;
  font-size: 14px;
  line-height: 1.75;
  color: var(--ink-2);
}
.cert-level {
  font-family: var(--fm);
  font-size: 11px;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--ink-3);
  margin-bottom: 8px;
}

.cert-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 14px;
}
/* Text links, not filled pills: "open the credential" is a secondary action
   on a card whose content is the credential itself. */
.cert-btn {
  font-family: var(--fm);
  font-size: 11px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink);
  border-bottom: 1px solid var(--line-2);
  padding: 6px 0;
  transition: border-color .2s;
}
.cert-btn:hover { border-color: var(--ink); }

/* ======================== HONORS ======================== */
#honors { border-top: 1px solid var(--line); }
.honor-list { display: flex; flex-direction: column; }
.honor-item {
  display: grid; grid-template-columns: 120px 1fr;
  padding: 32px 0; border-top: 1px solid var(--line);
  position: relative; gap: 0;
  transition: background .2s;
}
.honor-item:last-child { border-bottom: 1px solid var(--line); }
.honor-item::before {
  content: ''; position: absolute; left: 120px; top: 0; bottom: 0;
  width: 1px; background: var(--line);
}
.honor-year { font-family: var(--fm); font-size: 11px; letter-spacing: .06em; color: var(--ink-4); padding-top: 4px; }
.honor-right { padding-left: 32px; }
.honor-title { font-family: var(--fd); font-size: 18px; font-weight: 600; color: var(--ink); margin-bottom: 5px; line-height: 1.25; }
.honor-org   { font-family: var(--fm); font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--gold-ink); margin-bottom: 10px; }
/* Capped at 720px: the honor items span the full 1064px content column, so an
   uncapped .honor-desc ran to ~1100px — the longest measure anywhere on the
   page. Every other prose block is already held to 680-760px (.pg-sub,
   .hero-copy, .section-intro); honors was the one that slipped through. */
.honor-desc  { font-size: 13.5px; line-height: 1.7; color: var(--ink-2); max-width: 720px; }
.honor-meta {
  font-family: var(--fm);
  font-size: 11px;
  letter-spacing: .05em;
  color: var(--ink-3);
  margin-bottom: 10px;
}
.honor-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-top: 12px;
}
.honor-actions a {
  font-family: var(--fm);
  font-size: 11px;
  letter-spacing: .07em;
  text-transform: uppercase;
  color: var(--ink);
  border-bottom: 1px solid var(--line-2);
  padding: 6px 0;
  transition: border-color .2s;
}
.honor-actions a:hover { border-color: var(--ink); }
/* The floating timeline dot was removed — a dot that only exists to fill a 1px
   rule adds nothing a reader can act on. (A dead .honor-item:hover::after
   rule that referenced that dot was removed with it.) */

/* ======================== CONTACT ======================== */
#contact { border-top: 1px solid var(--line); }
/* minmax(0,1fr), not 1fr: a bare 1fr track floors at its content's min-content
   width, and the unbreakable email string below pushed the whole contact
   column past a 320px viewport (46px of clipped overflow). */
.contact-grid { display: grid; grid-template-columns: minmax(0,1fr) minmax(0,1fr); gap: 80px; align-items: start; }
.contact-heading {
  /* The <br> in the markup forces "Got a project" / "or opportunity?". At the old
     clamp(38px,5vw,68px) the second line measured wider than the ~492px grid
     column, so it broke AGAIN and stranded "or" alone on its own line. 58px is
     also the same ceiling as .about-title, so Contact ties About rather than
     out-shouting it. Verified: the heading now renders on 2 lines, not 3. */
  font-family: var(--fd); font-size: clamp(36px, 4.3vw, 58px);
  font-weight: 700; line-height: .95; letter-spacing: -.04em;
  color: var(--ink); margin-bottom: 24px;
  text-wrap: balance;
}
.contact-sub { font-size: 15px; line-height: 1.78; color: var(--ink-2); margin-bottom: 40px; }
.contact-links { display: flex; flex-direction: column; gap: 10px; }
.c-link {
  display: flex; align-items: center; gap: 14px;
  padding: 16px 20px; border: 1.5px solid var(--line); border-radius: var(--r4);
  background: var(--bg-card); font-family: var(--fb); font-size: 14px; color: var(--ink);
  transition: border-color .2s;
}
.c-link:hover { border-color: var(--ink); }
.c-icon { font-size: 18px; width: 24px; text-align: center; flex-shrink: 0; }
/* The address/handle is a single unbreakable token, so it needs an explicit
   break opportunity plus min-width:0 to be allowed to shrink as a flex item. */
.c-text { min-width: 0; overflow-wrap: anywhere; }
.c-label { font-family: var(--fm); font-size: 11px; letter-spacing: .12em; text-transform: uppercase; color: var(--ink-4); margin-left: auto; flex-shrink: 0; }

.form-wrap { display: flex; flex-direction: column; gap: 14px; }
.f-grp { display: flex; flex-direction: column; gap: 6px; }
.f-lbl { font-family: var(--fm); font-size: 11px; letter-spacing: .14em; text-transform: uppercase; color: var(--ink-4); }
.f-inp, .f-txt {
  background: var(--bg-card); border: 1.5px solid var(--line); border-radius: var(--r4);
  padding: 12px 16px; font-family: var(--fb); font-size: 14px; color: var(--ink);
  outline: none; width: 100%;
  transition: border-color .2s, box-shadow .2s;
}
.f-inp:focus, .f-txt:focus { border-color: var(--ink); box-shadow: 0 0 0 3px rgba(26,25,24,.07); }
.f-txt { resize: vertical; min-height: 120px; }
.f-submit {
  align-self: flex-start; font-family: var(--fm); font-size: 11px;
  letter-spacing: .1em; text-transform: uppercase;
  background: var(--ink); color: #fff; border: none;
  padding: 14px 32px; border-radius: var(--r2); cursor: pointer;
  transition: background .2s;
}
.f-submit:hover { background: #000; }

/* ======================== FOOTER ======================== */
footer {
  position: relative; z-index: 1;
  background: var(--bg-dark); color: #fff;
  overflow: hidden;
}
.footer-top {
  max-width: 1160px; margin: 0 auto;
  padding: 80px 48px 56px;
  display: grid; grid-template-columns: 1fr 1fr; gap: 80px;
  border-bottom: 1px solid rgba(255,255,255,.1);
}
.footer-brand .f-logo {
  font-family: var(--fd); font-weight: 700; font-size: 48px;
  letter-spacing: -.04em; color: #fff; line-height: 1; margin-bottom: 16px;
}
.footer-brand .f-logo em { color: var(--gold-lt); font-style: normal; }
.footer-brand p { font-size: 14px; line-height: 1.7; color: rgba(255,255,255,.5); max-width: 320px; }
.footer-links-col { display: flex; flex-direction: column; gap: 10px; margin-top: 4px; }
.footer-col-label { font-family: var(--fm); font-size: 11px; letter-spacing: .16em; text-transform: uppercase; color: rgba(255,255,255,.3); margin-bottom: 12px; }
.footer-link {
  display: flex; align-items: center; gap: 12px;
  font-family: var(--fb); font-size: 14px; color: rgba(255,255,255,.65);
  padding: 10px 0; border-bottom: 1px solid rgba(255,255,255,.07);
  transition: color .2s, gap .2s;
}
.footer-link:hover { color: #fff; gap: 18px; }
.footer-link .fl-arr { font-size: 16px; margin-left: auto; opacity: .4; transition: opacity .2s; }
.footer-link:hover .fl-arr { opacity: 1; }


.footer-bottom {
  max-width: 1160px; margin: 0 auto;
  padding: 24px 48px;
  display: flex; justify-content: space-between; align-items: center;
}
.footer-copy { font-family: var(--fm); font-size: 11px; letter-spacing: .08em; color: rgba(255,255,255,.3); }
.footer-back {
  font-family: var(--fm); font-size: 11px; letter-spacing: .1em; text-transform: uppercase;
  color: rgba(255,255,255,.4); display: flex; align-items: center; gap: 8px;
  transition: color .2s;
}
.footer-back:hover { color: var(--gold-lt); }

/* ======================== RESPONSIVE ======================== */
@media (max-width: 960px) {
  nav { padding: 0 24px; }
  .nav-links { display: none; }
  section { padding: 80px 24px; }
  /* minmax(0,...) throughout: a bare 1fr track floors at its content's
     min-content width, which let the About and Projects columns exceed a
     320px viewport instead of shrinking. */
  .about-grid { grid-template-columns: minmax(0,1fr); gap: 40px; }
  .exp-grid, .proj-grid { grid-template-columns: minmax(0,1fr); }
  .cert-grid { grid-template-columns: minmax(0,1fr) minmax(0,1fr); }
  .contact-grid { grid-template-columns: minmax(0,1fr); gap: 48px; }
  .footer-top { grid-template-columns: 1fr; gap: 40px; }
  .footer-bottom { flex-direction: column; gap: 12px; }
  .hero-stats { display: none; }
  .honor-item { grid-template-columns: 80px 1fr; }
  .honor-item::before { left: 80px; }
}
@media (max-width: 600px) {
  .cert-grid { grid-template-columns: 1fr; }
  .hero-nickname { font-size: clamp(32px, 13vw, 64px); margin-bottom: 8px; }
  .hero-name { font-size: clamp(30px, 10vw, 54px); margin-bottom: 16px; }
  .hero-role { font-size: clamp(17px, 6vw, 24px); }
  .hero-desc { font-size: 15px; line-height: 1.65; }
}

/* ======================== ACCESSIBILITY + STRUCTURE OVERRIDES ======================== */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  white-space: nowrap;
  border: 0;
}

.skip-link {
  position: fixed;
  left: 16px;
  top: -56px;
  z-index: 2000;
  background: var(--ink);
  color: #fff;
  padding: 10px 14px;
  border-radius: var(--r4);
  transition: top .2s var(--ease-s);
}
.skip-link:focus-visible { top: 16px; }

.hero-section {
  max-width: none;
  padding-left: 48px;
  padding-right: 48px;
}
.hero-shell {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(320px, 420px);
  align-items: center;
  gap: 48px;
  max-width: 1160px;
  margin: 0 auto;
}
.hero-copy {
  max-width: 720px;
}
.hero-stats-shell {
  max-width: 1160px;
  margin: 0 auto;
  position: absolute;
  right: 48px;
  bottom: 48px;
}
.about-title {
  font-family: var(--fd);
  font-size: clamp(36px, 4.5vw, 58px);
  font-weight: 700;
  line-height: 1.05;
  letter-spacing: -.035em;
  color: var(--ink);
  margin-bottom: 28px;
}
.about-title .line { display: block; }
.about-em { font-style: italic; color: var(--gold-ink); }
.about-intro { margin-top: 24px; }

/* Desktop-only two-column About. This MUST stay inside a min-width query.
   It used to be unscoped, so it sat after the max-width:960px block and won on
   source order at every phone width — re-imposing a 350px second column that
   forced 28px of clipped overflow at 320px. This is the source-order trap
   AGENTS.md warns about: the later rule wins regardless of specificity. */
@media (min-width: 961px) {
  .about-grid {
    grid-template-columns: minmax(0, 1fr) minmax(350px, 460px);
    gap: 64px;
    /* start, not stretch: with the nested boxes gone the stack card is much
       shorter than the prose column, and stretching left a large empty void. */
    align-items: start;
  }
}
#about p {
  font-size: 16px;
  line-height: 1.82;
  color: var(--ink-2);
  margin-bottom: 16px;
}
/* The stack rows now reveal with their parent card via [data-reveal]; the
   per-row transform/opacity reset and the 5-step stackRise ladder are gone,
   along with .stack-level (self-assessed proficiency) and the five coloured
   rails, which were decoration pretending to be a taxonomy. */

/* ---- DISPLAY TIER: editorial serif (protocol S3) ----
   These four blocks are the effective declarations for the display tier and sit
   late in the file, so they win on source order over the base hero block above.

   Line-heights are RAISED versus the Clash Display values (.88 / .96). A
   geometric grotesque with tall caps absorbs sub-1.0 leading; a serif with long
   ascenders and descenders collides below ~1.0, and "Abira Wisnunggal" wraps to
   two lines at most desktop widths, so this is load-bearing rather than
   cosmetic. Tracking is loosened to -0.02/-0.03em for the same reason.

   Weights preserve the Reversed Hero Rule: the name is largest AND heaviest
   (600), the h1 beneath it is smaller and lighter (500) and lower-contrast.
   .hero-role deliberately STAYS Clash Display — it reads as a geometric
   caption under a serif statement, which is the contrast the hero wants. */
.hero-name {
  font-family: var(--fserif);
  font-weight: 500;
  font-size: clamp(30px, 4.2vw, 58px);
  line-height: 1.08;
  letter-spacing: -.02em;
}
.hero-nickname {
  font-family: var(--fserif);
  font-weight: 600;
  font-size: clamp(48px, 7.2vw, 94px);
  line-height: 1.02;
  letter-spacing: -.025em;
  margin-bottom: 16px;
}
.hero-role {
  font-size: clamp(18px, 1.9vw, 26px);
  margin-bottom: 14px;
}
.about-title {
  font-family: var(--fserif);
  font-weight: 600;
  line-height: 1.1;
  letter-spacing: -.03em;
}
.contact-heading {
  font-family: var(--fserif);
  font-weight: 600;
  line-height: 1.1;
}
/* .pg-title is NOT overridden here: its base declaration sits further down the
   file (~1690) and would win on source order at equal specificity. It is
   changed in place instead. */
.hero-desc {
  max-width: 700px;
  margin-bottom: 30px;
}

.hero-profile {
  justify-self: end;
  align-self: start;
  /* The invariant this repo holds: the photograph's top edge sits 0.4 x the
     nickname font-size (about 38px at the 94px ceiling) ABOVE the cap-height of
     the first h1 line ("I build AI systems"). It was exactly level with that
     cap-height, and before that level with the "Abira Wisnunggal" cap-height;
     the user asked for it a little higher, so 0.4 x is a tuned nudge, not a
     typographic alignment.

     align-self stays `start`, so the cell top == .hero-copy top and the margin
     is the whole offset. Terms, in order:

       1.64 x nicknameFS = 2 lines x 1.02 line-height (the nickname wraps to two
                           lines at every desktop width, measured) minus the
                           0.4 x nudge
       16px              = .hero-nickname margin-bottom
       0.07 x h1FS       = gap from the h1 box top to the cap-height of its first
                           line, canvas measureText in Newsreader 500 at line-height
                           1.08. Chrome rounds ascent/descent to whole pixels, so
                           the true ratio wobbles 0.065-0.075 with size; 0.07
                           keeps the error under 0.3px.
       -12.22px          = the gold plate's constant frame inset
                           (--hero-media-inset x aspectRatio / 2), because the
                           visible photo sits that far inside the cell.

     Expressed as a formula because both font sizes are vw clamps while the 16px
     and 12.22px terms are constant. If the wordmark ever fits on one line, or
     either type scale or the display face changes, re-measure. Do not switch to
     align-self: center. */
  margin-top: calc(1.64 * clamp(48px, 7.2vw, 94px) + 16px + 0.07 * clamp(30px, 4.2vw, 58px) - 12.22px);
  width: min(34vw, 420px);
  aspect-ratio: var(--hero-media-ar);
  display: grid;
  place-items: center;
  position: relative;
}
.hero-profile-ring {
  position: absolute;
  inset: 0;
  border-radius: 22px;
  border: 1px solid rgba(184,146,74,.45);
  background:
    radial-gradient(circle at 25% 20%, rgba(255,255,255,.7), rgba(255,255,255,0) 45%),
    conic-gradient(from 220deg, rgba(184,146,74,.2), rgba(26,25,24,.05), rgba(184,146,74,.35));
  box-shadow: 0 24px 46px rgba(26,25,24,.16);
  animation: profileOrbit 6.5s ease-in-out infinite alternate;
}
.hero-profile-frame {
  position: relative;
  width: calc(100% - (2 * var(--hero-media-inset)));
  aspect-ratio: var(--hero-media-ar);
  border-radius: 22px;
  overflow: hidden;
  border: 4px solid rgba(255,255,255,.95);
  background: radial-gradient(circle at 30% 25%, #f4efe5 0%, #ddd8cf 100%);
  box-shadow: inset 0 0 0 1px rgba(26,25,24,.08);
}
.hero-profile-frame img {
  width: 100%;
  height: 100%;
  aspect-ratio: var(--hero-media-ar);
  object-fit: cover;
  object-position: center;
}
.hero-profile-placeholder {
  display: none;
  position: absolute;
  inset: 0;
  place-items: center;
  text-align: center;
  padding: 24px;
  font-family: var(--fm);
  font-size: 12px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: #68625d;
  background: linear-gradient(180deg, rgba(255,255,255,.55), rgba(230,224,213,.9));
}
@keyframes profileOrbit {
  from { transform: translateY(-6px) scale(1); }
  to   { transform: translateY(6px) scale(1.015); }
}

.hero-sub-ctas {
  margin-top: 14px;
  display: flex;
  flex-wrap: wrap;
  gap: 18px;
}

.nav-actions {
  display: flex;
  align-items: center;
  gap: 10px;
}
.nav-toggle {
  width: 40px;
  height: 40px;
  display: none;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  gap: 4px;
  border: 1px solid var(--line-2);
  background: #fff;
  border-radius: var(--r4);
}
.nav-toggle span:not(.sr-only) {
  display: block;
  width: 18px;
  height: 1.5px;
  background: var(--ink);
}

.nav-drawer {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  width: min(340px, 90vw);
  background: var(--bg-card);
  border-left: 1px solid var(--line);
  transform: translateX(102%);
  /* visibility removes the off-screen panel from sequential keyboard focus.
     Delayed on close so it hides after the slide-out; immediate on open.
     JS also toggles `inert` as belt-and-braces for engines that keep
     visibility:hidden subtrees focusable. */
  visibility: hidden;
  transition: transform .28s var(--ease-s), visibility 0s linear .28s;
  z-index: 180;
  padding: 18px;
}
.nav-drawer.open {
  transform: translateX(0);
  visibility: visible;
  transition: transform .28s var(--ease-s), visibility 0s;
}
.nav-drawer-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 14px;
}
.nav-drawer-title {
  font-family: var(--fm);
  font-size: 12px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-3);
}
.nav-drawer-close {
  border: 1px solid var(--line-2);
  background: #fff;
  width: 34px;
  height: 34px;
  border-radius: var(--r4);
}
.nav-drawer-links {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.nav-drawer-links a {
  padding: 12px;
  border: 1px solid var(--line);
  border-radius: var(--r4);
  font-family: var(--fm);
  font-size: 12px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-2);
}

.exp-card {
  cursor: default;
  padding: 0;
}
.exp-trigger {
  width: 100%;
  border: 0;
  padding: 0;
  text-align: left;
  background: transparent;
  color: inherit;
  cursor: pointer;
}

.exp-brand {
  display: flex;
  align-items: flex-start;
  gap: 12px;
  min-width: 0;
}
.exp-brand > div:last-child {
  min-width: 0;
}
.exp-logo-slot {
  width: 54px;
  height: 54px;
  border-radius: 50%;
  overflow: hidden;
  flex-shrink: 0;
  border: 1px solid var(--line);
  background: var(--bg-card);
  display: grid;
  place-items: center;
}
.exp-logo-slot img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.exp-logo-ph {
  display: none;
  place-items: center;
  text-align: center;
  font-family: var(--fm);
  font-size: 11px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink-3);
}

.proj-card {
  cursor: default;
}
.proj-metrics {
  margin-bottom: 16px;
  display: flex;
  flex-direction: column;
  gap: 8px;
  color: var(--ink-2);
  font-size: 14px;
  line-height: 1.55;
}
/* Global `ul { list-style: none }` suppresses ::marker, so the gold marker
   documented here never rendered. Match .exp-list: a flex row with a gold
   '›' glyph — list glyphs are allowed to stay gold per the Restrained Gold
   Rule, and this keeps both lists visually consistent. */
.proj-metrics li { display: flex; gap: 10px; }
.proj-metrics li::before {
  content: '›';
  color: var(--gold);
  font-size: 16px;
  flex-shrink: 0;
  line-height: 1.4;
}
.proj-metrics li::marker { color: var(--gold); }

.proj-proof {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  margin-bottom: 16px;
}
.proj-proof { gap: 18px; }
.proj-proof a {
  font-family: var(--fm);
  font-size: 11px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink);
  border-bottom: 1px solid var(--line-2);
  padding: 6px 0;
  transition: border-color .2s;
}
.proj-proof a:hover { border-color: var(--ink); }

.proj-open-btn {
  margin-top: auto;
  align-self: flex-start;
  font-family: var(--fm);
  font-size: 12px;
  letter-spacing: .08em;
  text-transform: uppercase;
  border: 1.5px solid var(--ink);
  color: var(--ink);
  background: transparent;
  padding: 10px 14px;
  border-radius: var(--r4);
}
.proj-open-btn:hover {
  background: var(--ink);
  color: #fff;
}

/* Focusable error summary. Sits above the fields, receives focus on failed submit,
   and links to each invalid field. Inline .f-err messages are retained. */
.f-summary {
  border: 1.5px solid #b91c1c;
  border-radius: var(--r4);
  background: #fdf5f5;
  padding: 14px 16px;
  margin-bottom: 18px;
}
.f-summary:focus { outline: 3px solid #b91c1c; outline-offset: 3px; }
.f-summary-title {
  font-family: var(--fd);
  font-size: 16px;
  font-weight: 600;
  color: #b91c1c;
  margin-bottom: 8px;
}
.f-summary-list {
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
.f-summary-list a {
  font-size: 13.5px;
  color: #b91c1c;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.f-summary-list a:hover { color: #7f1d1d; }

.f-err {
  min-height: 16px;
  font-size: 12px;
  color: #b91c1c;
}
.f-inp.invalid,
.f-txt.invalid {
  border-color: #b91c1c;
  box-shadow: 0 0 0 3px rgba(185,28,28,.08);
}
.form-status {
  min-height: 22px;
  font-size: 13px;
  color: var(--ink-2);
}
.form-status.success { color: #166534; }
.form-status.error { color: #b91c1c; }


.c-icon {
  font-family: var(--fm);
  font-size: 11px;
  letter-spacing: .06em;
  width: 30px;
  height: 30px;
  border-radius: 50%;
  border: 1px solid var(--line-2);
  display: inline-flex;
  align-items: center;
  justify-content: center;
}
.c-icon svg { width: 15px; height: 15px; display: block; }

/* Increase micro typography readability and contrast */
.sec-label,
.nav-links a,
.nav-btn,
.h-stat .l,
.stack-title,
.exp-company,
.exp-meta,
.exp-body-label,
.proj-num,
.modal-bar-label,
.modal-sub,
.modal-sh,
.flow-label,
.cert-issuer,
.honor-org,
.c-label,
.f-lbl,
.footer-col-label,
.footer-copy,
.footer-back {
  font-size: 11px;
  letter-spacing: .08em;
}

.sec-label,
.exp-meta,
.proj-num,
.modal-bar-label,
.modal-sh,
.flow-label,
.c-label,
.f-lbl,
.h-stat .l,
/* Second pass: metadata that was still inheriting --ink-4. --ink-4 (#b0ada8) is
   2.1:1 on paper and 2.2:1 on white, so it is decoration-only from here on. */
.exp-body-label,
.stack-title,
.honor-year,
.honor-meta {
  color: var(--ink-3);
}

/* The footer is --bg-dark (#111110), so the light-surface repaint above is wrong
   there. These three were folded into that block and inherited #6f6b66, which is
   3.57:1 on the dark ground. Their original rgba(255,255,255,.3/.4) was worse
   still at 2.68:1. rgba(255,255,255,.5) composites to ~#888888 = 5.37:1 and .65
   to ~#acacac = 8.39:1, both passing at 11px. .65 matches .footer-link, keeping
   the back-to-top CTA at the same weight as the other footer links. */
.footer-copy,
.footer-col-label { color: rgba(255,255,255,.5); }
.footer-back { color: rgba(255,255,255,.65); }

/* The playground block sits below this override block, so `.pg-*` labels cannot be
   repainted from here — they lose on source order at equal specificity. They are
   fixed at their own declarations instead. */

/* ======================== BUTTON PRESS FEEDBACK (protocol S5/S7) ========================
   Hover already lifts these (-2px/-3px). There was no :active state at all, so a
   tap on a touch device got no acknowledgement beyond a hover that may never
   fire. scale(.98) is the protocol's specified press response. It composes with
   the existing lift because hover uses translate and press uses scale — two
   different properties — so the button never fights itself mid-gesture. */
.btn-solid:active,
.btn-outline:active,
.f-submit:active,
.pg-submit:active,
.nav-btn:active,
.pg-chip:active {
  transform: scale(.98);
}

a:focus-visible,
button:focus-visible,
input:focus-visible,
textarea:focus-visible,
.exp-trigger:focus-visible,
.proj-open-btn:focus-visible,
.nav-toggle:focus-visible,
.nav-drawer-close:focus-visible,
.cert-btn:focus-visible,
.pg-chip:focus-visible,
.pg-submit:focus-visible {
  /* --gold-ink, not --gold (2.7:1 on paper, fails WCAG 1.4.11) and not --ink
     (16:1 on paper but 1.08:1 on the #111110 footer, where the ring vanishes).
     --gold-ink is 4.7:1 on --bg, 5.0:1 on white, 3.75:1 on --bg-dark. */
  outline: 3px solid var(--gold-ink);
  outline-offset: 3px;
}

/* Invisible hit-area expansion for sub-44px icon buttons. The ::after box is
   part of the button itself, so it extends the clickable region without
   changing the visual size or the surrounding layout. Web WCAG 2.5.8 only
   requires 24px, which these already meet — this is a comfort upgrade for
   coarse pointers, not a conformance fix. */
.exp-toggle,
.modal-close,
.nav-drawer-close,
.nav-toggle { position: relative; }
.exp-toggle::after,
.modal-close::after,
.nav-drawer-close::after,
.nav-toggle::after {
  content: '';
  position: absolute;
}
.exp-toggle::after { inset: -8px; }      /* 28px -> 44px */
.modal-close::after { inset: -8px; }    /* 29-32px -> 44px+ */
.nav-drawer-close::after { inset: -8px; } /* 33-34px -> 44px+ */
.nav-toggle::after { inset: -3px; }      /* 39-40px -> 44px */

@media (max-width: 960px) {
  .hero-section {
    padding-left: 24px;
    padding-right: 24px;
  }
  .hero-shell,
  .hero-stats-shell {
    max-width: none;
  }
  .hero-shell {
    grid-template-columns: 1fr;
    gap: 26px;
    align-items: end;
  }
  .hero-profile {
    justify-self: start;
    width: min(72vw, 330px);
    /* NOTE: do NOT reset margin-top here. This block sets .hero-shell to one
       column, but the `min-width: 768px and max-width: 1023px` block further
       down restores a two-column shell at 768-960px and wins on source order.
       So this range is still the side-by-side hero, the cap-height invariant
       still applies, and .hero-profile keeps its base calc() offset. The reset
       belongs at 767px, where the shell actually becomes a flex column. */
  }
  .about-grid {
    grid-template-columns: minmax(0,1fr);
    gap: 34px;
  }
  .nav-actions { display: none; }
  .nav-toggle { display: inline-flex; }
  .nav-links { display: none; }
  .hero-sub-ctas { gap: 14px; }

  /* Stack the proof label above the pills once the copy column is full width. */
  .hero-proof {
    flex-direction: column;
    align-items: center;
    gap: 12px;
    margin-left: auto;
    margin-right: auto;
  }
  .hero-proof-label { text-align: center; }
  .hero-proof-list { justify-content: center; }
}

@media (max-width: 600px) {
  section {
    padding: 68px 18px;
  }
  #hero {
    padding-top: 92px;
    padding-bottom: 52px;
  }
  .hero-ctas a {
    width: 100%;
    text-align: center;
  }
  .hero-status-badge {
    font-size: 9px;
    letter-spacing: .06em;
  }
  .hero-proof-label,
  .hero-proof-item {
    font-size: 10px;
  }
  .hero-proof-item { padding: 5px 10px; letter-spacing: .04em; }
  .hero-name {
    font-size: clamp(26px, 8.4vw, 42px);
  }
  .hero-nickname {
    font-size: clamp(40px, 13vw, 62px);
  }
  .hero-profile {
    width: min(86vw, 300px);
  }
  .exp-logo-slot {
    width: 48px;
    height: 48px;
  }
  .proj-body,
  .cert-body,
  .modal-content {
    padding-left: 18px;
    padding-right: 18px;
  }
  .section-intro {
    margin-top: -10px;
    font-size: 13px;
  }
  .honor-actions {

    align-items: flex-start;
  }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
  /* [data-reveal] targets are forced visible outright rather than animated in,
     so nothing depends on an IntersectionObserver firing to become readable.
     This is the safety net for JS being disabled or slow. */
  [data-reveal] { opacity: 1 !important; transform: none !important; }
}

/* ======================== HERO LAYOUT BREAKPOINT FIX ======================== */
@media (min-width: 1024px) {
  #hero {
    min-height: 100vh;
    justify-content: center;
    padding-top: 104px;
    padding-bottom: 72px;
  }

  /* The portrait column is `auto` so it is exactly as wide as the portrait
     (min(38vw, 500px)) instead of a fixed 430px track the photo never filled.
     The shell is 80px wider than the base 1160px so the extra photo width comes
     out of the empty right margin, not out of the copy column ("Wisnunggal" is
     ~5.4em wide and clips if the copy column drops under it at ~1024px). */
  .hero-shell {
    grid-template-columns: minmax(0, 1.05fr) auto;
    align-items: center;
    gap: clamp(36px, 5vw, 78px);
    max-width: 1240px;
  }

  .hero-copy {
    text-align: left;
    max-width: 680px;
  }

  .hero-profile {
    justify-self: end;
    width: min(38vw, 500px);
    margin-inline: 0;
  }
}

@media (min-width: 768px) and (max-width: 1023px) {
  #hero {
    min-height: auto;
    justify-content: center;
    padding-top: 96px;
    padding-bottom: 56px;
  }

  .hero-shell {
    grid-template-columns: minmax(0, 1fr) minmax(260px, 350px);
    align-items: center;
    gap: 24px;
  }

  .hero-copy {
    text-align: left;
    max-width: 100%;
  }

  .hero-profile {
    justify-self: end;
    width: min(40vw, 330px);
    margin-inline: 0;
  }

  .hero-stats {
    display: none;
  }
}

@media (max-width: 767px) {
  #hero {
    min-height: auto;
    justify-content: flex-start;
    padding-top: 88px;
    padding-bottom: 44px;
  }

  .hero-shell {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 22px;
  }

  .hero-copy {
    max-width: 100%;
    text-align: center;
  }

  .hero-name {
    font-size: clamp(26px, 8.2vw, 44px);
  }

  .hero-nickname {
    font-size: clamp(40px, 12vw, 64px);
  }

  .hero-desc {
    max-width: 100%;
    margin-left: auto;
    margin-right: auto;
  }

  .hero-ctas,
  .hero-sub-ctas {
    justify-content: center;
  }

  .hero-profile {
    justify-self: center;
    align-self: center;
    width: min(82vw, 320px);
    max-width: 320px;
    margin-inline: auto;
    /* At <=767px .hero-shell becomes a flex column: the portrait sits UNDER the
       copy, so there is no cap-height to align to and the desktop calc() offset
       would just nudge the photo up. This is the only place the reset belongs —
       putting it at 960px would also kill the 768-960px range, which is still
       the two-column hero. */
    margin-top: 0;
  }
}

/* ======================== SCROLLBAR ======================== */
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--line-2); border-radius: 2px; }

/* ======================== MODAL LESSONS LEARNED ======================== */
.modal-lessons {
  list-style: none; display: flex; flex-direction: column; gap: 10px;
  margin-top: 4px; margin-bottom: 4px;
}
.modal-lessons li {
  font-size: 14px; line-height: 1.72; color: var(--ink-2);
  display: flex; gap: 10px; align-items: flex-start;
}
.modal-lessons li::before {
  content: '↳'; color: var(--gold); font-size: 14px; flex-shrink: 0; margin-top: 1px;
}

/* ======================== RAG PLAYGROUND ======================== */
#playground { border-top: 1px solid var(--line); }
.pg-header { margin-bottom: 36px; }
.pg-title {
  font-family: var(--fserif); font-size: clamp(26px, 3.5vw, 44px);
  font-weight: 600; line-height: 1.1; letter-spacing: -.03em; color: var(--ink); margin-bottom: 12px;
}
.pg-sub { font-size: 15px; line-height: 1.72; color: var(--ink-2); max-width: 680px; }
.pg-shell {
  background: var(--bg-card); border: 1.5px solid var(--line); border-radius: var(--r8);
  padding: 32px; box-shadow: var(--sh-sm);
}
.pg-input-wrap { display: flex; gap: 10px; margin-bottom: 14px; }
.pg-input {
  /* min-width:0 so the field can shrink below its intrinsic input size;
     without it the input+button pair overflowed a 320px viewport. */
  flex: 1 1 auto; min-width: 0;
  background: var(--bg); border: 1.5px solid var(--line); border-radius: var(--r4);
  padding: 12px 16px; font-family: var(--fb); font-size: 14px; color: var(--ink);
  outline: none; transition: border-color .2s, box-shadow .2s;
}
.pg-input:focus { border-color: var(--ink); box-shadow: 0 0 0 3px rgba(26,25,24,.07); }
.pg-submit {
  font-family: var(--fm); font-size: 11px; letter-spacing: .08em; text-transform: uppercase;
  background: var(--ink); color: #fff; border: none; padding: 12px 22px; border-radius: var(--r4);
  cursor: pointer; white-space: nowrap;
  /* The label must not be the thing that shrinks; the field gives way instead. */
  flex: 0 0 auto;
  transition: background .2s, transform .2s var(--ease-s);
}
.pg-submit:hover { background: #000; }
/* Loading feedback while the simulated pipeline runs (ux: Submit Feedback —
   Loading -> result). Disabled state carries no transform so the button does
   not jump when it becomes unavailable mid-hover. */
.pg-submit:disabled { opacity: .6; cursor: wait; transform: none; }
.pg-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 28px; }
.pg-chip {
  font-family: var(--fm); font-size: 11px; letter-spacing: .06em; text-transform: uppercase;
  border: 1px solid var(--line-2); border-radius: var(--r4); padding: 8px 12px;
  color: var(--ink-3); background: var(--bg); cursor: pointer;
  transition: border-color .2s, color .2s, background .2s;
}
.pg-chip:hover { border-color: var(--ink); color: var(--ink); background: #fff; }
.pg-pipeline {
  display: flex; align-items: center; flex-wrap: wrap; gap: 6px;
  padding: 20px; background: var(--bg); border: 1px solid var(--line); border-radius: var(--r8);
  margin-bottom: 20px;
}
/* aria-hidden connector, but it still has to be visible enough to read the flow. */
.pg-arrow { font-size: 16px; color: var(--ink-3); transition: color .2s; }
/* The connector into the running stage carries the signal forward. */
.pg-arrow.pg-arrow-lit { color: var(--gold-ink); }
.pg-step {
  position: relative; overflow: hidden;
  display: flex; align-items: center; gap: 10px;
  padding: 10px 14px; border: 1.5px solid var(--line); border-radius: var(--r4);
  background: #fff; min-width: 0;
  transition: border-color .3s var(--ease-s), background .3s;
}
/* Stage timer. JS sets --pg-dur per stage. Retrieve and Generate use half of
   their measured DemoLabs latency, so Generate visibly dominates the run, as
   it does in the real system. Resetting a run removes the classes and the
   bar snaps back with no transition. */
.pg-step::after {
  content: ''; position: absolute; left: 0; right: 0; bottom: 0; height: 2px;
  background: var(--gold-ink); transform: scaleX(0); transform-origin: left;
}
.pg-step-active::after { transform: scaleX(1); transition: transform var(--pg-dur, .2s) linear; }
.pg-step-done::after { transform: scaleX(1); opacity: 0; transition: opacity .25s; }
.pg-step-ico {
  width: 28px; height: 28px; border-radius: 50%; background: var(--bg);
  border: 1.5px solid var(--line-2); display: flex; align-items: center; justify-content: center;
  font-family: var(--fm); font-size: 11px; font-weight: 500; color: var(--ink-3); flex-shrink: 0;
  transition: background .3s, border-color .3s, color .3s;
}
.pg-step-info { min-width: 0; }
.pg-step-label {
  font-family: var(--fm); font-size: 11px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--ink-3); margin-bottom: 2px;
}
.pg-step-val {
  font-family: var(--fb); font-size: 12px; color: var(--ink-2);
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 120px;
}
.pg-step-active { border-color: var(--gold); background: #fffbf4; }
/* White on --gold is 2.90:1. The step letter is 10px, so the active fill uses
   the darker step: white on --gold-ink is 5.04:1. */
.pg-step-active .pg-step-ico { background: var(--gold-ink); border-color: var(--gold-ink); color: #fff; }
/* Done is ink, not green: a finished step is a state, not a success claim, and
   a fifth hue on the page buys nothing. */
.pg-step-done { border-color: var(--line-2); }
.pg-step-done .pg-step-ico { background: var(--ink); border-color: var(--ink); color: #fff; }
.pg-answer-box {
  background: var(--bg); border: 1px solid var(--line); border-radius: var(--r8); padding: 24px;
}
.pg-answer-label {
  font-family: var(--fm); font-size: 11px; letter-spacing: .14em; text-transform: uppercase;
  color: var(--ink-3); margin-bottom: 12px;
}
.pg-answer-text { font-size: 14.5px; line-height: 1.8; color: var(--ink-2); min-height: 1.8em; }
/* The answer is uncovered top-down, the way generated text arrives. There is
   no fill-mode, so if the animation never runs the box is simply visible. */
.pg-answer-box:not([hidden]) { animation: pgAnswerIn .45s var(--ease-s); }
@keyframes pgAnswerIn {
  from { opacity: 0; clip-path: inset(0 0 100% 0); }
  to   { opacity: 1; clip-path: inset(0 0 0 0); }
}
.pg-timing {
  font-family: var(--fm); font-size: 12px; letter-spacing: .04em; color: var(--ink-3);
  margin-top: 16px; padding-top: 12px; border-top: 1px solid var(--line);
}
/* Stated before anything runs, not after: a reader must know it is scripted
   before deciding whether the output means anything. */
.pg-disclaimer { font-family: var(--fm); font-size: 11px; color: var(--ink-3); letter-spacing: .04em; margin-top: 12px; }
@media (max-width: 960px) {
  .pg-shell { padding: 20px; }
  .pg-arrow { display: none; }
  .pg-pipeline { flex-direction: column; align-items: stretch; gap: 8px; }
  .pg-step { width: 100%; }
  .pg-step-val { max-width: none; }
}
@media (max-width: 600px) { .pg-shell { padding: 16px; } }

/* ======================== MOBILE MICRO-TEXT IMPROVEMENTS ======================== */
@media (max-width: 600px) {
  .sec-label { font-size: 12px; }
  .proj-num, .proj-tags, .cert-issuer,
  .exp-meta, .exp-company, .honor-meta, .honor-org,
  .modal-sh, .stack-title, .f-lbl, .pg-step-label,
  .pg-disclaimer { font-size: 12px; }
  .exp-list li, .modal-lessons li { font-size: 14px; }
  .modal-p { font-size: 15px; }
}

/* ======================== CASE STUDY (case-study-smartfaq.html) ========================
   Uses the same tokens and type tiers as the portfolio. The KPI row is a <dl>:
   each number sits with the test it came from, never on its own. */
.case-wrap { max-width: 880px; margin: 0 auto; padding: 48px 24px 96px; }
.case-nav {
  display: flex; justify-content: space-between; gap: 12px; flex-wrap: wrap;
  margin-bottom: 64px;
}
.case-nav a {
  font-family: var(--fm); font-size: 11px; letter-spacing: .08em; text-transform: uppercase;
  color: var(--ink-2); border-bottom: 1px solid var(--line-2); padding: 6px 0;
  transition: border-color .2s, color .2s;
}
.case-nav a:hover { color: var(--ink); border-color: var(--ink); }
.case-eyebrow {
  font-family: var(--fm); font-size: 11px; letter-spacing: .1em; text-transform: uppercase;
  color: var(--gold-ink); margin-bottom: 16px;
}
.case-title {
  font-family: var(--fserif); font-weight: 600;
  font-size: clamp(38px, 6vw, 68px); line-height: 1.05; letter-spacing: -.025em;
  color: var(--ink); margin-bottom: 20px;
}
.case-sub { font-size: 18px; line-height: 1.7; color: var(--ink-2); max-width: 720px; }
.case-kpi {
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr));
  margin: 48px 0 16px; border-top: 1px solid var(--ink); border-bottom: 1px solid var(--line);
}
.case-kpi-item { padding: 22px 20px 22px 0; }
.case-kpi-item + .case-kpi-item { padding-left: 20px; border-left: 1px solid var(--line); }
.case-kpi-label {
  font-family: var(--fm); font-size: 11px; letter-spacing: .08em; text-transform: uppercase;
  color: var(--ink-3); margin-bottom: 10px;
}
.case-kpi-value {
  font-family: var(--fd); font-size: clamp(32px, 4vw, 44px); font-weight: 700;
  letter-spacing: -.03em; line-height: 1; color: var(--ink); margin: 0 0 10px;
}
.case-kpi-note { font-size: 14px; line-height: 1.5; color: var(--ink-3); margin: 0; }
.case-section { margin-top: 56px; }
.case-h {
  font-family: var(--fd); font-size: 24px; font-weight: 600; letter-spacing: -.015em;
  color: var(--ink); margin-bottom: 14px;
}
.case-p { font-size: 16px; line-height: 1.8; color: var(--ink-2); margin-bottom: 14px; max-width: 720px; }
.case-list { display: flex; flex-direction: column; gap: 10px; max-width: 720px; }
.case-list li { font-size: 16px; line-height: 1.7; color: var(--ink-2); display: flex; gap: 10px; }
.case-list li::before { content: '›'; color: var(--gold); flex-shrink: 0; }
.case-flow {
  list-style: none; display: grid; grid-template-columns: repeat(5, minmax(0, 1fr));
  margin-top: 22px; border: 1px solid var(--line); border-radius: var(--r8);
  background: var(--bg-card); counter-reset: flow;
}
.case-flow li { padding: 16px 14px; counter-increment: flow; }
.case-flow li + li { border-left: 1px solid var(--line); }
.case-flow b {
  display: block; font-family: var(--fd); font-size: 15px; font-weight: 600;
  color: var(--ink); margin-bottom: 4px;
}
.case-flow b::before {
  content: counter(flow) ' '; font-family: var(--fm); font-size: 11px; font-weight: 400; color: var(--ink-3);
}
.case-flow span { font-size: 13px; line-height: 1.45; color: var(--ink-3); }
@media (max-width: 760px) {
  .case-kpi { grid-template-columns: minmax(0, 1fr); }
  .case-kpi-item + .case-kpi-item { padding-left: 0; border-left: 0; border-top: 1px solid var(--line); }
  .case-flow { grid-template-columns: minmax(0, 1fr); }
  .case-flow li + li { border-left: 0; border-top: 1px solid var(--line); }
}

/* ======================== TOUCH + PHONE ADAPTATION ========================
   Last in the file so it wins by source order over the earlier breakpoint
   blocks. The query is (pointer: coarse) OR a phone-width viewport, so a
   narrow desktop window gets the same larger hit areas; they are invisible.

   Hit areas follow the existing nav-toggle / modal-close pattern: the visible
   control keeps its size and an ::after extends the tappable box to 44px.
   Text links sit on an 18px row gap or more, so extended areas never overlap. */
@media (pointer: coarse), (max-width: 767px) {
  a, button { touch-action: manipulation; -webkit-tap-highlight-color: rgba(184, 146, 74, .2); }

  .proj-proof a, .cert-btn, .honor-actions a, .footer-back, .nav-logo { position: relative; }
  .proj-proof a::after, .cert-btn::after, .honor-actions a::after { content: ''; position: absolute; inset: -9px 0; }
  .footer-back::after { content: ''; position: absolute; inset: -15px 0; }
  .nav-logo::after { content: ''; position: absolute; inset: -11px -6px; }

  .proj-proof { row-gap: 18px; }
  .honor-actions { row-gap: 18px; }
  .cert-actions { row-gap: 18px; }

  /* Filled controls grow for real: their whole box is the visible shape. */
  .proj-open-btn, .pg-chip, .pg-submit, .f-submit, .btn-solid, .btn-outline, .skip-link {
    min-height: 44px;
  }
  .pg-chip { display: inline-flex; align-items: center; }
  .footer-link, .nav-drawer-links a { min-height: 44px; }
}

/* Phone type floor. Hero text is left as set. Everything else that was 11px
   or below reads at 12px, matching the micro-label bump the 600px block
   already gave other labels. */
@media (max-width: 767px) {
  .proj-proof a, .honor-actions a, .cert-btn, .cert-level, .honor-year,
  .stack-tools, .exp-body-label, .proj-status, .proj-tags, .c-label,
  .footer-col-label, .footer-copy, .footer-back, .pg-chip, .pg-submit, .f-submit,
  .pg-step-ico, .pg-timing, .pg-disclaimer, .modal-bar-label { font-size: 12px; }

  /* Horizontal reveals travel up on a phone. Entering from 24px off the right
     edge adds 24px of scrollable width until the element is revealed, which
     iOS Safari does not hide behind overflow-x:hidden on <body>. The hero
     portrait is excluded: it is revealed on load and its entrance stays as set. */
  .js [data-reveal="left"]:not(.in):not(.hero-profile),
  .js [data-reveal="right"]:not(.in):not(.hero-profile) { transform: translateY(18px); }
}

/* Portrait tablet: the project cards were already built to fit a 390px phone,
   so two of them fit across 700-960px instead of one card floating in a
   770px-wide column. Experience stays single-column: opening one entry would
   stretch the card beside it. */
@media (min-width: 700px) and (max-width: 960px) {
  .proj-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
