/* ============================================================================
   COMPONENTS
   ----------------------------------------------------------------------------
   Class names describe WHAT A THING IS, never what it looks like, so
   restyling never means renaming.

   CONTENTS
     01  Button
     02  Link
     03  Label and meta
     04  Card
     05  Panel (the bone surface)
     06  Figure
     07  Hero
     08  Featured case
     09  Case study
     10  About
     11  Contact
     12  Tag
     12b Toggle
     13  Pagination
     14  Form
     15  Notice
   ========================================================================== */


/* ============================================================================
   01  BUTTON
   ----------------------------------------------------------------------------
   Three variants by emphasis: primary, secondary, ghost. One primary per view.

   No destructive variant and no loading state, this site has no destructive
   actions and nothing asynchronous, so both would be dead code. Add them the
   day something actually deletes or waits.

   Sizes hold the radius at 40% of height. A fixed radius becomes a pill the
   moment it passes half the height, which is why each size sets its own.
   ========================================================================== */

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-2);
  font-family: var(--family-sans);
  font-weight: var(--weight-medium);
  line-height: 1;
  white-space: nowrap;
  text-align: center;
  border: var(--stroke-thick) solid transparent;
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease),
              color var(--duration-fast) var(--ease),
              border-color var(--duration-fast) var(--ease);

  /* Default size, 56px tall (incl. 1.5px borders), radius 21px. */
  font-size: var(--size-400);
  padding: 19px 32px;
  border-radius: 21px;
}

/* --- Variants ------------------------------------------------------------- */

.button--primary {
  background: var(--color-accent);
  color: var(--color-on-accent);
  border-color: var(--color-accent);
}
.button--primary:hover {
  background: var(--color-accent-hover);
  border-color: var(--color-accent-hover);
  color: var(--color-on-accent);
}
.button--primary:active {
  background: var(--color-accent-active);
  border-color: var(--color-accent-active);
}

.button--secondary {
  background: transparent;
  color: var(--color-text);
  border-color: var(--color-line-interactive);
}
.button--secondary:hover {
  color: var(--color-text);
  border-color: var(--color-text);
  background: var(--color-surface);
}
.button--secondary:active { background: var(--color-raised); }

.button--ghost {
  background: transparent;
  color: var(--color-text-muted);
  border-color: transparent;
  padding-inline: var(--space-3);
}
.button--ghost:hover { color: var(--color-text); background: var(--color-surface); }

/* --- Sizes ---------------------------------------------------------------- */

.button--sm {
  font-size: var(--size-300);
  padding: 14px 23px;      /* 44px tall, exactly meets the touch minimum */
  border-radius: 16px;
}
.button--lg {
  font-size: var(--size-500);
  padding: 22px 37px;      /* 64px tall */
  border-radius: 24px;
}
.button--hero {
  font-size: 18px;
  padding: 27px 45px;      /* 75px tall */
  border-radius: 29px;
}

/* --- Modifiers ------------------------------------------------------------ */

.button--block { display: flex; width: 100%; }

.button--icon {
  padding: var(--space-3);
  aspect-ratio: 1;
  border-radius: var(--radius-full);
}

/* Disabled is its own state, not a faded copy of the resting one. Opacity
   alone left the yellow reading as an active button. This drops it to a flat
   surface with dim text, so it is obviously inert at a glance. */
.button[disabled],
.button[aria-disabled="true"] {
  background: var(--color-surface);
  color: var(--color-text-dim);
  border-color: var(--color-line);
  cursor: not-allowed;
  pointer-events: none;
}
.panel .button[disabled],
.panel .button[aria-disabled="true"] {
  background: var(--color-panel-line);
  color: var(--color-panel-dim);
  border-color: var(--color-panel-line);
}

/* --- On the bone panel ----------------------------------------------------
   The yellow fill still works on bone (14.9:1 with dark ink). Only the
   outlined and ghost variants need re-pointing at the light text scale.     */

/* Every light surface, not only .panel. A white secondary button is 1.0:1 on
   bone and simply disappears, so any component that introduces a light ground
   must be listed here. */
.panel .button--primary { color: var(--color-panel-text); }

.panel .button--secondary { color: var(--color-panel-text); border-color: var(--color-panel-dim); }
.panel .button--secondary:hover { border-color: var(--color-panel-text); background: transparent; }

.panel .button--ghost { color: var(--color-panel-muted); }
.panel .button--ghost:hover { color: var(--color-panel-text); background: transparent; }

.panel .button:focus-visible { box-shadow: var(--focus-ring-on-panel); }


/* ============================================================================
   02  LINK
   ========================================================================== */

.link-arrow {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--size-300);
  font-weight: var(--weight-medium);
}
.link-arrow::after { content: "\2192"; transition: transform var(--duration-fast) var(--ease); }
.link-arrow:hover::after { transform: translateX(3px); }

.link-quiet { color: var(--color-text-muted); }
.link-quiet:hover { color: var(--color-text); }

/* Inside a bone panel the accent is 1.1:1 and would vanish. */
.panel a { color: var(--color-accent-on-panel); }
.panel a:hover { color: var(--color-panel-text); }
.panel h1 a, .panel h2 a, .panel h3 a, .panel h4 a { color: var(--color-panel-text); }
.panel h1 a:hover, .panel h2 a:hover, .panel h3 a:hover, .panel h4 a:hover { color: var(--color-accent-on-panel); }


/* ============================================================================
   03  LABEL AND META
   ========================================================================== */

/* There is no .eyebrow component. A generic kicker sitting above a real
   heading ("Context" above "An internship at a climate impact platform") adds
   no information: it is the most recognisable tell of generated layout.
   If a section needs naming, name it in the heading.

   .meta is different: it carries real data (discipline and year), so it stays, but set in sentence case at body tracking, not as a spaced-out micro-label. */

/* .meta carries real data (discipline and year), so it survives. It sits BELOW
   the content it describes, never above a heading, where it would just be a
   kicker wearing a different name. */
.meta {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin-top: var(--space-3);
  font-size: var(--size-200);
  color: var(--color-text-muted);
}
.panel .meta { color: var(--color-panel-muted); }


/* ============================================================================
   04  CARD
   ----------------------------------------------------------------------------
   At 1.05:1 against the page a card is barely a box. That is correct for a
   dark interface: it means the card is defined by its padding, not its edge.
   ========================================================================== */

.card {
  background: var(--color-surface);
  border: var(--stroke-thin) solid var(--color-line);
  border-radius: var(--radius-lg);
  padding: var(--space-5);
  transition: background var(--duration-base) var(--ease),
              border-color var(--duration-base) var(--ease);
}
.card--interactive:hover {
  background: var(--color-raised);
  border-color: var(--color-line-strong);
}

.card__title { font-size: var(--size-600); margin-block: var(--space-2); }
.card__body  { font-size: var(--size-300); color: var(--color-text-muted); }

/* Sizes. Padding and title size move together so a large card reads as the
   same component at a bigger scale, not as a stretched small one. */
.card--sm { padding: var(--space-4); border-radius: var(--radius-md); }
.card--sm .card__title { font-size: var(--size-400); }

.card--lg { padding: var(--space-7); }
.card--lg .card__title { font-size: var(--size-700); margin-block: var(--space-3); }
.card--lg .card__body  { font-size: var(--size-400); color: var(--color-text-body); }

/* A card that leads with an image. The media sits flush to the edges, so the
   card's own padding has to come off the container and go on the body. */
.card--media { padding: 0; overflow: hidden; }
.card--media .card__media img { width: 100%; aspect-ratio: 16 / 10; object-fit: cover; display: block; }
.card--media .card__content { padding: var(--space-5); }
.card--lg.card--media .card__content { padding: var(--space-7); }


/* ============================================================================
   05  PANEL: the bone surface
   ----------------------------------------------------------------------------
   A section-level device, not a card style. Use it for one or two moments per
   page: a featured case, a framed screenshot, the bio block. Everywhere and it
   stops being an accent and becomes a light site with black gaps.

   Everything inside inverts to the light text scale automatically.
   ========================================================================== */

