/* Hero */

/* Top-aligned, not bottom-aligned. The text column is taller than the portrait,
   so bottom-aligning pushed the portrait below the name and left a hole beside
   it. Aligning the tops reads as one block. */
.hero {
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  gap: var(--space-8);
  padding-block: var(--space-16) var(--space-12);
}

/* A circle, cropped to the face rather than centred on the frame: the source is
   a 827x1063 portrait, so a square crop centred at 50% 50% would cut the chin.
   The two rings are one element: a band of the page ground, then a hairline.
   No wrapper, no extra markup. */
.hero-portrait {
  width: 11rem;
  height: 11rem;
  border-radius: 50%;
  object-fit: cover;
  object-position: 50% 18%;
  filter: saturate(0.96);
  /* Band in the page ground, then a hairline that actually contrasts. See
     --portrait-ring in css/tokens.css for why this is not --ground-raised. */
  box-shadow: 0 0 0 5px var(--ground), 0 0 0 6px var(--portrait-ring);
  /* The rings sit outside the box, so keep them off the section edge. */
  margin: 6px;
}

.hero-text { flex: 1 1 20rem; }

.hero-name {
  font-size: var(--text-2xl);
  letter-spacing: -0.015em;
}

/* His name in his own script, at full accent. It was --ink-muted, which filed
   it as metadata under the Latin name — a caption on a heading. It is not a
   caption; it is the same name, and the only place the accent stands for
   identity rather than structure. The Latin name above stays --ink on purpose:
   two names in accent would make the top of the page a logotype.

   700 because Amiri's regular is fine-stroked and went thin at --text-xl the
   moment it stopped being near-black. The weight is already in the subset. */
.hero-name-ar {
  font-family: var(--font-arabic);
  font-size: var(--text-xl);
  font-weight: 700;
  color: var(--accent);
  margin: var(--space-1) 0 var(--space-4);
  /* Physical `left`, deliberately. dir="rtl" is correct for the script, but it
     makes the inherited `text-align: start` resolve to the right edge, which
     flushed the Arabic name 326px away from the Latin name it translates on a
     phone. Do not "modernise" this to `start`; that is the bug. */
  text-align: left;
}

.hero-role {
  font-family: var(--font-meta);
  font-size: var(--text-sm);
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--ink-muted);
  margin-bottom: var(--space-3);
}

.hero-tagline {
  max-width: var(--measure-prose);
  font-size: var(--text-lg);
}

/* Topic pills: the thirty-second scan, before anyone reads a sentence. They
   state facts without tense, which is why the degree lives here and not in the
   tagline. Outlined rather than filled, so they read as labels on the page
   rather than as buttons. */
.hero-pills {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2);
  margin: var(--space-6) 0 var(--space-4);
}
/* The pills take the accent in their letterforms and nowhere else: the fill is
   still plain white and only the border moves to --rule-accent, so DESIGN.md's
   "outlined rather than filled so they read as labels, not buttons" still
   holds. 6.63:1 on the white fill.

   The page-wide tell that keeps this unambiguous is the underline. Every anchor
   is underlined by default and css/base.css never turns it off, so blue with an
   underline is clickable and blue without one is a label. That is why every
   accent decision below can use plain --accent rather than inventing a second,
   quieter blue to mean "not a link". */
.hero-pills li {
  font-family: var(--font-meta);
  font-size: var(--text-xs);
  line-height: 1.4;
  color: var(--accent);
  background: var(--ground-raised);
  border: var(--rule-width) solid var(--rule-accent);
  border-radius: 999px;
  padding: 0.3em 0.75em;
}
.hero-pills li[lang="ar"] {
  font-family: var(--font-arabic);
  font-size: var(--text-sm);
  padding-block: 0.15em;
}

/* The ask, on a plate. This line was deliberately unmarked for a while — the
   reasoning being that a chip reads as a fourth pill and a dot is LinkedIn's
   open-to-work grammar, which is not this page's register. Ahmed's judgement,
   which decides it: unmarked, it did not stop anyone. So it takes a surface
   rather than a badge, and no motion: a plate is a place on the page, where a
   pulsing dot is a notification. It stays --ink, because the sentence is a
   statement he makes and the accent is reserved for structure. */
