/* ==========================================================================
   components.css
   Separation is done with borders and background steps. There are no
   drop shadows in this file, and no gradients used as decoration.
   ========================================================================== */

/* ==========================================================================
   Buttons and links
   ========================================================================== */

.btn {
  display: inline-block;
  padding: var(--s3) var(--s5);
  border-radius: var(--r-sm);
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  text-decoration: none;
  border: 1px solid transparent;
  transition: background var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}

.btn--solid {
  background: var(--accent-fill);
  color: var(--on-accent-fill);
}

.btn--solid:hover {
  background: var(--accent-fill-hover);
  color: var(--on-accent-fill);
}

.btn--outline {
  border-color: var(--border-strong);
  color: var(--text);
}

.btn--outline:hover {
  border-color: var(--accent);
  color: var(--accent-hover);
}

/* A text link that reads as a link without a glyph tacked onto it. */
.link {
  font-size: var(--fs-sm);
  color: var(--accent);
  text-decoration: underline;
  text-decoration-color: var(--accent-dim);
  text-underline-offset: 4px;
}

.link:hover { color: var(--accent-hover); text-decoration-color: var(--accent); }

/* ==========================================================================
   Highlights — a handful of projects given room, above the manifest.

   Deliberately not a card grid: three identical rounded cards is the shape
   every generated portfolio reaches for. These are wide alternating rows, so
   the section reads as editorial and stays visually distinct from the dense
   table below it.
   ========================================================================== */

.featured__item {
  display: grid;
  grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
  gap: var(--s8);
  align-items: center;
}

.featured__item + .featured__item {
  margin-top: var(--s7);
  padding-top: var(--s7);
  border-top: var(--rule);
}

/* Alternate which side the image sits on. */
.featured__item:nth-child(even) .featured__media { order: 2; }

.featured__media img {
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
  object-position: top center;
  border: 1px solid var(--border-strong);
  border-radius: var(--r-md);
}

.featured__title {
  font-size: var(--fs-lg);
  font-weight: var(--fw-semi);
}

/* Sans, not mono: mono stays reserved for code and the manifest stack. */
.featured__meta {
  margin-top: var(--s2);
  font-size: var(--fs-sm);
  color: var(--text-dim);
}

.featured__body {
  margin-top: var(--s4);
  color: var(--text-muted);
  max-width: 52ch;
}

.featured__links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s5);
  margin-top: var(--s5);
}

@media (max-width: 860px) {
  .featured__item {
    grid-template-columns: 1fr;
    gap: var(--s5);
  }
  /* Image above text on every item, whatever the desktop order was. */
  .featured__item:nth-child(even) .featured__media { order: 0; }
  .featured__item + .featured__item {
    margin-top: var(--s6);
    padding-top: var(--s6);
  }
}

/* ==========================================================================
   Manifest: dense, table-style, one row per project
   ========================================================================== */

.filters {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s2);
  margin-bottom: var(--s6);
}

/* Filtering needs JS, so the chips ship hidden and main.js reveals them.
   This has to out-specify `display: flex` above, or the [hidden] attribute
   loses and the chips render as dead controls when scripts do not run. */
.filters[hidden] { display: none; }

.chip {
  padding: var(--s2) var(--s4);
  font-size: var(--fs-sm);
  color: var(--text-muted);
  background: transparent;
  border: 1px solid var(--border);
  border-radius: 999px;
  transition: color var(--dur-fast) var(--ease),
              border-color var(--dur-fast) var(--ease),
              background var(--dur-fast) var(--ease);
}

.chip:hover { color: var(--text); border-color: var(--border-strong); }

.chip[aria-pressed="true"] {
  color: var(--text);
  background: var(--accent-dim);
  border-color: var(--accent);
}

.manifest { border-top: var(--rule); }

.manifest__count {
  margin-top: var(--s5);
  font-size: var(--fs-sm);
  color: var(--text-dim);
}

.row { border-bottom: var(--rule); }

.row[hidden] { display: none; }