.panel {
  background: var(--color-panel);
  color: var(--color-panel-body);
  border-radius: var(--radius-lg);
  padding: var(--space-6);
}
.panel h1, .panel h2, .panel h3, .panel h4 { color: var(--color-panel-text); }
.panel p { color: var(--color-panel-body); }
.panel hr { border-color: var(--color-panel-line); }
.panel .text-muted { color: var(--color-panel-muted); }
.panel ::selection { background: var(--yellow-800); color: var(--color-panel); }

.panel--flush { padding: var(--space-4); }


/* ============================================================================
   06  FIGURE
   ----------------------------------------------------------------------------
   Case screenshots are light-grey interfaces. On bare black they glare; a bone
   frame settles them into the page. That is what .figure--framed is for.
   ========================================================================== */

.figure img {
  width: 100%;
  border-radius: var(--radius-md);
  background: var(--color-surface);
}

.figure--framed {
  background: var(--color-panel);
  border-radius: var(--radius-lg);
  padding: var(--space-4);
}
.figure--framed img { background: transparent; }

.figure__caption {
  margin-top: var(--space-3);
  font-size: var(--size-200);
  color: var(--color-text-muted);
}
.figure--framed .figure__caption { color: var(--color-panel-muted); }

.figure-group {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: repeat(auto-fit, minmax(min(20rem, 100%), 1fr));
}
.figure-group:has(img:only-child) { grid-template-columns: minmax(0, 1fr); }


/* ============================================================================
   07  HERO
   ========================================================================== */

.hero { padding-block: var(--space-8) var(--space-6); }
.hero__name { margin-bottom: var(--space-4); }
.hero__sub {
  font-size: var(--size-500);
  color: var(--color-text-body);
  margin-bottom: var(--space-7);
}
.hero__actions { display: flex; gap: var(--space-3); flex-wrap: wrap; }


/* ============================================================================
   07a POSTER
   ----------------------------------------------------------------------------
   The opening of the page, composed as a printed poster: one enormous
   typographic block top left, a photograph top right, the signature bottom
   right, and a great deal of nothing in the middle.

   The empty centre is the device. Filling it turns this back into an ordinary
   hero, so resist it.

   No background, border or radius. This is not a card on the page, it is the
   page opening, so it inherits the ground and the gutter.
   ========================================================================== */

.poster {
  color: var(--color-text);
  min-height: min(76vh, 42rem);

  display: grid;
  gap: var(--space-6);
  grid-template-columns: minmax(0, 1fr);
  grid-template-areas:
    "title"
    "aside"
    "actions";
  /* Stacked, the rows must not stretch to fill the hero's min-height. Left to
     stretch they spread three rows across 608px and open ~100px gaps between
     the headline, the statement and the buttons, which reads as three
     unrelated blocks rather than one hero. */
  align-content: start;
}

@media (min-width: 52rem) {
  /* Two columns. The row sizes to the headline block and the panel stretches
     to match it, rather than the panel filling the whole hero height: a block
     that tall left the statement stranded in the middle of it with a third of
     the panel empty above and below. Matched to the headline the two read as a
     pair, which is the composition. The pair then centres in the hero. */
  .poster {
    grid-template-columns: minmax(0, 1.3fr) minmax(0, 1fr);
    grid-template-areas:
      "title   aside"
      "actions .";
    column-gap: clamp(var(--space-8), 10vw, 7rem);
    align-content: center;
    /* The headline and the statement share a first line, so the grid aligns
       them on their baselines and the browser reads both out of the real font
       metrics.

       This is why the headline is a grid item rather than sitting in a wrapper
       with the buttons, which is how it was built. A block wrapper makes the
       grid synthesise a baseline for the column instead of taking the
       headline's own, and it landed 16px out.

       A fixed offset in place of this does not survive. The display face
       carries an optical size axis, so its ascent moves with the size: one
       coefficient measured at 1280px was 5px out at 900px. Only letting the
       browser read the metrics holds at every width. */
    align-items: baseline;
  }
  .poster__actions { align-self: start; }

  /* The two first lines are aligned on the top of their capital letters, not on
     their baselines. Cap height is what the eye reads as the top of a line, and
     with a 104px headline against 26px body text the two are nowhere near each
     other: aligned on baselines the statement sits visibly high.

     CSS has no cap-height alignment, so this is built in two parts.

     The grid still aligns the baselines, which is what gets the font metrics
     right for free. From there the statement rises by the difference between
     the two cap heights: on a shared baseline the taller capital reaches
     further up, so it is the small text that has to move. Those were measured off the rendered fonts
     with canvas TextMetrics at five widths: the display face is a constant
     0.6596 of its size and the body face 0.6975 of its own, so the drop is
     expressible as a proportion of each and tracks both clamps.

     The 16px is separate and is a flat residual left by the grid's own baseline
     alignment, identical at every width from 860 to 1440.

     All of it has to be a relative offset rather than padding or margin.
     Baseline alignment measures the item's own baseline and then positions the
     item to put that baseline on the line, so padding added to shift it is
     absorbed and nothing moves. A relative offset lands after layout, which is
     the only place the grid has stopped compensating. */
  .poster__aside {
    position: relative;
    top: calc(16px
              - 0.6596 * clamp(2.75rem, 9vw, 6.5rem)
              + 0.6975 * clamp(1.125rem, 2vw, 1.625rem));
  }
}

/* --- The statement --------------------------------------------------------
   The second voice in the poster: no box, no rule, no ground of its own. It
   sits on the page beside the headline and is held there by the baseline of
   its first line rather than by anything drawn around it.

   Set well above body size, and in the body colour rather than the heading
   colour. Size gives it standing next to a 104px headline; the lighter colour
   keeps it from competing with one.                                          */

.poster__aside { grid-area: aside; }

.poster__statement {
  margin: 0;
  font-size: clamp(1.125rem, 2vw, 1.625rem);
  line-height: var(--leading-snug);
  letter-spacing: var(--tracking-snug);
  color: var(--color-text-body);   /* 8.5:1 on the page */
  text-wrap: pretty;
  max-width: 28ch;
}

/* --- The headline --------------------------------------------------------- */

.poster__title {
  grid-area: title;
  margin: 0;
  font-family: var(--family-display);
  /* Regular, not bold. At 104px a display face carries on its own shapes and
     does not need weight to have presence: bold at this size reads as shouting
     rather than as confidence. */
  font-weight: var(--weight-regular);
  font-size: clamp(2.75rem, 9vw, 6.5rem);
  /* Both loosened with the weight. -0.045em was tuned for bold, where the
     heavy stems close the gaps themselves; on regular the same tracking jams
     the letters together. Same for the leading: thin strokes need more air
     between lines or the block turns into a texture. */
  line-height: 1.0;
  letter-spacing: -0.02em;
  color: var(--color-text);
  text-wrap: balance;
}
.poster__title span { display: block; }

/* --- The entrance ---------------------------------------------------------
   The three lines rise into place in sequence, then the buttons. One event,
   done in 830ms, and the page is still after it.

   Written as a keyframe on elements that already exist rather than driven from
   a script. That is not only tidier: a JS entrance has to hide the headline
   until the script says otherwise, so a script that fails to load leaves the
   front page blank. With animation-fill-mode: both there is no such state. If
   this stylesheet never arrives, the headline is simply text.

   The movement is the same fade-and-rise as process-in and details-in, so the
   entrance speaks the motion language already on the site instead of
   introducing a second one.

   The 150ms head start is deliberate. The display face loads on display=swap,
   so the first paint is the fallback and Bricolage arrives a moment later.
   Starting on frame one animates the wrong typeface and then snaps to the
   right one. 150ms is under the threshold where a delay reads as lag.        */

@keyframes poster-in {
  from { opacity: 0; transform: translateY(1rem); }
  to   { opacity: 1; transform: none; }
}

/* Scoped to no-preference rather than switched off under reduce, which is the
   safer way round for an entrance that starts at opacity 0. Written the usual
   way, the headline is hidden by default and something has to arrive to reveal
   it. Written this way the default is the finished, readable state, and only a
   browser that understands the query and reports no motion preference opts
   into starting it hidden. Anything else, including a browser that supports
   the properties but never runs the animation, shows the headline. */