.hero-status {
  max-width: var(--measure-prose);
  margin-bottom: var(--space-4);
  padding: var(--space-3) var(--space-4);
  color: var(--ink);
  background: var(--accent-plate);
  border: var(--rule-width) solid var(--rule-accent);
  /* Not 999px: that would make it a very large pill, which is the reading the
     comment above rules out. */
  border-radius: 0.375rem;
}

.hero-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  font-family: var(--font-meta);
  font-size: var(--text-sm);
}

/* Link icons. The mark and its label are one object: inline-flex so they never
   wrap apart, and the underline the page uses to mean "link" runs under the
   text only — an underlined icon reads as a drawing error. */
.hero-links a,
.entry-more a,
.work-links a {
  display: inline-flex;
  align-items: center;
  gap: 0.45em;
  text-decoration: none;
}
.hero-links a span,
.entry-more a span,
.work-links a span { text-decoration: underline; }

.link-icon {
  flex: none;
  width: 1.05em;
  height: 1.05em;
  /* Optical, not geometric: a 24-unit glyph beside 15px type sits a hair high
     on the baseline it shares. */
  transform: translateY(0.5px);
}
/* The outbound marker is a signal, not a brand: smaller and quieter than a
   logo, and it follows the label rather than leading it. */
.link-icon.is-external {
  width: 0.85em;
  height: 0.85em;
  order: 1;
  opacity: 0.75;
}

/* Timeline */

/* One continuous rail behind every badge. Drawn on the wrapper rather than per
   entry, because a line that restarts at each boundary reads as a stack of
   lists rather than one chronology. */
.timeline {
  position: relative;
  --rail: 3.25rem;
  --stop: 1.875rem;
}
.timeline::before {
  content: "";
  position: absolute;
  /* Start on the first badge's centre so the badge occludes the line, which is
     what makes a date read as a station on it, and run to the bottom edge,
     which is the last entry's last line. Ending on the last badge's centre
     instead would need that entry's height, which CSS cannot know here; the
     short tail below the final station is the cheaper of the two. */
  top: calc(var(--rail) / 2);
  bottom: 0;
  left: calc(var(--rail) / 2);
  width: var(--rule-width);
  background: var(--rule);
}

/* Two sizes of station on one rail. The year opens a run and is the larger
   circle; each entry marks its month underneath in a smaller one. Both sit on
   the rail with the page ground behind them, which is what makes a date read as
   a station rather than a label beside one — so neither fill can be tinted or
   translucent. --rule-accent on both, the same hairline the pills use. */
.year-group { padding-block: var(--space-8); }
.year-group:first-child { padding-top: 0; }
.year-group:last-child { padding-bottom: 0; }

.year-label {
  position: relative;
  z-index: 1;
  width: var(--rail);
  height: var(--rail);
  display: grid;
  place-items: center;
  margin-bottom: var(--space-6);
  border-radius: 50%;
  border: var(--rule-width) solid var(--rule-accent);
  background: var(--ground);
  font-family: var(--font-meta);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--accent);
  font-variant-numeric: tabular-nums;
}

.entries { display: grid; gap: var(--space-8); }

/* Each entry is a row on the rail: stop column, then the entry itself. */
.entry {
  display: grid;
  grid-template-columns: var(--rail) 1fr;
  gap: var(--space-6);
  align-items: start;
}
/* Grid children default to min-width:auto, so one long unbroken token in a
   bullet would push the column wider than the page instead of wrapping. */
.entry-body { min-width: 0; }

.entry-badge {
  position: relative;
  z-index: 1;
  justify-self: center;
  width: var(--stop);
  height: var(--stop);
  display: grid;
  place-items: center;
  border-radius: 50%;
  border: var(--rule-width) solid var(--rule-accent);
  background: var(--ground);
  font-family: var(--font-meta);
  font-size: 0.625rem;
  font-weight: 600;
  letter-spacing: 0.01em;
  color: var(--accent);
}
/* The one entry whose source date carries no month. It keeps the column so the
   grid stays aligned, and shows nothing: the year station directly above it is
   already the marker, and an invented month would be a lie. */