.row__btn {
  position: relative;
  display: grid;
  grid-template-columns: minmax(200px, 1.4fr) 150px 1fr 60px;
  gap: var(--s4);
  align-items: baseline;
  width: 100%;
  padding: var(--s4) var(--s4) var(--s4) var(--s5);
  text-align: left;
  transition: background var(--dur-fast) var(--ease);
}

.row__btn::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 2px;
  background: var(--accent-fill);
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease);
}

.row__btn:hover,
.row__btn[aria-expanded="true"] { background: var(--surface); }

.row__btn:hover::before,
.row__btn[aria-expanded="true"]::before { opacity: 1; }

.row__name {
  color: var(--text);
  font-weight: var(--fw-medium);
}

.row__role {
  color: var(--text-muted);
  font-size: var(--fs-sm);
}

/* Three rows carry a case study. Nothing on a collapsed row said so, which
   made the only route to the long-form writing a lucky click. */
.row__flag {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  margin-left: var(--s3);
  font-size: var(--fs-xs);
  font-weight: var(--fw-regular);
  color: var(--text-dim);
  white-space: nowrap;
}

.row__flag::before {
  content: "";
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: var(--accent-fill);
}

/* The one place mono earns its keep in the list. */
.row__stack {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--text-dim);
}

/* The longest stack string — "Django · DRF · PostgreSQL · Redis · Celery" —
   needs 323px on one line, more than the plain 1fr track offers at these
   widths, so it wrapped and pushed that row to double height. `nowrap` raises
   the track's automatic minimum to the string's own width (`1fr` is
   `minmax(auto, 1fr)`), and the name column — which has far more room than
   any name needs — surrenders the difference instead.

   The breakpoint is measured, not guessed, and it moved from 1000px to
   1024px when the type changed to Google Sans: the wider names start wrapping
   a little sooner, and squeezing the name column below ~330px just trades a
   wrapped stack for a wrapped name. At 1024px and above every row is a single
   58px line; below it the rule lifts and the stack text wraps as before.
   Neither ever overflows or clips. */
@media (min-width: 1024px) {
  .row__stack { white-space: nowrap; }
}

.row__year {
  font-size: var(--fs-sm);
  color: var(--text-dim);
  text-align: right;
}

.row__panel {
  overflow: hidden;
  background: var(--surface);
  border-left: 2px solid var(--accent-fill);
}

.row__panel[hidden] { display: none; }

.row__panel-inner {
  padding: 0 var(--s5) var(--s6) var(--s5);
  max-width: var(--measure);
}

.row__panel-inner p { color: var(--text-muted); }

.row__links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s5);
  margin-top: var(--s4);
}

@media (max-width: 720px) {
  .row__btn {
    grid-template-columns: minmax(0, 1fr) auto;
    gap: var(--s1) var(--s4);
    padding-inline: var(--s4);
  }
  .row__name  { grid-column: 1; }
  .row__year  { grid-column: 2; grid-row: 1; }
  .row__role  { grid-column: 1 / -1; }
  .row__stack { grid-column: 1 / -1; }
  .row__panel-inner { padding-inline: var(--s4); }
}

/* No-JS fallback list */
.manifest-fallback { border-top: var(--rule); }

.manifest-fallback li {
  padding: var(--s4) var(--s5);
  border-bottom: var(--rule);
}

.manifest-fallback .row__stack { display: block; margin-top: var(--s1); }

/* ==========================================================================
   Experience: the only place a spine is justified
   ========================================================================== */

.timeline {
  position: relative;
  padding-left: var(--s6);
}

.timeline__rule {
  position: absolute;
  left: 0;
  top: 6px;
  bottom: 6px;
  width: 1px;
  background: var(--border);
  transform-origin: top center;
}

.job { position: relative; }
.job + .job { margin-top: var(--s7); }

.job__marker {
  position: absolute;
  left: calc(var(--s6) * -1 - 1px);
  top: 11px;
  width: 3px;
  height: 3px;
  border-radius: 50%;
  outline: 3px solid var(--bg);
  background: var(--accent-fill);
}

.job__head {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: baseline;
  gap: var(--s2) var(--s5);
}