@media (prefers-reduced-motion: no-preference) {
  .poster__title span,
  .poster__actions,
  .poster__aside {
    animation: poster-in var(--duration-slow) var(--ease) both;
  }
  /* 70ms apart: close enough to read as one connected movement rather than
     four separate ones. */
  .poster__title span:nth-child(1) { animation-delay: 150ms; }
  .poster__title span:nth-child(2) { animation-delay: 220ms; }
  .poster__title span:nth-child(3) { animation-delay: 290ms; }
  .poster__actions                 { animation-delay: 410ms; }
  .poster__aside                   { animation-delay: 300ms; }

  /* Set by the language switch once the hero has already arrived. See i18n.js. */
  .poster.is-settled .poster__title span,
  .poster.is-settled .poster__actions,
  .poster.is-settled .poster__aside { animation: none; }
}

/* --- Actions --------------------------------------------------------------- */

.poster__actions {
  grid-area: actions;
  display: flex;
  gap: var(--space-3);
  flex-wrap: wrap;
}


/* --- Index -----------------------------------------------------------------
   A capability list: a small mono label against a column of plain values,
   ruled off. Editorial rather than decorative, and the label carries real
   information rather than naming the obvious.
   ------------------------------------------------------------------------- */

.index {
  display: grid;
  gap: var(--space-6);
  grid-template-columns: 1fr;
}
@media (min-width: 44rem) { .index { grid-template-columns: repeat(3, 1fr); gap: var(--space-5); } }

.index__group {
  display: flex;
  flex-direction: column;
  gap: var(--space-3);
}

.index__label {
  font-family: var(--family-mono);
  font-size: var(--size-100);
  color: var(--color-text-muted);
}

.index__group ul { display: flex; flex-direction: column; gap: var(--space-2); }
.index__group li { font-size: var(--size-400); color: var(--color-text-body); }
.index__group li span { display: block; font-size: var(--size-200); color: var(--color-text-dim); }


/* ============================================================================
   07b COVER
   ----------------------------------------------------------------------------
   A project shown as image only. The title appears on hover and on keyboard
   focus, and is otherwise absent.

   Two things this has to get right that a pure hover effect would not:

   Touch devices have no hover, so under (hover: none) the label is always
   visible. Otherwise the entire front page would be unlabelled on a phone.

   The link still needs an accessible name at all times, so the title is real
   text that is faded rather than removed, and the anchor carries an
   aria-label as well.
   ========================================================================== */

.cover {
  position: relative;
  display: block;
  overflow: hidden;
  border-radius: var(--radius-lg);
  background: var(--color-panel);
  color: var(--color-panel-text);
}

.cover img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform var(--duration-slow) var(--ease);
}
.cover:hover img,
.cover:focus-visible img { transform: scale(1.02); }

/* A wash that only exists while the label does, so the photo is untouched at
   rest. Sized generously so the text sits on the darkest part of it. */
.cover::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to top,
              rgba(13, 13, 12, 0.85) 0%,
              rgba(13, 13, 12, 0.35) 35%,
              rgba(13, 13, 12, 0) 65%);
  opacity: 0;
  transition: opacity var(--duration-base) var(--ease);
  pointer-events: none;
}
.cover:hover::after,
.cover:focus-visible::after { opacity: 1; }

.cover__label {
  position: absolute;
  left: var(--space-6);
  right: var(--space-6);
  bottom: var(--space-6);
  z-index: 1;
  font-family: var(--family-display);
  font-weight: var(--weight-semi);
  font-size: var(--size-700);
  letter-spacing: var(--tracking-snug);
  line-height: var(--leading-tight);
  color: var(--color-panel-text);   /* on the scrim, see .bento-card__title */
  text-wrap: balance;
  opacity: 0;
  transform: translateY(8px);
  transition: opacity var(--duration-base) var(--ease),
              transform var(--duration-base) var(--ease);
}
.cover:hover .cover__label,
.cover:focus-visible .cover__label { opacity: 1; transform: none; }

/* No hover available: show it permanently, or the page is a wall of unlabelled
   pictures. */
@media (hover: none) {
  .cover::after { opacity: 1; }
  .cover__label { opacity: 1; transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .cover img, .cover__label { transition: opacity var(--duration-base) var(--ease); transform: none !important; }
}

/* Aspect ratios, so the covers hold their shape before the image loads. */
.cover--wide  { aspect-ratio: 16 / 9; }
.cover--tall  { aspect-ratio: 4 / 5; }
.cover--square{ aspect-ratio: 1 / 1; }

@media (max-width: 40rem) {
  .cover--wide, .cover--tall, .cover--square { aspect-ratio: 4 / 3; }
  .cover__label { font-size: var(--size-600); left: var(--space-5); right: var(--space-5); bottom: var(--space-5); }
}


/* ============================================================================
   07c FEATURE PAIR and STRIP
   ----------------------------------------------------------------------------
   The front page runs contained, then breaks out once. Two devices:

   .feature-pair  two covers side by side, weighted rather than equal, so the
                  lead project reads as the lead.

   .strip         a full-bleed row that runs off both edges of the viewport and
                  scrolls sideways. The overhang is the affordance: seeing a
                  card cut off at the edge is what tells you there is more.
   ========================================================================== */

.feature-pair { display: grid; gap: var(--space-4); }
@media (min-width: 52rem) {
  .feature-pair { grid-template-columns: 1.45fr 1fr; }
}

.strip {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: min(78vw, 30rem);
  gap: var(--space-4);
  overflow-x: auto;
  overscroll-behavior-x: contain;
  scroll-snap-type: x proximity;
  padding-inline: var(--gutter);
  padding-block: var(--space-1);
  /* The scrollbar is noise on a page like this; the cut-off card is the cue. */
  scrollbar-width: none;
}
.strip::-webkit-scrollbar { display: none; }
.strip > * { scroll-snap-align: start; }

/* Line the first card up with the contained text above it, so the break-out
   reads as deliberate rather than as a misalignment. */
@media (min-width: 78rem) {
  .strip { padding-inline: max(var(--gutter), calc((100% - var(--width-max)) / 2 + var(--gutter))); }
}


/* ============================================================================
   07d BENTO GRID
   ----------------------------------------------------------------------------
   Adapted from Magic UI's bento grid. The interaction is theirs: cells of
   different spans, the text block lifting on hover to make room for a call to
   action that slides up underneath it.

   Rebuilt in plain CSS. Departures from the original, all deliberate:

   No per-card icon. The original puts a generic glyph above each title, which
   on a portfolio would be decoration standing in for the thing the card is
   actually about. The project image does that job.

   The whole cell is the link, not just the button at the bottom. A card whose
   only target is a small button at the end is a worse click target and reads
   as a widget rather than a project.

   Its light and dark shadow stacks are gone. Shadows do not register on
   #0D0D0C, so elevation is carried by border and surface, as everywhere else.
   ========================================================================== */

.bento {
  display: grid;
  gap: var(--space-4);
  grid-template-columns: 1fr;
  grid-auto-rows: 16rem;
}
@media (min-width: 40rem) {
  .bento { grid-template-columns: repeat(2, 1fr); grid-auto-rows: 18rem; }
}
@media (min-width: 64rem) {
  .bento { grid-template-columns: repeat(3, 1fr); grid-auto-rows: 20rem; }
}

.bento-card {
  position: relative;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  overflow: hidden;
  border-radius: var(--radius-lg);
  border: var(--stroke-thin) solid var(--color-line);
  /* Dark, not the page surface. These cards are image-led with light text on a
     dark scrim, so a light placeholder would flash white and would leave a
     white card behind any image that fails to load. */
  background: var(--color-panel);
  color: var(--color-panel-text);
  isolation: isolate;
}

/* Spans. Only applied once there is more than one column to span. */
@media (min-width: 40rem) {
  .bento-card--wide { grid-column: span 2; }
}
@media (min-width: 64rem) {
  .bento-card--lead { grid-column: span 2; grid-row: span 2; }
  .bento-card--tall { grid-row: span 2; }
}

.bento-card__media { position: absolute; inset: 0; z-index: -2; }

.bento-card__media img {
  width: auto;
  height: auto;
  max-width: 100%;
  max-height: 100%;
  object-fit: contain;
  border-radius: var(--radius-sm);
}
.bento-card__media img {
  width: 100%; height: 100%;
  object-fit: cover;
  display: block;
  transition: transform var(--duration-slow) var(--ease);
}
.bento-card:hover .bento-card__media img,
.bento-card:focus-visible .bento-card__media img { transform: scale(1.03); }

/* A scrim, so the title is legible over any image. Heavier at the foot where
   the text sits, and it deepens on hover as the card comes forward. */
/* The scrim only exists to carry the caption, so it arrives with it. At rest
   the card is the picture and nothing else. */
.bento-card::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(to top,
              rgba(13, 13, 12, 0.95) 0%,
              rgba(13, 13, 12, 0.7) 45%,
              rgba(13, 13, 12, 0.35) 85%);
  opacity: 0;
  transition: opacity var(--duration-base) var(--ease);
}
.bento-card:hover::before,
.bento-card:focus-visible::before { opacity: 1; }