.entry-badge.is-undated { border: 0; background: none; }

.entry-title {
  font-size: var(--text-lg);
  font-weight: 600;
}

.entry-meta {
  display: flex;
  flex-wrap: wrap;
  /* The type label runs a size smaller than the rest of the row, so the items
     sit on a shared baseline rather than on a shared box edge. The logo sets
     its own align-self and is unaffected. */
  align-items: baseline;
  gap: var(--space-3);
  margin: var(--space-1) 0 var(--space-3);
  font-family: var(--font-meta);
  font-size: var(--text-sm);
  color: var(--ink-muted);
}

/* The organisation mark: a small anchor at the head of the meta row, sized to
   the row's own line box so it sits with the text rather than above it. A fixed
   square means it reserves its space before it loads, so no explicit intrinsic
   dimensions are needed on the element. */
.entry-logo {
  flex: none;
  align-self: center;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.375rem;
  height: 1.375rem;
  margin-right: calc(var(--space-3) * -1 + var(--space-2));
}
.entry-logo img {
  width: 100%;
  height: 100%;
  object-fit: contain;
}
/* A mark that ships in one colourway only. All three here are dark ink, so on
   the dark ground they need something to sit on; --logo-ground is transparent
   in light mode, where they need nothing. */
.entry-logo.is-plated {
  padding: 2px;
  border-radius: 2px;
  background: var(--logo-ground);
}
.entry-logo-dark { display: none; }
[data-theme="dark"] .entry-logo-light { display: none; }
[data-theme="dark"] .entry-logo-dark { display: block; }

/* The organisation reads slightly stronger than the date range beside it,
   so "Saal.ai" and "Sep 2024 — Present" don't just run together as one
   grey clause. Shared by the Selected Work meta row below. */
.entry-org, .work-org { color: var(--ink); font-weight: 500; }
.entry-dates, .work-dates { font-variant-numeric: tabular-nums; }
/* The type label: "Experience", "Certificate", "Volunteering". It classifies
   the entry rather than stating anything about it, so it is set as a label and
   not as another grey fact in the row — which is also why it takes the accent,
   the same job the accent does on skill and block labels. Trailing the row it
   was indistinguishable from the date beside it and, worse, sat at a different
   x on every entry, since the row's length varies with the employer's name.
   Leading, it holds one column the eye can run down. */
.entry-kind {
  flex: none;
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--accent);
}

.entry-note {
  font-family: var(--font-meta);
  font-size: var(--text-sm);
  color: var(--ink-muted);
}

.entry-bullets { display: grid; gap: var(--space-2); }
.entry-bullets li {
  position: relative;
  padding-left: 1.1em;
  max-width: var(--measure-prose);
}
.entry-bullets li::before {
  content: "";
  position: absolute;
  left: 0; top: 0.68em;
  width: 4px; height: 4px;
  border-radius: 50%;
  background: var(--accent);
}

/* A row now, not a single link: a project entry carries the jump into Selected
   Work plus its live destinations. Without the gap they ran together as one
   underlined string — "Read more about this workWebsiteApp Store". */
.entry-more {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-4);
  margin-top: var(--space-3);
  font-family: var(--font-meta);
  font-size: var(--text-sm);
}

/* Milestones read quieter than roles, since they are supporting evidence.
   Role, education, and project entries intentionally get no equivalent
   override here and share this base .entry treatment; see the companion
   note in test/css-contract.test.js's class-coverage allowlist. */
.entry.is-milestone .entry-title {
  font-size: var(--text-base);
  font-weight: 500;
}
/* Un-override, not a new colour. .entry-org is promoted to full --ink at 500 so
   a company name reads stronger than the date range beside it — but a milestone
   carries no date range, so the promotion has no job here and simply outshouts
   the certificate title above it. Inherit .entry-meta's muted 400 instead. */
.entry.is-milestone .entry-org {
  color: inherit;
  font-weight: inherit;
}

/* Galleries */