.job__title { font-size: var(--fs-md); font-weight: var(--fw-medium); }
.job__org   { color: var(--text-muted); font-weight: var(--fw-regular); }
.job__dates { font-size: var(--fs-sm); color: var(--text-dim); white-space: nowrap; }

.job__bullets {
  margin-top: var(--s4);
  max-width: var(--measure);
}

.job__bullets li {
  position: relative;
  padding-left: var(--s5);
  color: var(--text-muted);
}

.job__bullets li + li { margin-top: var(--s2); }

.job__bullets li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.8em;
  width: 6px;
  height: 1px;
  background: var(--border-strong);
}

/* Education is the least load-bearing fact on the page for someone four years
   into the job, so it stays a quiet line rather than a block. It does get a
   label though — every other item on the page is labelled, and "BCA, ..." on
   its own asks the reader to work out what they are looking at. Same
   label | value grid as the Skills rows, so it is not a new pattern. */
.education {
  display: grid;
  grid-template-columns: 160px minmax(0, 1fr);
  gap: var(--s4);
  align-items: baseline;
  margin-top: var(--s6);
  padding-top: var(--s5);
  border-top: var(--rule);
  font-size: var(--fs-sm);
  color: var(--text-dim);
}

.education__label {
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  color: var(--text-muted);
}

@media (max-width: 640px) {
  .education { grid-template-columns: 1fr; gap: var(--s1); }
}

/* ==========================================================================
   Skills: grouped lists, no pills, no meters
   ========================================================================== */

.skills__group {
  display: grid;
  grid-template-columns: 160px minmax(0, 1fr);
  gap: var(--s4);
  align-items: baseline;
  padding-block: var(--s4);
  border-bottom: var(--rule);
}

.skills__group:first-child { border-top: var(--rule); }

.skills__label {
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  color: var(--text);
}

.skills__items { color: var(--text-muted); }

.skills__note {
  margin-top: var(--s5);
  font-size: var(--fs-sm);
  color: var(--text-dim);
  max-width: var(--measure);
}

@media (max-width: 640px) {
  .skills__group { grid-template-columns: 1fr; gap: var(--s1); }
}

/* ==========================================================================
   About
   ========================================================================== */

.about p { max-width: 62ch; color: var(--text-muted); }
.about p + p { margin-top: var(--s5); }
.about p:first-of-type { color: var(--text); }

/* ==========================================================================
   Contact
   ========================================================================== */

.contact__list { margin-top: var(--s5); border-top: var(--rule); }

.contact__item {
  display: grid;
  grid-template-columns: 160px minmax(0, 1fr);
  gap: var(--s4);
  align-items: baseline;
  padding: var(--s4) 0;
  border-bottom: var(--rule);
}

.contact__label { font-size: var(--fs-sm); color: var(--text-dim); }
.contact__value a { text-decoration: none; }
.contact__value a:not(.btn--wa):hover { color: var(--accent-hover); }

@media (max-width: 640px) {
  .contact__item { grid-template-columns: 1fr; gap: var(--s1); }
}

/* ==========================================================================
   Footer
   ========================================================================== */

.footer {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--s7);
  padding-block: var(--s7);
  border-top: var(--rule);
  font-size: var(--fs-sm);
}

.footer__heading {
  font-size: var(--fs-sm);
  font-weight: var(--fw-medium);
  color: var(--text-muted);
  margin-bottom: var(--s4);
}

.footer__col li + li { margin-top: var(--s2); }

.footer a {
  color: var(--text);
  text-decoration: none;
}

.footer a:hover { color: var(--accent); }

/* On its own case study, a page does not link to itself. */
.footer__current { color: var(--text-dim); }

.footer__col--end {
  text-align: right;
  color: var(--text-muted);
}

.footer__col--end p + p { margin-top: var(--s2); }
.footer__col--end .footer__name { color: var(--text); font-weight: var(--fw-medium); }

@media (max-width: 720px) {
  .footer {
    grid-template-columns: 1fr;
    gap: var(--s6);
    padding-block: var(--s7);
  }
  .footer__col--end { text-align: left; }
}

/* ==========================================================================
   Case study
   ========================================================================== */