/* Hidden with opacity, not with display or visibility. The title is the link's
   accessible name, so it has to stay in the accessibility tree whether or not
   a pointer is hovering it: a card that announces nothing is worse than one
   that shows nothing. */
.bento-card__body {
  padding: var(--space-5);
  display: flex;
  flex-direction: column;
  gap: var(--space-2);
  opacity: 0;
  transform: translateY(0.75rem);
  transition: opacity var(--duration-base) var(--ease),
              transform var(--duration-base) var(--ease);
}
.bento-card:hover .bento-card__body,
.bento-card:focus-visible .bento-card__body {
  opacity: 1;
  transform: translateY(-2.75rem);
}

.bento-card__title {
  font-size: var(--size-600);
  line-height: var(--leading-tight);
  /* Pinned light: this sits on the dark scrim over the image, not on the page,
     so it must not follow the page text token. */
  color: var(--color-panel-text);
}
.bento-card--lead .bento-card__title { font-size: var(--size-700); }

.bento-card__desc {
  font-size: var(--size-300);
  line-height: var(--leading-snug);
  color: var(--color-panel-body);   /* on the scrim, see above */
  max-width: 34ch;
}

/* The call to action lives underneath and slides up into the space the body
   vacates. Hidden from assistive tech, since the whole cell is already a link
   with a full accessible name. */
.bento-card__cta {
  position: absolute;
  left: var(--space-5);
  bottom: var(--space-5);
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  font-size: var(--size-300);
  font-weight: var(--weight-medium);
  color: var(--color-accent);       /* 15.6:1 on the scrim */
  opacity: 0;
  transform: translateY(2.75rem);
  transition: opacity var(--duration-base) var(--ease),
              transform var(--duration-base) var(--ease);
}
.bento-card__cta::after { content: "\2192"; }
.bento-card:hover .bento-card__cta,
.bento-card:focus-visible .bento-card__cta { opacity: 1; transform: none; }

/* No hover means the reveal never happens, and a grid of six unlabelled
   pictures is not a list of projects. On touch the caption is simply there. */
@media (hover: none) {
  .bento-card::before { opacity: 1; }
  .bento-card__body { opacity: 1; transform: none; }
  .bento-card__cta { position: static; opacity: 1; transform: none; padding: 0 var(--space-5) var(--space-5); }
  .bento-card:hover .bento-card__body { transform: none; }
}

@media (prefers-reduced-motion: reduce) {
  .bento-card__body, .bento-card__cta, .bento-card__media img { transition: opacity var(--duration-base) var(--ease); }
  .bento-card__body { transform: none; }
  .bento-card:hover .bento-card__body,
  .bento-card:focus-visible .bento-card__body { transform: none; }
  .bento-card:hover .bento-card__media img { transform: none; }
  .bento-card__cta { transform: none; }
}

/* ============================================================================
   07e PROCESS
   ----------------------------------------------------------------------------
   A process navigator: a numbered list of stages on the left, the content for
   the active stage on the right.

   Built as the ARIA tabs pattern rather than a bespoke widget, because that is
   exactly what this is: one list selecting one of several panels. That buys
   arrow-key navigation, correct announcement, and a shape screen reader users
   already know, none of which a div-based version would have.

   Hover activates on pointer devices, as asked. Keyboard uses the tab pattern
   proper. Touch taps. No interaction depends on hover alone.

   Content lives in the markup, not in this file. Each case study writes its
   own stages, so the component carries no copy.
   ========================================================================== */

.process {
  /* One value for both headings. They are a pair that has to read across, so
     the size lives here rather than being written twice and drifting apart. */
  --process-heading-size: clamp(2.25rem, 4.5vw, 3rem);
  display: grid;
  gap: var(--space-6);
  grid-template-columns: minmax(0, 1fr);
}
@media (min-width: 52rem) {
  .process {
    grid-template-columns: minmax(17rem, 28%) minmax(0, 1fr);
    gap: clamp(var(--space-6), 5vw, var(--space-9));
    align-items: start;
  }

  /* The two first lines are set to sit on the same line, and neither the grid's
     own baseline alignment nor plain top alignment gets there: the headline is
     104px on a 1.0 leading in the display face and the statement is 26px on 1.4
     in the body face, so their first baselines fall in completely different
     places inside their own line boxes. Grid baseline alignment lands 16px out
     because it synthesises the left column's baseline from the wrapper rather
     than from the text.

     The offset is a fraction of the headline's own size, written with the same
     clamp, so it tracks the headline at every width instead of being right at
     one and drifting at the rest. It belongs in here: stacked, there is no
     headline beside it to line up with, and the padding would only be a gap. */
  .poster__aside { padding-top: calc(0.72 * clamp(2.75rem, 9vw, 6.5rem)); }
}

/* --- The stage list ------------------------------------------------------- */

/* The section heading lives inside the left column rather than above the whole
   block. That puts it and the panel heading at the top of their own columns,
   so the two read across on one line. */
.process__aside { display: flex; flex-direction: column; gap: var(--space-5); }

.process__label {
  font-family: var(--family-display);
  font-weight: var(--weight-semi);
  font-size: var(--process-heading-size);
  letter-spacing: var(--tracking-snug);
  line-height: var(--leading-tight);
  color: var(--color-text);
  margin: 0;
}

.process__nav {
  display: flex;
  flex-direction: column;
}
@media (min-width: 52rem) {
  /* Stays in reach once the panel is taller than the viewport, so a stage is
     always one click away. Offset clears the fixed header. */
  .process__aside {
    position: sticky;
    top: calc(var(--control-height) + var(--space-8));
  }
}
@media (max-width: 52rem) {
  .process__nav { flex-direction: row; flex-wrap: wrap; gap: var(--space-2); }
}

.process__step {
  position: relative;
  display: flex;
  align-items: baseline;
  gap: var(--space-3);
  width: 100%;
  padding: var(--space-4) 0 var(--space-4) var(--space-4);
  background: none;
  border: 0;
  border-top: var(--stroke-thin) solid var(--color-line);
  font-family: var(--family-sans);
  text-align: left;
  cursor: pointer;
  color: var(--color-text-muted);
  transition: color var(--duration-fast) var(--ease),
              padding-left var(--duration-base) var(--ease);
}
.process__step:last-child { border-bottom: var(--stroke-thin) solid var(--color-line); }
@media (max-width: 52rem) {
  .process__step { width: auto; border: 0; padding: var(--space-2) 0; }
  .process__step:last-child { border-bottom: 0; }
}

/* The rule that marks the active stage. Grows from nothing rather than
   appearing, so the eye follows it down the list. */
.process__step::before {
  content: "";
  position: absolute;
  left: 0;
  top: 50%;
  width: 2px;
  height: 0;
  background: var(--color-text);
  transform: translateY(-50%);
  transition: height var(--duration-base) var(--ease);
}
@media (max-width: 52rem) { .process__step::before { display: none; } }

.process__num {
  font-family: var(--family-mono);
  font-size: var(--size-100);
  color: var(--color-text-dim);
  font-variant-numeric: tabular-nums;
  transition: color var(--duration-fast) var(--ease);
}