.gallery { margin-top: var(--space-4); display: grid; gap: var(--space-3); }
.gallery.is-single { grid-template-columns: 1fr; }
/* Every photograph, in every gallery, shares one height ceiling. Applying it
   only to lone images left the grid uncapped, so a portrait in the spanning
   lead slot would have rendered half again as tall as the landscape it
   replaced. See --photo-cap in css/tokens.css for the number and why. */
.gallery .figure img { max-height: var(--photo-cap); width: auto; }

@media (min-width: 40rem) {
  .gallery.is-grid { grid-template-columns: 1fr 1fr; }
  /* With an odd count the LEAD image spans, and the rest pair off beneath it.
     It used to be the trailing one, which put the first two side by side — so
     the graduation ceremony sat level with the competition that followed it a
     month later, reading as two things that happened at once rather than in
     sequence. `:nth-last-child(odd)` on the first child is how a selector asks
     "is the total odd", since for the first child it counts the whole set. */
  .gallery.is-grid > .figure:first-child:nth-last-child(odd) { grid-column: 1 / -1; }
}

.figure { margin: 0; min-width: 0; }
.figure img { border: var(--rule-width) solid var(--rule); border-radius: 2px; }
.figure figcaption {
  margin-top: var(--space-2);
  font-family: var(--font-meta);
  font-size: var(--text-xs);
  color: var(--ink-muted);
}

/* Numbers */

.metrics {
  display: grid;
  gap: var(--space-6);
  /* 8rem, not 11rem: three figures fit three-across at 768 and 1280 rather
     than breaking 2+1 with an orphan, and fall to a clean 2-then-1 at 390
     (the last one spans below, see the media query under .metric). And not
     9rem either: --gutter is partly vw-based, so it keeps eating into the
     capped 736px shell as the viewport grows, leaving as little as 608px
     here. */
  grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
}
/* A bare grid-item wrapper: min-width: 0 keeps a long label from forcing
   the auto-fit track wider than the viewport, which is what would open a
   horizontal scrollbar. All other styling is on its children below. */
.metric { min-width: 0; }
/* Measured in headless Edge, not guessed: with three items .metrics holds 3
   columns until its content box drops below 432px, which is 476px of
   viewport (29.75rem) and below once --gutter is subtracted twice. Below
   that the third figure is a leftover orphan; span it full-width there only
   — at 768/1280 three items already fill three columns evenly.
   :nth-child(odd) keys this to an odd count rather than to "third", so a
   future fourth metric (even, filling 2x2) does not pick up a span it does
   not need. */
@media (max-width: 29.75rem) {
  .metric:last-child:nth-child(odd) { grid-column: 1 / -1; }
}
.metric-value {
  font-size: var(--text-xl);
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  margin-bottom: var(--space-1);
}
.metric-label {
  font-family: var(--font-meta);
  font-size: var(--text-sm);
  color: var(--ink-muted);
  max-width: 16rem;
}

/* Selected work */

.work + .work {
  margin-top: var(--space-12);
  padding-top: var(--space-12);
  border-top: var(--rule-width) solid var(--rule);
}
.work-title { font-size: var(--text-lg); }
.work-meta {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-3);
  margin: var(--space-1) 0 var(--space-4);
  font-family: var(--font-meta);
  font-size: var(--text-sm);
  color: var(--ink-muted);
}
.block-text { max-width: var(--measure-prose); }

.block-callout {
  max-width: var(--measure-prose);
  margin: var(--space-4) 0;
  padding: var(--space-4);
  background: var(--ground-raised);
  border: var(--rule-width) solid var(--rule);
  font-size: var(--text-sm);
  color: var(--ink-muted);
}
/* Tones step up emphasis through border colour only — no stripe, no
   colour-coded background, no generated label text (CSS ::before content
   is unreliably exposed to assistive tech, and the callout's own copy
   already says what it is). "Note" is the baseline above and gets no
   override; see the coverage allowlist for why. */
.block-callout.is-tip { border-color: var(--accent); }
.block-callout.is-warning { border-color: var(--accent-strong); color: var(--ink); }

/* Other block types from the registry. Only description, callout, and
   benchmark appear in data/projects.json today, but every renderer in
   js/blocks.js can be reached the moment the admin authors one, so each
   gets a real rule now rather than shipping unstyled the day it is used. */