.case__back { font-size: var(--fs-sm); color: var(--text-muted); text-decoration: none; }
.case__back:hover { color: var(--accent-hover); }

.case__header { padding-top: var(--s7); }

.case__summary {
  margin-top: var(--s5);
  font-size: var(--fs-md);
  color: var(--text-muted);
  max-width: 60ch;
}

.meta-strip {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  gap: var(--s5);
  margin-top: var(--s7);
  padding-block: var(--s5);
  border-top: var(--rule);
  border-bottom: var(--rule);
}

.meta-strip dt {
  font-size: var(--fs-xs);
  color: var(--text-dim);
  margin-bottom: var(--s1);
}

.meta-strip dd { margin: 0; font-size: var(--fs-sm); color: var(--text); }

.case__index ol { border-left: var(--rule); }

.case__index a {
  display: block;
  padding: var(--s2) var(--s4);
  font-size: var(--fs-sm);
  color: var(--text-dim);
  text-decoration: none;
  border-left: 2px solid transparent;
  margin-left: -1px;
}

.case__index a:hover,
.case__index a.is-active { color: var(--text); border-left-color: var(--accent); }

.case__section + .case__section { margin-top: var(--s7); }
.case__section h2 { font-size: var(--fs-lg); }
.case__section > * + * { margin-top: var(--s4); }
.case__section p { color: var(--text-muted); }

.case__list li {
  position: relative;
  padding-left: var(--s5);
  color: var(--text-muted);
  max-width: var(--measure);
}

.case__list li + li { margin-top: var(--s2); }

.case__list li::before {
  content: "";
  position: absolute;
  left: 0;
  top: 0.8em;
  width: 6px;
  height: 1px;
  background: var(--border-strong);
}

.case__list strong { color: var(--text); font-weight: var(--fw-medium); }

/* A pull-out for the one genuine emphasis moment per case study. */
.callout {
  padding: var(--s5);
  background: var(--surface);
  border-left: 2px solid var(--accent-fill);
  border-radius: 0 var(--r-sm) var(--r-sm) 0;
  max-width: var(--measure);
}

.callout p { color: var(--text); }

.case__next {
  margin-top: var(--s8);
  padding-top: var(--s5);
  border-top: var(--rule);
  font-size: var(--fs-sm);
}

/* ==========================================================================
   Code block
   ========================================================================== */

.code {
  margin: 0;
  background: var(--surface-raised);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  overflow: hidden;
}

.code__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s4);
  padding: var(--s2) var(--s3) var(--s2) var(--s4);
  border-bottom: 1px solid var(--border);
}

.code__lang {
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--text-dim);
  order: 2;
}

.code__copy {
  order: 1;
  padding: var(--s1) var(--s3);
  font-size: var(--fs-xs);
  color: var(--text-muted);
  border: 1px solid var(--border-strong);
  border-radius: var(--r-sm);
}

.code__copy:hover { color: var(--text); border-color: var(--accent); }

.code pre {
  margin: 0;
  padding: var(--s4);
  overflow-x: auto;
  font-size: var(--fs-sm);
  line-height: 1.6;
  color: var(--text);
  tab-size: 4;
}

.code pre code { background: none; padding: 0; font-family: var(--font-mono); }

/* Prism tokens mapped onto this palette rather than a stock theme. */
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata { color: var(--text-dim); }
.token.punctuation { color: var(--text-muted); }
.token.keyword,
.token.boolean,
.token.builtin { color: var(--accent); }
.token.string,
.token.char,
.token.triple-quoted-string { color: var(--code-string); }
.token.number,
.token.constant { color: var(--code-number); }
.token.function,
.token.class-name { color: var(--code-fn); }
.token.decorator,
.token.operator { color: var(--accent-hover); }

/* The no-JS list is real markup, not a noscript block, so it is indexable.
   When JS is available it is hidden before paint, then removed outright. */
.js .manifest-fallback { display: none; }

/* ==========================================================================
   Glass — the one morphism on this site

   Frosted translucent panels, used by the stats band and the process cards
   only. They are deliberately not applied to the manifest, the timeline or
   the case studies: those carry the substance, and glass on everything is
   exactly how a site stops looking designed and starts looking themed.
   ========================================================================== */