.process__title {
  font-family: var(--family-display);
  font-weight: var(--weight-semi);
  font-size: var(--size-800);
  letter-spacing: var(--tracking-snug);
  line-height: 1.1;
  transition: color var(--duration-fast) var(--ease);
}

.process__step:hover { color: var(--color-text); }
.process__step:focus-visible { box-shadow: var(--focus-ring); border-radius: var(--radius-sm); }

/* Active. Emphasis comes from contrast and the rule, not from colour: a
   coloured active state would spend the accent on navigation. */
.process__step[aria-selected="true"] { color: var(--color-text); padding-left: var(--space-5); }
.process__step[aria-selected="true"]::before { height: calc(100% - var(--space-6)); }
.process__step[aria-selected="true"] .process__num { color: var(--color-text); }

/* --- The panel ------------------------------------------------------------ */

.process__panels { position: relative; min-width: 0; }

.process__panel { display: flex; flex-direction: column; gap: var(--space-4); }
.process__panel[hidden] { display: none; }

/* The entrance. Runs once per change, keyed off a class the script adds, so
   nothing loops. */
.process__panel.is-entering { animation: process-in var(--duration-base) var(--ease) both; }

@keyframes process-in {
  from { opacity: 0; transform: translateY(8px); }
  to   { opacity: 1; transform: none; }
}

.process__figure { margin: 0; }
.process__figure img {
  width: 100%;
  aspect-ratio: 16 / 10;
  object-fit: cover;
  border-radius: var(--radius-md);
  background: var(--color-surface);
  display: block;
}

.process__heading {
  font-family: var(--family-display);
  font-weight: var(--weight-semi);
  font-size: var(--process-heading-size);   /* matches .process__label, see below */
  letter-spacing: var(--tracking-snug);
  line-height: var(--leading-tight);
  color: var(--color-text);
  margin: 0;
}
.process__body { font-size: var(--size-500); color: var(--color-text-body); max-width: var(--width-text); margin: 0; }

/* The one line each case page already had describing its stage. It was a
   heading before the stages existed; here it opens the stage rather than
   titling it, so it is set as text, not as another heading fighting the one
   above it. */
.process__lead {
  font-size: var(--size-500);
  font-weight: var(--weight-medium);
  color: var(--color-text);
  max-width: var(--width-text);
  margin: 0;
}

/* The panel heading is deliberately the same size as the section heading it
   sits beside. They are meant to read across as a pair, and any size
   difference puts their first lines on different heights. It also has to be
   the same on every stage, or the alignment would shift as you click through. */

@media (prefers-reduced-motion: reduce) {
  .process__step,
  .process__step::before,
  .process__num,
  .process__title { transition: none; }
  .process__panel.is-entering { animation: none; }
}


/* --- Slideshow -------------------------------------------------------------
   Sits inside a process panel, under the text. One image at a time with arrow
   controls.

   Announced through a live region carrying the position, so a screen reader
   user knows the view changed and where they are. The arrows disable at the
   ends rather than wrapping: wrapping in a short set makes it impossible to
   tell you have seen everything.

   With a single slide the controls hide themselves. Drop more images in and
   they appear.
   ------------------------------------------------------------------------- */

.slideshow { display: flex; flex-direction: column; gap: var(--space-3); }

.slideshow__stage { position: relative; }

.slideshow__slide { display: none; margin: 0; }
.slideshow__slide.is-active { display: block; }
.slideshow__slide.is-active img { animation: process-in var(--duration-base) var(--ease) both; }

/* Natural proportions, not a fixed frame. A 16/10 box with object-fit: cover
   was cropping the top and bottom off screenshots, which is the opposite of
   what a case study image is for. Height follows the picture instead, and the
   width and height attributes in the markup reserve the space so nothing
   jumps while it loads. */
.slideshow__slide img {
  width: auto;
  max-width: 100%;
  height: auto;
  /* Several of these cases are portrait phone screenshots. At the column's
     full width a 440x1467 shot renders nearly 2400px tall and swallows the
     page, so height is capped and the width follows from it. Nothing is
     cropped or letterboxed, the picture is just smaller, and the viewer is
     one click away for the full size. */
  max-height: 40rem;
  margin-inline: auto;
  border-radius: var(--radius-md);
  /* A hairline, so a screenshot with white edges stops bleeding into the page.
     Line-strong rather than line: the default at 1.4:1 against the page is too
     faint to do the job here, this one reads at 1.8:1 and still stays quiet. */
  border: var(--stroke-thin) solid var(--color-line-strong);
  background: var(--color-surface);
  display: block;
}

/* Covers the stage, which holds nothing but the picture. Transparent until
   hovered or focused, when the corner icon comes up: the image itself is the
   affordance, the icon only confirms it. */
.slideshow__zoom {
  position: absolute;
  inset: 0;
  width: 100%;
  padding: 0;
  background: transparent;
  border: 0;
  border-radius: var(--radius-md);
  cursor: zoom-in;
}
.slideshow__zoom:focus-visible { outline: none; box-shadow: var(--focus-ring); }

.slideshow__zoom-icon {
  position: absolute;
  right: var(--space-3);
  bottom: var(--space-3);
  display: grid;
  place-items: center;
  width: var(--control-height);
  height: var(--control-height);
  border-radius: var(--radius-full);
  background: var(--color-panel);
  color: var(--color-panel-text);
  opacity: 0;
  transform: scale(0.9);
  transition: opacity var(--duration-fast) var(--ease),
              transform var(--duration-fast) var(--ease);
}
.slideshow__zoom:hover .slideshow__zoom-icon,
.slideshow__zoom:focus-visible .slideshow__zoom-icon { opacity: 1; transform: none; }

/* The caption is below the stage, so it must clear the button covering it. */
.slideshow__caption {
  margin: var(--space-3) 0 0;
  font-size: var(--size-200);
  color: var(--color-text-muted);
}

.slideshow__controls {
  display: flex;
  align-items: center;
  gap: var(--space-3);
}
.slideshow[data-single] .slideshow__controls { display: none; }

.slideshow__arrow {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: var(--control-height);
  height: var(--control-height);
  padding: 0;
  background: transparent;
  border: var(--stroke-thin) solid var(--color-line-interactive);
  border-radius: var(--radius-full);
  color: var(--color-text);
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease),
              border-color var(--duration-fast) var(--ease);
}
.slideshow__arrow:hover:not(:disabled) { background: var(--color-surface); border-color: var(--color-text); }
.slideshow__arrow:disabled { opacity: 0.35; cursor: not-allowed; }
.slideshow__arrow:focus-visible { box-shadow: var(--focus-ring); }

.slideshow__count {
  font-family: var(--family-mono);
  font-size: var(--size-200);
  color: var(--color-text-muted);
  font-variant-numeric: tabular-nums;
}

@media (prefers-reduced-motion: reduce) {
  .slideshow__slide.is-active img { animation: none; }
  .slideshow__zoom-icon { transition: none; }
}


/* --- Lightbox -------------------------------------------------------------
   The image at the size the screen allows, on a ground dark enough that the
   picture is the only thing left to look at.

   The controls are the same shape as the inline ones, scaled up, so stepping
   through the set feels like the same set rather than a different widget. They
   sit outside the picture on a wide screen and under it on a narrow one, where
   there is no room beside it and they would otherwise cover the image.
   ------------------------------------------------------------------------- */

.lightbox {
  position: fixed;
  inset: 0;
  z-index: calc(var(--layer-overlay) + 2);
  display: grid;
  grid-template-columns: auto minmax(0, 1fr) auto;
  grid-template-rows: minmax(0, 1fr);
  align-items: center;
  justify-items: center;
  gap: var(--space-4);
  padding: clamp(var(--space-5), 5vw, var(--space-9));
  background: rgba(13, 13, 12, 0.94);
  opacity: 0;
  transition: opacity var(--duration-base) var(--ease);
}
.lightbox.is-open { opacity: 1; }

/* The UA stylesheet's [hidden] { display: none } loses to any author rule, and
   `display: grid` above is one. Without this the closed viewer stays laid out:
   invisible at zero opacity, but still fixed across the whole viewport, still
   swallowing every click and scroll on the page behind it. Nothing looked
   wrong, the page just stopped responding until a reload. */