/* js/blocks.js wraps a run of consecutive `responsibility` blocks in this list,
   so the bullets are a real list to assistive technology rather than orphaned
   <li> elements. Matches .entry-bullets, which does the same job for the
   bullets a timeline entry carries directly. */
.block-bullets {
  display: grid;
  gap: var(--space-2);
  margin: var(--space-4) 0;
}

.block-bullet {
  position: relative;
  padding-left: 1.1em;
  max-width: var(--measure-prose);
}
.block-bullet::before {
  content: "";
  position: absolute;
  left: 0; top: 0.68em;
  width: 4px; height: 4px;
  border-radius: 50%;
  background: var(--accent);
}

.block-metric {
  font-family: var(--font-meta);
  font-size: var(--text-sm);
  color: var(--ink-muted);
}
.block-metric-value {
  font-weight: 600;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
.block-metric-label { font-weight: 400; }

.block-link { font-family: var(--font-meta); font-size: var(--text-sm); }

.block-honor {
  font-family: var(--font-meta);
  font-size: var(--text-sm);
  color: var(--ink-muted);
  max-width: var(--measure-prose);
}

.block-thesis { max-width: var(--measure-prose); }
/* The same uppercase-sans label family as .skill-group-name, naming a piece of
   structure ("Thesis") rather than saying anything, so it takes the accent for
   the same reason. .benchmark thead th is that family too and deliberately does
   NOT: inside that table the accent already means "this row is his result", and
   a blue header would be competing with the one thing it marks. */
.block-label {
  font-family: var(--font-meta);
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--accent);
  margin-right: var(--space-2);
}

.block-coursework {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-1) var(--space-3);
  font-family: var(--font-meta);
  font-size: var(--text-sm);
  color: var(--ink-muted);
}

/* No font-family override here: the user-agent stylesheet already sets
   `code { font-family: monospace }`, and tokens.css asserts no monospace
   family is declared, since the terminal metaphor is retired. overflow-x
   keeps a long unwrapped line from opening the page's own horizontal
   scrollbar. */
.block-code {
  margin: var(--space-4) 0;
  padding: var(--space-4);
  overflow-x: auto;
  background: var(--ground-raised);
  border: var(--rule-width) solid var(--rule);
  font-size: var(--text-sm);
  line-height: var(--leading-tight);
}

.benchmark {
  margin: var(--space-4) 0;
  border-collapse: collapse;
  font-variant-numeric: tabular-nums;
  width: 100%;
  max-width: var(--measure-prose);
}
.benchmark caption {
  caption-side: bottom;
  margin-top: var(--space-2);
  text-align: left;
  font-family: var(--font-meta);
  font-size: var(--text-xs);
  color: var(--ink-muted);
}
.benchmark th, .benchmark td {
  padding: var(--space-2) var(--space-3);
  text-align: left;
  border-bottom: var(--rule-width) solid var(--rule);
  font-weight: 400;
}
.benchmark thead th {
  font-family: var(--font-meta);
  font-size: var(--text-xs);
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-muted);
}
.benchmark td { text-align: right; }
.benchmark .is-mine th, .benchmark .is-mine td {
  color: var(--accent-strong);
  font-weight: 600;
}

/* The Stmnt captures are 1:2.17 portraits. Constrain them so one screenshot
   does not consume a whole screen of scroll. */
.work .gallery.is-grid { grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr)); }
@media (min-width: 40rem) {
  .work .gallery.is-grid > .figure:first-child:nth-last-child(odd) { grid-column: auto; }
}

.work-tags, .work-links {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-2) var(--space-3);
  margin-top: var(--space-4);
  font-family: var(--font-meta);
  font-size: var(--text-xs);
}
.work-tags span {
  padding: 2px var(--space-2);
  border: var(--rule-width) solid var(--rule);
  color: var(--ink-muted);
}
.work-links { font-size: var(--text-sm); gap: var(--space-4); }

/* Skills */

/* Rows, not a card grid. Six categories in an auto-fit grid put every group in
   its own box of empty space, and the items inside were set in the prose serif
   with only a gap between them — so "Python R MATLAB Java" read as a sentence
   with the punctuation missing. As rows with a label column and a hairline
   between them, the section scans like the rest of the document. */