.glass {
  position: relative;
  background: var(--glass-bg);
  border: 1px solid var(--glass-border);
  border-radius: var(--r-lg);
  box-shadow: var(--glass-shadow);
  backdrop-filter: blur(var(--glass-blur)) saturate(140%);
  -webkit-backdrop-filter: blur(var(--glass-blur)) saturate(140%);
}

/* The light catch along the top edge — what sells it as a physical sheet. */
.glass::after {
  content: "";
  position: absolute;
  inset: 0 0 auto 0;
  height: 1px;
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  background: linear-gradient(90deg, transparent, var(--glass-highlight), transparent);
  pointer-events: none;
}

/* Where the browser cannot blur, fall back to an opaque surface rather than
   leaving text floating on a see-through panel. */
@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {
  .glass { background: var(--surface); }
}

/* ==========================================================================
   Stats band
   ========================================================================== */

.stats {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: var(--s5);
}

.stat { position: relative; }

/* The offset edge: a second rounded outline sitting behind and below, so the
   card reads as lifted without a drop shadow doing the work. It lives on the
   wrapper — inside the glass panel it would paint behind that panel's own
   background, since backdrop-filter opens a stacking context. */
.stat::before {
  content: "";
  position: absolute;
  inset: 12px -7px -9px 12px;
  border-radius: var(--r-lg);
  border: 2px solid var(--accent-fill);
  border-top: 0;
  border-left: 0;
  opacity: 0.6;
}

/* Alternating edge colour, taken from the ring's far end rather than from a
   new hue invented for the occasion. */
.stat:nth-child(even)::before { border-color: var(--ref-edge); }

.stat__card {
  position: relative;
  height: 100%;
  padding: var(--s5);
  transition: border-color var(--dur-base) var(--ease),
              transform var(--dur-base) var(--ease);
}

/* The offset edge brightens and the card slides onto it, so the lift that was
   only implied becomes real on hover. */
.stat::before {
  transition: opacity var(--dur-base) var(--ease),
              border-color var(--dur-base) var(--ease);
}

.stat:hover .stat__card {
  border-color: var(--accent-fill);
  transform: translate(3px, 3px);
}

.stat:hover::before { opacity: 1; }

.stat:hover .stat__figure { color: var(--accent); }

.stat__figure { transition: color var(--dur-base) var(--ease); }

@media (prefers-reduced-motion: reduce) {
  .stat:hover .stat__card { transform: none; }
}

.stat__figure {
  font-size: clamp(2rem, 4vw, 2.6rem);
  font-weight: var(--fw-semi);
  letter-spacing: -0.03em;
  line-height: 1.05;
  color: var(--text);
}

.stat__label {
  margin-top: var(--s2);
  font-size: var(--fs-sm);
  color: var(--text-on-glass);
}

@media (max-width: 860px) {
  .stats { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--s4); }
}

@media (max-width: 420px) {
  .stats { grid-template-columns: 1fr; }
}

/* ==========================================================================
   How I work — the process steps
   ========================================================================== */

.process {
  position: relative;
  isolation: isolate;
  background: var(--grad-wash);
  /* The ring deliberately bleeds past the right edge. `clip` crops it without
     creating a scroll container — `hidden` here would both add a horizontal
     scrollbar's worth of trouble and risk the sticky-nav bug again. */
  overflow-x: clip;
}

/* Anchoring the ring to the heading block rather than to the section means it
   cannot creep over the card grid as the layout reflows: wherever the heading
   ends, the ring ends. */
.process .section__head { position: relative; }

/* The ring is decoration and nothing more: it is pinned outside the text
   column, marked aria-hidden, and never sits behind a word — so it cannot
   affect any contrast measurement. */
/* Measured, not eyeballed: bleeding down behind the first row of cards, the
   ring's glow lifted the frosted panel enough to drop muted body text to
   3.90:1 in dark — under AA. Anchored to the heading block it clears the grid
   at every width, so no text is ever composited over it. */