.lightbox[hidden] { display: none; }

.lightbox__figure {
  grid-column: 2;
  grid-row: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-3);
  margin: 0;
  /* A definite height, so the picture's own max-height: 100% has something to
     resolve against. Left to shrink-wrap its content the figure is as tall as
     the image, the cap means nothing, and a tall portrait screenshot runs off
     both ends of the screen. */
  height: 100%;
  min-height: 0;
  max-width: 100%;
}
.lightbox__figure img {
  flex: 0 1 auto;
  border: var(--stroke-thin) solid var(--color-panel-muted);
  max-width: 100%;
  max-height: 100%;
  min-height: 0;
  width: auto;
  height: auto;
  object-fit: contain;
  border-radius: var(--radius-md);
  background: var(--color-surface);
}
/* Caption and counter share one line under the picture.

   They used to be two blocks, the caption inside the figure and the counter in
   a row of its own beneath it. Centring the lot then pushed the picture well
   above the middle of the screen, since everything that balanced it sat on one
   side. One short line costs half as much, and the picture sits where the eye
   expects it. */
.lightbox__meta {
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: var(--space-4);
  flex-wrap: wrap;
  font-size: var(--size-300);
  color: var(--color-panel-muted);   /* 8.3:1 on the ground */
  text-align: center;
}
.lightbox__caption { max-width: 60ch; }

.lightbox__arrow,
.lightbox__close {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.75rem;                    /* 44px, WCAG 2.5.5 target size */
  height: 2.75rem;
  padding: 0;
  background: transparent;
  border: var(--stroke-thin) solid var(--color-panel-muted);
  border-radius: var(--radius-full);
  color: var(--color-panel-text);
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease),
              border-color var(--duration-fast) var(--ease);
}
.lightbox__arrow:hover:not(:disabled),
.lightbox__close:hover {
  background: var(--color-panel-text);
  border-color: var(--color-panel-text);
  color: var(--color-panel);
}
.lightbox__arrow:disabled { opacity: 0.3; cursor: not-allowed; }
/* The page's focus ring is a dark outer band, which would vanish here. On the
   viewer's ground the ring inverts, same idea, opposite values. */
.lightbox__arrow:focus-visible,
.lightbox__close:focus-visible {
  outline: none;
  box-shadow: 0 0 0 2px var(--color-panel), 0 0 0 4px var(--color-panel-text);
}

.lightbox__arrow--prev { grid-column: 1; grid-row: 1; }
.lightbox__arrow--next { grid-column: 3; grid-row: 1; }
.lightbox__close { position: absolute; top: var(--space-5); right: var(--space-5); }

.lightbox__count {
  font-family: var(--family-mono);
  font-size: var(--size-200);
  color: var(--color-panel-muted);
  font-variant-numeric: tabular-nums;
}

/* Narrow: no room for arrows beside the picture, so they go under it. */
@media (max-width: 40rem) {
  .lightbox { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(0, 1fr) auto; }
  .lightbox__figure { grid-column: 1; }
  .lightbox__arrow--prev,
  .lightbox__arrow--next { grid-column: 1; grid-row: 2; }
  .lightbox__arrow--prev { justify-self: start; }
  .lightbox__arrow--next { justify-self: end; }
}

/* Scroll is locked behind the viewer, the same as behind the menu. */
body.lightbox-is-open { overflow: hidden; }

@media (prefers-reduced-motion: reduce) {
  .lightbox { transition: none; }
}


/* ============================================================================
   08  FEATURED CASE
   ========================================================================== */

.featured { display: grid; gap: var(--section-gap); }

.featured__item { display: grid; gap: var(--space-6); align-items: start; }
@media (min-width: 60rem) {
  .featured__item { grid-template-columns: 1fr 1fr; }
  .featured__item:nth-child(even) .featured__media { order: -1; }
}

.featured__title { font-size: var(--size-700); margin-block: var(--space-3); }
.featured__body  { color: var(--color-text-body); margin-bottom: var(--space-5); }
.featured__media img { width: 100%; border-radius: var(--radius-md); }


/* ============================================================================
   09  WORK LIST
   ----------------------------------------------------------------------------
   One project per row on a dark full-bleed ground: the name set large on the
   left with its category and year under it, the cover filling the right.

   The dark band is the point. Every cover here is a light screenshot, and on
   the bone page they ran together into one pale field with no edges. Against
   near-black each becomes a discrete object, which is what makes a list this
   sparse readable at all.

   The whole row is the link. A row whose only target is the title is a worse
   click target, and it makes the cover beside it read as decoration rather
   than as part of the same thing.
   ========================================================================== */

.work-list {
  background: var(--color-panel);
  color: var(--color-panel-text);
  padding-block: clamp(var(--space-8), 9vw, var(--space-10));
}
.work-list__inner {
  max-width: var(--width-max);
  margin-inline: auto;
  padding-inline: var(--gutter);
  display: flex;
  flex-direction: column;
  gap: clamp(var(--space-8), 8vw, var(--space-10));
}

/* Nothing draws the boundary between one project and the next: not a rule, not
   a frame. Space does it, which is why the gap below is large and not
   decorative. It also puts every name back on the same left edge as the page
   title above, which a padded box was pushing off. */
.work-row {
  display: grid;
  gap: var(--space-6);
  align-items: center;
  color: inherit;
  text-decoration: none;
}
@media (min-width: 56rem) {
  /* A third to the name, two thirds to the picture. Narrower than that and the
     longer project names break into four and five lines. */
  .work-row {
    grid-template-columns: minmax(0, 4fr) minmax(0, 8fr);
    gap: clamp(var(--space-6), 5vw, var(--space-9));
  }
}
.work-row:focus-visible {
  outline: none;
  box-shadow: 0 0 0 3px var(--color-panel), 0 0 0 5px var(--color-panel-text);
}

.work-row__title {
  font-family: var(--family-display);
  font-weight: var(--weight-bold);
  font-size: clamp(2rem, 3.6vw, 3rem);
  letter-spacing: var(--tracking-tight);
  line-height: var(--leading-tight);
  color: var(--color-panel-text);     /* 17.8:1 */
  margin: 0;
  text-wrap: balance;
}

.work-row__kind {
  display: block;
  margin-top: var(--space-6);
  font-size: var(--size-200);
  font-weight: var(--weight-semi);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--color-panel-text);
}
.work-row__year {
  display: block;
  margin-top: var(--space-2);
  font-size: var(--size-300);
  color: var(--color-panel-muted);    /* 8.3:1 */
}
/* The rest of the project's name, under the short form set large above it.
   Splitting them is what lets the name be big: run whole, "Emanuel Vigeland
   Museum: Website Redesign" takes five lines in this column and the reference's
   composition falls apart. */
.work-row__sub {
  margin: var(--space-3) 0 0;
  font-size: var(--size-400);
  line-height: var(--leading-snug);
  color: var(--color-panel-body);    /* 13.6:1 */
  max-width: 24ch;
}

.work-row__media {
  overflow: hidden;
  aspect-ratio: 16 / 10;
  background: var(--bone-100);
  border-radius: var(--radius-sm);
}
.work-row__media img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  display: block;
  transition: transform var(--duration-slow) var(--ease);
}
.work-row:hover .work-row__media img,
.work-row:focus-visible .work-row__media img { transform: scale(1.03); }

@media (prefers-reduced-motion: reduce) {
  .work-row:hover .work-row__media img { transform: none; }
}

/* Icon only. The label lives in aria-label, so the control still announces
   itself; a bare arrow with no accessible name would be a dead end for anyone
   not looking at it. */
.case-back {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 3.5rem;
  height: 3.5rem;
  margin-left: calc(var(--space-4) * -1);
  border-radius: var(--radius-full);
  color: var(--color-text);
  transition: color var(--duration-fast) var(--ease),
              background var(--duration-fast) var(--ease),
              transform var(--duration-fast) var(--ease);
}
.case-back:hover { color: var(--color-text); background: var(--color-surface); transform: translateX(-2px); }

.case-header { max-width: var(--width-narrow); }
.case-title  { margin-block: var(--space-4); }
.case-intro  { font-size: var(--size-500); color: var(--color-text-body); }