.skill-groups { display: grid; }
.skill-group {
  display: grid;
  grid-template-columns: 11rem 1fr;
  gap: var(--space-2) var(--space-6);
  align-items: baseline;
  padding-block: var(--space-4);
  border-top: var(--rule-width) solid var(--rule);
}
.skill-group:first-child { border-top: 0; padding-top: 0; }
/* Sub-headings, so they follow .section-heading rather than the metadata they
   sit above. Skills is the one section with no photograph, no link and no
   badge — the flattest, greyest stretch of the page — and these four or five
   words are its entire structure. 6.41:1 on the ground at 13px, which clears
   AA for normal text, not just for large. */
.skill-group-name {
  font-family: var(--font-meta);
  font-size: var(--text-xs);
  font-weight: 600;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: var(--space-3);
}
.skill-list {
  display: flex;
  flex-wrap: wrap;
  /* The metadata face, not the prose serif. These are labels, not sentences,
     and setting them in the reading face is most of why the section read as
     a run-on line. */
  font-family: var(--font-meta);
  font-size: var(--text-sm);
  gap: var(--space-1) var(--space-2);
}
/* The page's own separator, the one the pills and entry meta rows already use.
   Quiet enough to be punctuation rather than a third column of content. */
.skill-list li:not(:last-child)::after {
  content: "·";
  margin-left: var(--space-2);
  color: var(--ink-muted);
}
/* What he reaches for most. Everything else stays at full ink — this is
   emphasis, not demotion, and nothing on the list is filler. Muting the rest
   instead was built and compared: it reads calmly until a category holds
   nothing emphasised, and then the whole row goes grey and looks switched
   off. */
.skill-list li.is-primary { font-weight: 600; }
.skill-level { color: var(--ink-muted); }

/* Contact */

.contacts { display: grid; gap: var(--space-3); }
.contact-row {
  display: grid;
  grid-template-columns: 7rem 1fr;
  gap: var(--space-3);
  font-family: var(--font-meta);
  font-size: var(--text-sm);
}
/* The mark sits with the label, not beside the address: next to
   "AhmedARadhi00@gmail.com" an envelope only repeats the word "Email" that is
   already in the column to its left. */
.contact-label {
  display: flex;
  align-items: center;
  gap: 0.45em;
  color: var(--ink-muted);
}

/* Narrow screens: the year column stacks above its entries. */
@media (max-width: 40rem) {
  /* The rail survives the phone. Stacking the badge above its entry was what
     made the timeline read as an undifferentiated list on mobile, which is the
     surface most readers actually arrive on. It only narrows. */
  /* The label column costs more than it buys under about 30rem of content
     width, so the category name stacks above its row. */
  .skill-group { grid-template-columns: 1fr; }

  /* Three phone screenshots in a 390px column get about 140px each, at which
     the app's own interface is unreadable — and stacking them full width makes
     one project 2,300px tall. A swipe strip keeps them legible and the page
     short, which is the trade a gallery on a phone actually faces. The partly
     clipped next figure is the affordance; nothing else announces it. */
  .gallery.is-grid,
  .work .gallery.is-grid {
    display: flex;
    grid-template-columns: none;
    overflow-x: auto;
    scroll-snap-type: x proximity;
    overscroll-behavior-x: contain;
    /* Room for the focus ring, which would otherwise be clipped by the
       scroll container's own overflow. */
    padding: var(--focus-width);
    margin: var(--space-4) calc(var(--focus-width) * -1) 0;
  }
  .gallery.is-grid > .figure {
    flex: 0 0 72%;
    scroll-snap-align: start;
  }
  .gallery.is-grid .figure img,
  .work .gallery.is-grid .figure img {
    width: 100%;
    height: auto;
    max-height: none;
  }
  .timeline { --rail: 2.75rem; --stop: 1.625rem; }
  .entry { gap: var(--space-4); }
  .year-group { padding-block: var(--space-6); }

  .hero { align-items: flex-start; }
  .hero-portrait { width: 8rem; height: 8rem; }
  .contact-row { grid-template-columns: 6.25rem 1fr; }
}