.process__ring {
  position: absolute;
  right: -180px;
  bottom: 0;
  width: 480px;
  max-width: 55vw;
  opacity: var(--ring-opacity);
  z-index: -1;
  pointer-events: none;
  user-select: none;
}

.steps {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--s5);
}

.step {
  display: flex;
  flex-direction: column;
  padding: var(--s5) var(--s5) var(--s5);
}

.step__num {
  font-size: 3.4rem;
  font-weight: var(--fw-semi);
  line-height: 1;
  letter-spacing: -0.04em;
  color: var(--step-ghost);
}

/* Gradient-filled numerals. The flat colour above stays as the fallback —
   `color: transparent` without working background-clip would erase them. */
@supports ((background-clip: text) or (-webkit-background-clip: text)) {
  .step__num {
    background: var(--grad-num);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    opacity: 0.55;
  }
}

.step__title {
  margin-top: var(--s3);
  font-size: var(--fs-md);
  font-weight: var(--fw-medium);
  color: var(--text);
}

.step__body {
  margin-top: var(--s3);
  font-size: var(--fs-sm);
  color: var(--text-on-glass);
}

.step code {
  font-size: 0.85em;
  color: var(--accent);
}

/* The dashed run between steps, borrowed from the reference's step rail.
   Drawn only where three sit in a row, and never under the last one. */
@media (min-width: 861px) {
  .step::before {
    content: "";
    position: absolute;
    top: calc(var(--s6) + 1.7rem);
    right: calc(var(--s5) * -1);
    width: var(--s5);
    border-top: 1px dashed var(--border-strong);
  }
  .step:nth-child(3n)::before,
  .step:last-child::before { content: none; }
}

@media (max-width: 860px) {
  .steps { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .process__ring { right: -120px; width: 360px; }
}

@media (max-width: 620px) {
  .steps { grid-template-columns: 1fr; }
  .process__ring { display: none; }
}

/* ==========================================================================
   WhatsApp
   ========================================================================== */

/* Fixed, so without this the two bright circles float over the open mobile
   menu. */
body.menu-open .float-stack {
  opacity: 0;
  pointer-events: none;
}

/* The two floating actions, stacked. Call sits above WhatsApp. */
.float-stack {
  position: fixed;
  right: clamp(16px, 3vw, 28px);
  bottom: clamp(16px, 3vw, 28px);
  z-index: 60;
  display: flex;
  flex-direction: column;
  gap: var(--s3);
}

.float-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 52px;
  height: 52px;
  border-radius: 50%;
  box-shadow: 0 10px 28px -10px rgba(0, 0, 0, 0.55);
  transition: background var(--dur-fast) var(--ease),
              transform var(--dur-fast) var(--ease);
}

.float-btn svg { width: 24px; height: 24px; fill: currentColor; }

.float-btn:hover { transform: translateY(-2px); }

/* Not WhatsApp's #25D366: white on that brand green measures 1.98:1. This is
   the lightest green that clears AA for the white glyph. */