/* Framed the way the capability index on the front page is: each fact gets its
   own short rule rather than the set being boxed in by two long ones. Boxing a
   four-item row in rules makes it read as a table; ruling each column makes it
   read as an index, which is what it is.

   The rules also line the columns up with the process grid further down, so
   the page reads on one system rather than two. */
/* --- Details --------------------------------------------------------------
   Role, duration, year and tools, folded away behind a control.

   Built on <details>, not on a button plus a div. The element is a disclosure
   already: it opens on click and stays open, it is keyboard operable, it
   announces its own state, and it works with scripting off. Rebuilding that by
   hand would only be a way to get it slightly wrong.                        */

/* Tight to the intro. The page's section gap is meant for whole sections, and
   this is a control belonging to the text above it: title, intro and the way
   into the facts read as one block. */
.case-details {
  max-width: var(--width-narrow);
  margin-top: var(--space-4);
}

/* The space saved above goes here instead. Closing up the header block only
   helps if the gap to the work itself grows to match, otherwise the page just
   gets tighter everywhere. */
.case-details + * { margin-top: calc(var(--section-gap) + var(--space-8)); }

.case-details > summary {
  display: inline-flex;
  align-items: center;
  gap: var(--space-2);
  min-height: 2.75rem;   /* 44px, WCAG 2.5.5 target size */
  padding: var(--space-2) var(--space-4);
  border: var(--stroke-thin) solid var(--color-line-interactive);
  border-radius: var(--radius-full);
  font-family: var(--family-mono);
  font-size: var(--size-200);
  letter-spacing: var(--tracking-wide);
  text-transform: uppercase;
  color: var(--color-text);
  cursor: pointer;
  list-style: none;              /* the default triangle, replaced below */
  transition: background var(--duration-fast) var(--ease),
              border-color var(--duration-fast) var(--ease);
}
.case-details > summary::-webkit-details-marker { display: none; }
.case-details > summary:hover { background: var(--color-surface); }
.case-details > summary:focus-visible { outline: none; box-shadow: var(--focus-ring); }

/* The chevron is the state, so it turns rather than swapping glyph. */
.case-details > summary::after {
  content: "";
  width: 0.5rem;
  height: 0.5rem;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
  transition: transform var(--duration-base) var(--ease);
}
.case-details[open] > summary::after { transform: translateY(1px) rotate(225deg); }