.float-btn--wa { background: #17833F; color: #FFFFFF; }
.float-btn--wa:hover { background: #146E35; color: #FFFFFF; }

/* Amber fill with the dark glyph that sits on it elsewhere — 8.14:1. */
.float-btn--call { background: var(--accent-fill); color: var(--on-accent-fill); }
.float-btn--call:hover { background: var(--accent-fill-hover); color: var(--on-accent-fill); }

@media (prefers-reduced-motion: reduce) {
  .float-btn:hover { transform: none; }
}

@media print {
  .float-stack { display: none; }
}

/* The inline button in the contact section. */
.btn--wa {
  display: inline-flex;
  align-items: center;
  gap: var(--s2);
  background: #17833F;   /* 4.82:1 with white — see the note above */
  color: #FFFFFF;
  border-color: transparent;
}

.btn--wa,
.btn--wa:link,
.btn--wa:visited { color: #FFFFFF; }

.btn--wa:hover,
.btn--wa:focus-visible {
  background: #146E35;
  color: #FFFFFF;
}

.btn--wa .social-ico,
.btn--wa:hover .social-ico { color: #FFFFFF; fill: #FFFFFF; }
.btn--wa svg { width: 17px; height: 17px; fill: currentColor; }

/* ==========================================================================
   Social icons
   ========================================================================== */

.footer__col li a {
  display: inline-flex;
  align-items: center;
  gap: var(--s3);
}

.social-ico {
  width: 16px;
  height: 16px;
  flex: none;
  fill: currentColor;
  color: var(--text-dim);
  transition: color var(--dur-fast) var(--ease);
}

.footer__col li a:hover .social-ico { color: var(--accent); }

.contact__value a {
  display: inline-flex;
  align-items: center;
  gap: var(--s3);
}

/* `:not(.btn--wa)` matters: without it these out-specify .btn--wa and repaint
   the button's label and glyph in the accent colour on a green ground. */
.contact__value a:not(.btn--wa) .social-ico { color: var(--text-dim); }
.contact__value a:not(.btn--wa):hover .social-ico { color: var(--accent); }

/* ==========================================================================
   Experience — roles as cards beside a summary panel

   The old shape was a bare timeline. This keeps the rail and its draw-on-entry
   animation, but the roles are now glass cards and the right column carries
   the work that was never a job title of its own.
   ========================================================================== */

.xp {
  display: grid;
  grid-template-columns: minmax(0, 1.25fr) minmax(0, 0.85fr);
  gap: var(--s6);
  align-items: start;
}

.xp__roles { padding-left: var(--s6); }

.xp .job {
  padding: var(--s5);
}

.xp .job + .job { margin-top: var(--s4); }

.xp .job__marker { top: 30px; }

.xp .job__head {
  display: block;
}

.xp .job__title { font-size: var(--fs-base); }

.xp .job__org {
  display: block;
  margin-top: var(--s1);
  font-size: var(--fs-sm);
}

.xp .job__dates {
  display: inline-block;
  margin-top: var(--s3);
  padding: var(--s1) var(--s3);
  border: 1px solid var(--border-strong);
  border-radius: 999px;
  font-size: var(--fs-xs);
  color: var(--text-muted);
}

.xp .job__bullets { margin-top: var(--s4); }

/* ---- the summary panel ---- */

.xp__card { padding: var(--s5); }

.xp__card-title {
  font-size: var(--fs-md);
  font-weight: var(--fw-medium);
  color: var(--text);
}

.xp__card-intro {
  margin-top: var(--s3);
  font-size: var(--fs-sm);
  color: var(--text-on-glass);
}

.pills { margin-top: var(--s5); }

.pills li {
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--grad-pill);
  font-size: var(--fs-sm);
  color: var(--text);
  transition: border-color var(--dur-fast) var(--ease);
}

/* The padding lives on the inner element so the whole pill is the hit area
   when it is a link, and the two kinds of pill still measure the same. */
.pill__body {
  display: block;
  padding: var(--s3) var(--s4);
  color: inherit;
  text-decoration: none;
  border-radius: var(--r-lg);
}

/* Only the linked pills react. SMS Varanasi has no public URL, so it stays
   plain text rather than pretending to be clickable.

   The ring is an inset shadow on the link rather than a border change on the
   li via `:has()`, so it works in browsers that do not have `:has()` yet. */
a.pill__body {
  transition: box-shadow var(--dur-fast) var(--ease);
}

a.pill__body:hover {
  box-shadow: inset 0 0 0 1px var(--accent);
}

a.pill__body:hover strong,
a.pill__body:focus-visible strong {
  text-decoration: underline;
  text-underline-offset: 3px;
}

.pills li:nth-child(even) { background: var(--grad-pill-2); }
.pills li + li { margin-top: var(--s2); }

.pills strong { font-weight: var(--fw-medium); }

.pills span {
  display: block;
  margin-top: var(--s1);
  font-size: var(--fs-xs);
  color: var(--text-on-glass);
}

@media (max-width: 900px) {
  .xp { grid-template-columns: 1fr; gap: var(--s5); }
}

/* ==========================================================================
   Ambient motion

   Two small ones, both CSS, both gated on the motion preference rather than
   on JavaScript — so they are off before a single script has run.
   ========================================================================== */

@media (prefers-reduced-motion: no-preference) {
  /* The ring breathes rather than spins. A full rotation would walk the gap
     in the arc around to the bottom, which breaks the composition the section
     was laid out around. */
  .process__ring {
    animation: ring-drift 26s ease-in-out infinite alternate;
    transform-origin: 55% 60%;
  }

  @keyframes ring-drift {
    from { transform: rotate(-5deg) scale(1); }
    to   { transform: rotate(5deg) scale(1.03); }
  }

  /* The theme icon turns over rather than cutting. */
  .nav__theme svg {
    transition: transform var(--dur-base) var(--ease),
                opacity var(--dur-base) var(--ease);
  }

  .nav__theme:hover svg { transform: rotate(18deg); }
}

/* ==========================================================================
   404
   ========================================================================== */

.notfound { padding-block: var(--s9) var(--section-y); }

.notfound__code {
  font-size: clamp(4rem, 14vw, 9rem);
  font-weight: var(--fw-semi);
  line-height: 0.9;
  letter-spacing: -0.05em;
  color: var(--step-ghost);
}

@supports ((background-clip: text) or (-webkit-background-clip: text)) {
  .notfound__code {
    background: var(--grad-num);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    opacity: 0.6;
  }
}

.notfound h1 { margin-top: var(--s4); }

.notfound__lede {
  margin-top: var(--s4);
  font-size: var(--fs-md);
  color: var(--text-muted);
  max-width: 48ch;
}

.notfound__links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s3);
  margin-top: var(--s6);
}

.notfound__list {
  margin-top: var(--s9);
  padding-top: var(--s5);
  border-top: var(--rule);
  max-width: var(--measure);
}

.notfound__list h2 { font-size: var(--fs-md); font-weight: var(--fw-medium); }
.notfound__list ul { margin-top: var(--s4); }

.notfound__list li {
  padding-block: var(--s3);
  border-bottom: var(--rule);
  color: var(--text-muted);
  font-size: var(--fs-sm);
}

/* ==========================================================================
   Projects gallery
   ========================================================================== */

.gallery {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--s5);
}

.pcard {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border: 1px solid var(--border);
  border-radius: var(--r-lg);
  background: var(--surface);
  transition: border-color var(--dur-fast) var(--ease),
              transform var(--dur-fast) var(--ease);
}

.pcard[hidden] { display: none; }

.pcard:hover {
  border-color: var(--accent);
  transform: translateY(-3px);
}

.pcard__media {
  position: relative;
  aspect-ratio: 16 / 10;
  background: var(--surface-raised);
  border-bottom: 1px solid var(--border);
  overflow: hidden;
}

.pcard__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: top center;
}

/* Shown when a project has no screenshot — the initials stand in rather than
   leaving a hole in the grid. */
.pcard__fallback {
  position: absolute;
  inset: 0;
  display: none;
  align-items: center;
  justify-content: center;
  font-size: 2.4rem;
  font-weight: var(--fw-semi);
  letter-spacing: -0.03em;
  color: var(--step-ghost);
  background: var(--grad-pill-2, var(--surface-raised));
}

.pcard--noimg .pcard__media img { display: none; }
.pcard--noimg .pcard__fallback { display: flex; }

.pcard__body {
  display: flex;
  flex-direction: column;
  flex: 1;
  padding: var(--s4) var(--s5) var(--s5);
}

.pcard__name {
  font-size: var(--fs-base);
  font-weight: var(--fw-medium);
  color: var(--text);
}

.pcard__meta {
  margin-top: var(--s1);
  font-size: var(--fs-sm);
  color: var(--text-muted);
}

.pcard__stack {
  margin-top: var(--s3);
  font-family: var(--font-mono);
  font-size: var(--fs-xs);
  color: var(--text-dim);
}

.pcard__links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--s4);
  margin-top: auto;
  padding-top: var(--s4);
}

.gallery__count {
  margin-top: var(--s6);
  font-size: var(--fs-sm);
  color: var(--text-dim);
}

@media (max-width: 900px) {
  .gallery { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}

@media (max-width: 560px) {
  .gallery { grid-template-columns: 1fr; gap: var(--s4); }
}