.case-overview {
  display: grid;
  gap: var(--space-5);
  margin-top: var(--space-5);
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
}
.case-details[open] .case-overview { animation: details-in var(--duration-base) var(--ease) both; }
@keyframes details-in {
  from { opacity: 0; transform: translateY(-0.375rem); }
  to   { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .case-details[open] .case-overview { animation: none; }
  .case-details > summary::after { transition: none; }
}
/* No rules. Once the whole block is behind a disclosure it is already a
   bounded group, and a rule over each column only divides it again. */
.case-overview > div { padding-top: var(--space-1); }
.case-overview dt {
  font-family: var(--family-mono);
  font-size: var(--size-100);
  color: var(--color-text-muted);
  margin-bottom: var(--space-2);
}
.case-overview dd { font-size: var(--size-400); color: var(--color-text); }

.case-section { max-width: var(--width-narrow); }
.case-section__title { font-size: var(--size-700); margin-bottom: var(--space-4); }
.case-section p { color: var(--color-text-body); }
.case-section p + p { margin-top: var(--space-4); }


/* ============================================================================
   10  ABOUT
   ========================================================================== */

.about { display: grid; gap: var(--space-8); }
@media (min-width: 60rem) {
  .about { grid-template-columns: 20rem 1fr; align-items: start; }
  .about__photo { position: sticky; top: 7rem; }
}
.about__photo img { border-radius: var(--radius-lg); width: 100%; }
.about__text { display: grid; gap: var(--space-5); }
.about__lead { font-size: var(--size-500); color: var(--color-text); }

.skills { display: grid; gap: var(--space-6); margin-top: var(--space-8); }
@media (min-width: 40rem) { .skills { grid-template-columns: repeat(3, 1fr); } }
.skills__heading {
  font-size: var(--size-400);
  font-weight: var(--weight-medium);
  color: var(--color-text);
  margin-bottom: var(--space-3);
}
.skills li + li { margin-top: var(--space-2); }
.skills--education li { margin-bottom: var(--space-4); }
.skills--education span { display: block; }
.skills--education span + span { font-size: var(--size-300); color: var(--color-text-muted); }


/* ============================================================================
   11  CONTACT
   ========================================================================== */

/* --- Contact card ---------------------------------------------------------
   The whole contact page as one dark card: the invitation and the ways to
   reach him on the left, the form on the right.

   It runs on .panel, the system's contrasting surface, rather than on a ground
   of its own. That is what the panel is for, and the design-system page says so
   in as many words: one or two moments per page. A contact page is exactly one
   such moment, so this is the panel used as intended rather than a new device.

   The crosses at the corners are the only decoration. They mark the card's
   extent the way registration marks do on print, which is why they sit outside
   the border rather than inside it. Hidden from assistive tech: they carry no
   meaning, and announcing four plus signs before the heading would be noise.
   ------------------------------------------------------------------------- */

.contact-card {
  position: relative;
  display: grid;
  gap: 0;
  border-radius: var(--radius-lg);
  background: var(--color-panel);
  color: var(--color-panel-body);
}
@media (min-width: 56rem) {
  .contact-card { grid-template-columns: minmax(0, 1.6fr) minmax(0, 1fr); }
}

/* Placed clear of the card rather than centred on its corners. Centred, half
   of each cross lies on near-black and half on the olive page, and no single
   colour reads on both: page ink is 1.1:1 on the card, card ink is 1.1:1 on the
   page. Outside the corner the mark sits on one ground and reads at 6:1. */
.contact-card__cross {
  position: absolute;
  width: 0.75rem;
  height: 0.75rem;
  color: var(--color-text-muted);
  pointer-events: none;
}
.contact-card__cross::before,
.contact-card__cross::after {
  content: "";
  position: absolute;
  background: currentColor;
}
/* Both arms need two opposing offsets to have any extent. With one side left
   as auto and no explicit size, the box computes to zero and only the arm that
   happened to be sized shows up. */
.contact-card__cross::before {
  top: 50%; left: 0; right: 0; height: 1px; transform: translateY(-50%);
}
.contact-card__cross::after {
  left: 50%; top: 0; bottom: 0; width: 1px; transform: translateX(-50%);
}
.contact-card__cross:nth-of-type(1) { top: -1.5rem; left: -1.5rem; }
.contact-card__cross:nth-of-type(2) { top: -1.5rem; right: -1.5rem; }
.contact-card__cross:nth-of-type(3) { bottom: -1.5rem; left: -1.5rem; }
.contact-card__cross:nth-of-type(4) { bottom: -1.5rem; right: -1.5rem; }

.contact-card__intro {
  display: flex;
  flex-direction: column;
  gap: var(--space-5);
  padding: clamp(var(--space-6), 5vw, var(--space-9));
}
.contact-card__title {
  margin: 0;
  font-family: var(--family-display);
  font-weight: var(--weight-regular);
  font-size: clamp(2.25rem, 5vw, 3.5rem);
  line-height: var(--leading-tight);
  letter-spacing: var(--tracking-tight);
  color: var(--color-panel-text);    /* 17.8:1 */
}
.contact-card__lead {
  margin: 0;
  font-size: var(--size-500);
  line-height: var(--leading-snug);
  color: var(--color-panel-body);    /* 11.3:1 */
  max-width: 46ch;
}

/* Ways to reach him. A list, because that is what it is, and screen readers
   announce the count before reading it. */
.contact-card__ways {
  display: grid;
  gap: var(--space-4);
  margin: var(--space-3) 0 0;
  padding: 0;
  list-style: none;
}

.contact-way {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  color: inherit;
  text-decoration: none;
  border-radius: var(--radius-md);
}
.contact-way:focus-visible { outline: none; box-shadow: var(--focus-ring-on-panel); }

.contact-way__icon {
  flex: none;
  display: grid;
  place-items: center;
  width: 2.75rem;                    /* 44px, WCAG 2.5.5 target size */
  height: 2.75rem;
  border-radius: var(--radius-md);
  background: var(--color-panel-wash);
  color: var(--color-panel-text);
  transition: background var(--duration-fast) var(--ease);
}
.contact-way:hover .contact-way__icon { background: var(--color-accent); color: var(--color-on-accent); }

.contact-way__label { display: block; font-weight: var(--weight-medium); color: var(--color-panel-text); }
.contact-way__value { display: block; font-size: var(--size-300); color: var(--color-panel-muted); }  /* 7.0:1 */

/* The form side. A different ground would be invisible here: neutral-900 is
   1.05:1 against the panel. The rule between the two columns does the
   separating instead, and on a narrow screen it moves to the top edge. */
/* No rule between the two halves. They are one card, and a line down the
   middle splits it into two things that then have to be related again. The
   padding on both sides is what keeps them apart. */
.contact-card__form {
  display: flex;
  align-items: center;
  padding: clamp(var(--space-6), 5vw, var(--space-8));
}
.contact-card__form form { width: 100%; display: grid; gap: var(--space-4); }

/* Fields on the panel. The system's .input is built for the page, so it comes
   in light with dark text; on this ground it would be four white slabs. */
.contact-card .field__label { color: var(--color-panel-text); }
.contact-card .input,
.contact-card .textarea {
  background: var(--neutral-900);
  color: var(--color-panel-text);            /* 16.9:1 on that ground */
  /* neutral-400, not the panel frame: this is a control, so WCAG 1.4.11 wants
     3:1 for its boundary and the frame colour only reaches 2.3:1. */
  border-color: var(--color-panel-dim);      /* 4.6:1 on the panel */
}
.contact-card .input::placeholder,
.contact-card .textarea::placeholder { color: var(--color-panel-muted); }
.contact-card .input:hover,
.contact-card .textarea:hover { border-color: var(--color-panel-text); }
.contact-card .input:focus-visible,
.contact-card .textarea:focus-visible { outline: none; box-shadow: var(--focus-ring-on-panel); }
.contact-card .textarea { min-height: 6rem; }
.contact-card__note { font-size: var(--size-200); color: var(--color-panel-muted); margin: 0; }


.contact-list { display: grid; gap: var(--space-3); max-width: var(--width-narrow); }
.contact-list a {
  display: flex;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
  padding: var(--space-5);
  border: var(--stroke-thin) solid var(--color-line);
  border-radius: var(--radius-md);
  color: var(--color-text);
  transition: background var(--duration-fast) var(--ease),
              border-color var(--duration-fast) var(--ease);
}
.contact-list a:hover { background: var(--color-surface); border-color: var(--color-line-interactive); color: var(--color-text); }
.contact-list span:last-child { color: var(--color-text-muted); }


/* ============================================================================
   12  TAG
   ========================================================================== */

.tag {
  display: inline-flex;
  align-items: center;
  gap: var(--space-1);
  font-size: var(--size-200);
  color: var(--color-text-muted);
  border: var(--stroke-thin) solid var(--color-line-strong);
  border-radius: var(--radius-full);
  padding: var(--space-1) var(--space-3);
}
.tag--accent {
  color: var(--color-on-accent);
  background: var(--color-accent);
  border-color: var(--color-accent);
}
.panel .tag { color: var(--color-panel-muted); border-color: var(--color-panel-line); }


/* ============================================================================
   12b TOGGLE
   ----------------------------------------------------------------------------
   A segmented two-option control, built for the Norwegian / English switch.

   Markup is two real <button>s inside a role="group", each carrying
   aria-pressed. A switch role would be wrong here: this is not on/off, it is a
   choice between two equal options, and a screen reader should be able to hear
   which language is active.

   The active option uses the accent fill, so the current language is carried by
   colour AND by aria-pressed, never by colour alone.
   ========================================================================== */

.toggle {
  display: inline-flex;
  align-items: center;
  padding: 3px;
  gap: 2px;
  border: var(--stroke-thin) solid var(--color-line-interactive);
  border-radius: var(--radius-full);
}

.toggle__option {
  font-family: var(--family-sans);
  font-size: var(--size-200);
  font-weight: var(--weight-medium);
  line-height: 1;
  color: var(--color-text-muted);
  background: transparent;
  border: 0;
  border-radius: var(--radius-full);
  padding: var(--space-2) var(--space-3);
  cursor: pointer;
  transition: background var(--duration-fast) var(--ease),
              color var(--duration-fast) var(--ease);
}
.toggle__option:hover { color: var(--color-text); }

.toggle__option[aria-pressed="true"] {
  background: var(--color-accent);
  color: var(--color-on-accent);
}
.toggle__option[aria-pressed="true"]:hover { background: var(--color-accent-hover); }

.toggle__option:focus-visible { box-shadow: var(--focus-ring); }

/* On the bone panel the outline and the resting label need the light scale. */
.panel .toggle { border-color: var(--color-panel-dim); }
.panel .toggle__option { color: var(--color-panel-muted); }
.panel .toggle__option:hover { color: var(--color-panel-text); }
.panel .toggle__option[aria-pressed="true"] { color: var(--color-panel-text); }
.panel .toggle__option:focus-visible { box-shadow: var(--focus-ring-on-panel); }

/* Sizes, to sit beside the nav or stand alone. */
.toggle--sm .toggle__option { font-size: var(--size-100); padding: var(--space-1) var(--space-2); }
.toggle--lg .toggle__option { font-size: var(--size-400); padding: var(--space-3) var(--space-5); }


/* ============================================================================
   13  PAGINATION
   ========================================================================== */

.pagination {
  display: flex;
  justify-content: space-between;
  gap: var(--space-4);
  flex-wrap: wrap;
  padding-top: var(--space-5);
  border-top: var(--stroke-thin) solid var(--color-line);
  font-size: var(--size-300);
}
.pagination a { color: var(--color-text-muted); }
.pagination a:hover { color: var(--color-link); }
.pagination [aria-disabled="true"] { color: var(--color-text-dim); pointer-events: none; }


/* ============================================================================
   14  FORM
   ----------------------------------------------------------------------------
   Nothing on the site uses these yet. They exist so a contact form can be
   added without inventing new patterns under time pressure.
   ========================================================================== */

.field { display: flex; flex-direction: column; gap: var(--space-2); }

.field__label {
  font-size: var(--size-300);
  font-weight: var(--weight-medium);
  color: var(--color-text);
}

.input, .textarea, .select {
  appearance: none;
  -webkit-appearance: none;
  font-family: var(--family-sans);
  font-size: var(--size-400);
  color: var(--color-text);
  background: var(--color-surface);
  border: var(--stroke-thin) solid var(--color-line-interactive);
  border-radius: var(--radius-md);
  padding: var(--space-3) var(--space-4);
  width: 100%;
  transition: border-color var(--duration-fast) var(--ease);
}
.input::placeholder, .textarea::placeholder { color: var(--color-text-dim); }
.input:hover, .textarea:hover, .select:hover { border-color: var(--color-text-muted); }
.textarea { min-height: 8rem; resize: vertical; line-height: var(--leading-body); }

.field__help  { font-size: var(--size-200); color: var(--color-text-muted); }
.field__error { font-size: var(--size-200); color: var(--color-danger); }

/* Error is never signalled by colour alone: the border thickens too, and
   the message carries the reason. */
.field--invalid .input,
.field--invalid .textarea {
  border-color: var(--color-danger);
  border-width: var(--stroke-thick);
}


/* ============================================================================
   15  NOTICE
   ========================================================================== */

.notice {
  display: flex;
  gap: var(--space-3);
  padding: var(--space-4);
  border-radius: var(--radius-md);
  border: var(--stroke-thin) solid var(--color-line-strong);
  background: var(--color-surface);
  font-size: var(--size-300);
  color: var(--color-text-body);
}
.notice--accent  { border-color: var(--color-accent-quiet); background: var(--color-accent-wash); }
.notice--success { border-color: var(--color-success); }
.notice--danger  { border-color: var(--color-danger); }
