/* ═══════════════════════════════════════════════════════════════
   Portfolio, Yann Chrétienneau

   A paper ground hatched at forty-five degrees, astralaika's own
   texture, drifting slowly behind a calm reading column; slabs of red
   clay for the hero and the section banners, pressed by js/relief.js.

   Nothing here is an image file. The hatch is two gradients, the clay is
   a computed height field, and the mark in the bar is drawn in the
   markup. No request leaves the page.
   ═══════════════════════════════════════════════════════════════ */

/* ── Self-hosted typefaces (SIL OFL), no Google Fonts request ── */
@font-face {
  font-family: "Anton";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../assets/fonts/anton-400-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "PT Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../assets/fonts/ptmono-400-cyrillic.woff2") format("woff2");
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
  font-family: "PT Mono";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../assets/fonts/ptmono-400-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "PT Serif";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url("../assets/fonts/ptserif-400i-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "PT Serif";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../assets/fonts/ptserif-400-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}
@font-face {
  font-family: "PT Serif";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("../assets/fonts/ptserif-700-latin.woff2") format("woff2");
  unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}


/* Extra subsets: without these, any character outside Latin-1 falls back to a
   system font mid-word (Vietnamese, Hungarian, Czech, Polish, Romanian...).
   unicode-range means a browser only downloads the subset it actually needs. */
@font-face {
  font-family: "Anton";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../assets/fonts/anton-400-latin-ext.woff2") format("woff2");  /* latin-ext : pl, cs, hu, ro, tr */
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "Anton";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../assets/fonts/anton-400-vietnamese.woff2") format("woff2");  /* vietnamese */
  unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB;
}
@font-face {
  font-family: "PT Serif";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../assets/fonts/ptserif-400-latin-ext.woff2") format("woff2");  /* latin-ext */
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "PT Serif";
  font-style: normal;
  font-weight: 400;
  font-display: swap;
  src: url("../assets/fonts/ptserif-400-cyrillic.woff2") format("woff2");  /* cyrillic : ru, uk */
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
  font-family: "PT Serif";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("../assets/fonts/ptserif-700-latin-ext.woff2") format("woff2");  /* latin-ext */
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "PT Serif";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("../assets/fonts/ptserif-700-cyrillic.woff2") format("woff2");  /* cyrillic */
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}
@font-face {
  font-family: "PT Serif";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url("../assets/fonts/ptserif-400i-latin-ext.woff2") format("woff2");  /* latin-ext */
  unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
}
@font-face {
  font-family: "PT Serif";
  font-style: italic;
  font-weight: 400;
  font-display: swap;
  src: url("../assets/fonts/ptserif-400i-cyrillic.woff2") format("woff2");  /* cyrillic */
  unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}


/* ── Design tokens ────────────────────────────────────────────── */
:root {
  --paper: #F3EDE0;
  --paper-2: #EAE2CF;
  /* The ground of the page, and only that. It is the tone of CaptainFork's
     panel on astralaika.com. Everything that sits ON the page, the bar,
     the reading plates, the sheet, keeps --paper, so they now lift off
     this ground instead of merging with it. */
  --ground: #EAE2CF;
  --card: #FBF7EC;
  --ink: #16130E;
  --ink-soft: #57503F;
  --faded: #B4AA93;
  --red: #C8321E;
  --red-dark: #9E2412;
  --plate-gap: clamp(16px, 2.8vh, 34px);
  --gold: #C89B3C;
  --yellow: #D9A017;
  --line: rgba(22, 19, 14, .16);

  --font-d: "Anton", "Arial Black", system-ui, sans-serif;
  --font-d-weight: 400;
  --font-b: "PT Serif", Georgia, serif;
  --font-m: "PT Mono", ui-monospace, "Courier New", monospace;

  /* The reading column is the one measurement the whole page answers to:
     the field is erased under it, the bar aligns to it, the CV sheet
     inherits its rhythm. */
  --col: 66ch;

  /* The vertical rhythm, in one place. It was three separate figures that
     did not agree: a section's content had 49px above it and 49 + a 146px
     spacer below, so every block on the page sat high in its own space.
     Measured before it was believed. One step now, and it is applied on
     both sides of everything it separates. */
  --step: clamp(3.4rem, 8vh, 6.4rem);
  --step-card: clamp(2.2rem, 4.2vh, 3.4rem);
  /* 59 plus the two rules, 6 of red above, 3 of ink below, makes 68, which
     is astralaika's --bar-h and LaikaSuite's, measured in their own
     stylesheets. It was 56 with 12 pixels of padding on top, so the bar
     stood 3 pixels taller than the house's. It is a minimum, not a height:
     the bar still grows when a long language wraps it. */
  --bar-h: 59px;
  /* The bar's whole height: its own 6px rule above, the 3px rule below and
     the 2px progress line. The hero subtracts exactly this, so its foot
     lands on the fold rather than a few pixels past it. */
  --bar-total: calc(var(--bar-h) + 9px);

  /* Line weights, taken from the LaikaSuite hub's design system so the two
     products draw the same edge: --rule 1px, --rule-strong 2px (the black
     cartoon border round every plate), --rule-section 3px (the weight this
     page's own navigation bar already uses under itself). */
  --rule: 1px;
  --rule-strong: 2px;
  --rule-section: 3px;
}

/* ── Display font for the scripts Anton does not cover ──────────
   Anton is Latin-only. Without this, headings in Cyrillic/Greek/CJK/Arabic/
   Devanagari/Thai fall back to whatever the OS picks, and to weight 400,
   so they look thin next to Anton. We name a face per script and go heavy. */
html:lang(ru), html:lang(uk), html:lang(el) {
  --font-d: "Arial Black", "Noto Sans", "DejaVu Sans", system-ui, sans-serif;
  --font-d-weight: 900;
}
html:lang(zh) {
  --font-d: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans CJK SC", "Source Han Sans SC", sans-serif;
  --font-d-weight: 800;
}
html:lang(ja) {
  --font-d: "Hiragino Kaku Gothic ProN", "Yu Gothic", Meiryo, "Noto Sans CJK JP", "Source Han Sans JP", sans-serif;
  --font-d-weight: 800;
}
html:lang(ko) {
  --font-d: "Apple SD Gothic Neo", "Malgun Gothic", "Noto Sans CJK KR", "Source Han Sans KR", sans-serif;
  --font-d-weight: 800;
}
html:lang(ar) {
  --font-d: "Noto Naskh Arabic", "Geeza Pro", Tahoma, "Segoe UI", sans-serif;
  --font-d-weight: 700;
}
html:lang(hi) {
  --font-d: "Noto Sans Devanagari", "Nirmala UI", "Kohinoor Devanagari", sans-serif;
  --font-d-weight: 700;
}
html:lang(th) {
  --font-d: "Noto Sans Thai", "Leelawadee UI", Thonburi, sans-serif;
  --font-d-weight: 700;
}
/* Marks in these scripts stack high or hang low: a tight leading would collide. */
html:lang(ar) :is(h1, h2, h3),
html:lang(hi) :is(h1, h2, h3),
html:lang(th) :is(h1, h2, h3) { line-height: 1.3; }
html:lang(zh) :is(h1, h2, h3),
html:lang(ja) :is(h1, h2, h3),
html:lang(ko) :is(h1, h2, h3) { line-height: 1.15; letter-spacing: 0; }
/* Arabic is a joined script: tracking tears the letters apart, silently. */
html:lang(ar) * { letter-spacing: 0 !important; }

/* ── Reset and ground ─────────────────────────────────────────── */
*, *::before, *::after { box-sizing: border-box; }

/* The selection, in the poster's red rather than the system's blue, the
   one line astralaika.com uses, copied from its stylesheet. Paper on red
   is 4.58:1, which holds for the whole of a Ctrl+A. */
::selection { background: var(--red); color: var(--paper); }

/* The page's scrollbar, tinted with the poster's red, astralaika's own
   two lines, copied from its stylesheet rather than reinvented.

   Two properties, and deliberately no ::-webkit-scrollbar block after
   them: a browser that understands `scrollbar-width` or `scrollbar-color`
   abandons the WebKit pseudo-elements entirely, and these rules put both
   on `*`, so every element on every page would opt itself out of a block
   written for it. Square, like everything else here, a rounded thumb
   would be the only roundness on the site.

   Both axes are covered: neither property knows about direction.

   What it costs: Safari before 18.2 knows neither standard property and
   obeyed only the block that is deliberately absent. Those readers get
   the system scrollbar, a colour, not a function. */
* {
  scrollbar-width: thin;
  scrollbar-color: var(--red) transparent;
}
html { -webkit-text-size-adjust: 100%; scroll-behavior: smooth; }
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } }

body {
  margin: 0;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--font-b);
  font-size: 17px;
  line-height: 1.72;
  overflow-x: hidden;
}

img { max-width: 100%; display: block; }
a { color: var(--ink); text-decoration-thickness: 1px; text-underline-offset: 3px; }
a:hover { text-decoration-color: var(--red); }

:focus-visible {
  /* Gold measures 2.19:1 on paper, invisible. The dark red is 6.63:1. */
  outline: 2px solid var(--red-dark);
  outline-offset: 3px;
}

.mono {
  font-family: var(--font-m);
  font-size: 12px;
  letter-spacing: .14em;
  text-transform: uppercase;
}

.skip {
  position: absolute;
  inset-inline-start: -9999px;
  top: 0;
  z-index: 100;
  background: var(--ink);
  color: var(--paper);
  padding: .6rem 1rem;
}
.skip:focus { inset-inline-start: 0; }

/* ── The field ────────────────────────────────────────────────────
   Three fixed layers, drawn once. Scroll only translates them, each at
   its own rate, which is what makes the depth read. The mask above them
   is a plane of --paper that erases the field under the reading column. */
/* ═══ THE GROUND ═══════════════════════════════════════════════════
   Hatching, which is astralaika's own texture, the diagonal rule of its
   catalogue and the red counter-hatch of its classified file, copied from
   its stylesheet rather than invented.

   What was here before was a canvas: five thousand Perlin trajectories,
   traced on every load, a thousand steps of arithmetic drained through
   idle time. This is two gradients. It costs nothing after the first
   paint, and the drift below happens on the compositor, the main thread
   never sees it. */
.field {
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  overflow: hidden;
  contain: strict;
}
/* ── The ghosts ──────────────────────────────────────────────────
   The section's own emblem, six hundred pixels across, blurred and laid
   under both trames so it is read through the hatching. It is centred, so
   it spills into the free margin on either side of the reading column,
   the plate is opaque and covers its middle, which is what keeps it a
   ground rather than a picture.

   Ink at a fifth: measured against --paper-2, that is a shape one sees
   without ever being asked to look at it. */
.ghosts { position: absolute; inset: 0; }
/* Centred, and read both through the plate and around it: the sheet is
   pulled back to ninety-two hundredths, so the middle of the emblem shows
   faintly under the reading while the rest of it stands clear in the
   margins. It is far wider than the column on purpose, at the column's own
   width there was nothing left of it on either side. */
.ghost {
  position: absolute;
  inset-block-start: 50%;
  inset-inline-start: 50%;
  translate: -50% -50%;
  /* Eighty px narrower than it was. With every ink normalised to the same
     width, the two tall emblems, the arm and the runner, become half as
     tall again as the square ones, and at 880 they ran off the top and the
     bottom of the window. */
  inline-size: clamp(480px, 56vw, 800px);
  block-size: auto;
  /* Astralaika's red, not ink: in ink it read as a shadow cast by the
     plate, which is a thing the page does not otherwise do. */
  fill: var(--red);
  stroke: var(--red);
  color: var(--red);
  /* Normalised on width, two of them end up taller than their own viewBox;
     an SVG clips to it by default. */
  overflow: visible;
  opacity: 0;
  /* No blur. It was blurred for a long while and it was wrong: everything
     on this page has a hard edge, two-pixel borders, flat fills, not one
     gradient, so the one soft object read as a photograph out of focus,
     which is a register the page does not otherwise have. Sharp and faint,
     the shape reads as an impression under the paper, and the network's
     connections come back as thin diagonals in the margins instead of
     dissolving into three pink lumps. */
  transform: scale(.92) rotate(-4deg);
  transition: opacity .9s ease, transform 1.4s ease;
  will-change: opacity, transform;
}
/* A hairline does not survive a blur. Half of these emblems are drawn in
   stroke, the network, the arrow, the runner, and at four and a half
   units under seven pixels of blur they vanished entirely while the solid
   ones came through. The ghost is a silhouette, so it can afford a much
   heavier line than the band it comes from. */
.ghost [stroke-width] { stroke-width: 9; }
/* No drift. It moved the lit emblem sixteen pixels either side of the plate
   and ten to twelve up and down, on a twenty-six second cycle, which means
   that at any instant the reader looks at it, and in every screenshot, it
   was off centre by up to sixteen pixels. Measured against the plate it
   stands behind, that was the whole of the misalignment: the drawings
   themselves land on the middle of the column to the pixel. A ghost that is
   meant to read as an impression under the paper has no business sliding
   about; it now sits exactly where the box is. */
.ghost.is-on {
  opacity: .26;
  transform: scale(1) rotate(0deg);
}
@media (max-width: 1100px) { .ghosts { display: none; } }
@media (prefers-reduced-motion: reduce) {
  .ghost { transition: opacity .01s; transform: none; }
  .ghost.is-on { transform: none; }
}

.field-hatch {
  position: absolute;
  /* Larger than the window on every side: the layers travel with the
     scroll and drift on their own, and an edge must never come into view. */
  inset: -20%;
  will-change: transform;
}

/* The far field: ink at 2.8%, thirty pixels apart. It is astralaika's
   catalogue ground, at its own figures. */
.field-far {
  background-image: repeating-linear-gradient(45deg,
    transparent 0 30px, rgba(22, 19, 14, .028) 30px 31px);
  transform: translate3d(0, var(--far, 0px), 0);
  animation: hatch-far 96s linear infinite;
}

/* The near field: the same angle, never crossed. astralaika hatches at 45
   degrees on one surface and at minus 45 on another; it never puts both on
   the same one, and putting both on this one produced a diamond lattice,
   a plaid, which is a pattern, where a hatch is a texture.

   So it runs parallel to the far field and much wider apart: one red rule
   every ninety-seven pixels among the fine ink ones. Because the two
   layers travel at different rates, that heavy rule drifts slowly across
   the fine ones and the ground never looks twice the same. */
.field-near {
  background-image: repeating-linear-gradient(45deg,
    transparent 0 96px, rgba(200, 50, 30, .07) 96px 97px);
  transform: translate3d(0, var(--near, 0px), 0);
  animation: hatch-near 61s linear infinite;
}

/* The loop is seamless because the distance is the pattern's own period.
   A 45-degree hatch of period P shifts along its gradient line by dx/√2
   when the layer moves dx sideways, so a full period needs dx = P√2:
   43.84px for the far field at 31, 137.18px for the near one at 97. Any
   other figure and the pattern would jump at the end of the loop.

   The two run in opposite directions and on periods that do not divide
   into one another, ninety-six seconds and sixty-one, so the crossing
   of the two hatches never repeats itself inside a reading. */
@keyframes hatch-far {
  from { translate: 0 0; }
  to   { translate: 43.84px 0; }
}
@keyframes hatch-near {
  from { translate: 0 0; }
  to   { translate: -137.18px 0; }
}
@media (prefers-reduced-motion: reduce) {
  .field-hatch { animation: none; }
}

/* No veil, no gradient: the reading column is a solid plate of paper laid
   over the field, and the field is simply cut by it. The edge is declared
   rather than blurred, see .column below. */

/* ── The bar ──────────────────────────────────────────────────── */
.bar {
  position: sticky;
  top: 0;
  z-index: 60;
  background: var(--paper);
  /* astralaika.com's own bar: a heavy red rule above, a solid ink rule
     below. Those two lines are what makes the bar read as the house's. */
  border-block-start: 6px solid var(--red);
  border-block-end: 3px solid var(--ink);
}
/* The gap was a flat 32px between four blocks, 96 pixels of it, and the
   bar came to five pixels more than a 1440 window could hold, so it folded
   to two rows and the hero lost 37 pixels of height to it. Measured, not
   guessed: mark 185, nav 807, language 61, button 158, for 1302 of room. */
.bar-in {
  /* astralaika's bar spans the window: the mark sits against the left edge
     and the button against the right. Boxed and centred, it read as a
     floating strip rather than as the top of the page. */
  min-height: var(--bar-h);
  /* Three vw, not four and a half. The bar carries more than the page's
     other rows, a name, six entries, a language and a button, so it is
     the one place where the gutter has to give before the line does. It
     keeps its full 64 on a wide screen. */
  padding-inline: clamp(18px, 3vw, 64px);
  display: flex;
  align-items: center;
  gap: clamp(10px, 1.5vw, 26px);
  /* It wraps. Measured before it was believed: at a 745px viewport the
     row came to 1029px and the language button and the CV plate ran off
     the right edge, a band a laptop can easily sit in, between the point
     where the section links are dropped (700px) and the point where there
     is room for everything (about 1180px). A bar that overflows is worse
     than a bar on two rows.

     The height that follows from wrapping is not guessed either: hero.js
     measures the bar and publishes it, because the hero subtracts exactly
     this and a German "Curriculum vitæ" is not a French one. */
  flex-wrap: wrap;
  row-gap: 6px;
}
/* The mark: a generated ring-and-bust in the poster's red, and the first
   name. Nothing is fetched, the shape is drawn in the markup and takes
   its colour from the link, so it is one file fewer and one request
   fewer. */
.mark-logo {
  inline-size: 1.55em;
  block-size: 1.55em;
  flex: none;
  color: var(--red);
  /* No vertical-align: the mark is a flex row and centres its items. The
     property did nothing here and was the wrong tool for the job. */
}
.mark { display: inline-flex; align-items: center; gap: .55rem; }
.mark {
  font-family: var(--font-d);
  font-weight: var(--font-d-weight);
  font-size: 18px;
  letter-spacing: .05em;      /* .brand-name on astralaika.com */
  text-transform: uppercase;
  color: var(--ink);
  text-decoration: none;
  white-space: nowrap;
}
.mark:hover { text-decoration: underline; text-decoration-color: var(--red); text-decoration-thickness: 2px; }
/* The gap gives before the line does. Seven entries, a full name on the
   left and a button on the right came to more than a 1500px window could
   hold on one line, and the bar folded to 105px, which the hero then
   subtracts from its own height. Narrower gaps and a shorter label for the
   longest entry keep it on one line down to a small laptop. */
.bar-nav { display: flex; gap: clamp(.45rem, 1.3vw, 1.2rem); margin-inline-start: auto; }
.bar-nav a {
  font-family: var(--font-m);
  /* Twelve pixels on a wide screen, eleven on a laptop, and the tracking
     gives with it. Six entries plus a full name and a button are more than
     a 1366 window holds at a fixed size, and the bar folding costs the hero
     thirty-seven pixels of height. */
  font-size: clamp(11px, .82vw, 12px);
  letter-spacing: clamp(.06em, .09vw, .12em);
  text-transform: uppercase;
  text-decoration: none;
  /* This was --paper, and on the beige ground it was beige on beige.
     The bar inherits the page's ink now, as astralaika's does. */
  color: var(--ink);
  padding: 8px 12px;
  border-block-end: 3px solid transparent;
  transition: border-color .15s, color .15s;
}
.bar-nav a:hover,
.bar-nav a:focus-visible {
  border-block-end-color: var(--red);
  color: var(--red-dark);     /* 6.63:1 */
}
.bar-end { display: flex; align-items: center; gap: .6rem; margin-inline-start: auto; }
.bar-nav + .bar-end { margin-inline-start: clamp(.75rem, 2.5vw, 2rem); }

/* ── Buttons: notched constructivist plates ──────────────────────
   astralaika cuts the top-right corner off every button at 14px. That
   notch, and the hard offset shadow on hover, are the house's signature,
   a rectangle with a red fill is just a rectangle. */
.btn-cv, .btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-family: var(--font-d);
  font-weight: var(--font-d-weight);
  font-size: 14px;
  letter-spacing: .06em;
  text-transform: uppercase;
  text-decoration: none;
  padding: 13px 26px;
  border: 0;
  white-space: nowrap;
  flex-shrink: 0;
  clip-path: polygon(0 0, calc(100% - 14px) 0, 100% 14px, 100% 100%, 0 100%);
  transition: transform .12s, box-shadow .12s, background .15s, color .15s;
}
.btn-cv { background: var(--red); color: var(--paper); }
.btn-cv:hover,
.btn-cv:focus-visible {
  background: var(--red-dark);
  transform: translate(-2px, -2px);
  box-shadow: 4px 4px 0 var(--ink);
}
.btn-cv:active, .btn:active { transform: none; box-shadow: none; }

html[dir="rtl"] 
/* ── Language picker ──────────────────────────────────────────── */
.lang { position: relative; }
.lang-btn {
  font-family: var(--font-m);
  font-size: 12px;
  letter-spacing: .12em;
  /* astralaika's .lang-current: an ink-bordered plate on card, which
     inverts the moment it is hovered or opened. It was --paper on --paper
     here, which is to say invisible. */
  background: var(--card);
  color: var(--ink);
  border: 2px solid var(--ink);
  padding: 7px 12px;
  letter-spacing: .08em;
  cursor: pointer;
  white-space: nowrap;
  transition: background .15s, color .15s;
}
.lang-btn:hover,
.lang-btn[aria-expanded="true"] { background: var(--ink); color: var(--paper); }
.lang-panel {
  position: absolute;
  inset-block-start: calc(100% + .4rem);
  inset-inline-end: 0;
  z-index: 80;
  width: 15rem;
  max-height: min(60vh, 22rem);
  overflow-y: auto;
  background: var(--card);
  border: 2px solid var(--ink);
  padding: 0;
  scrollbar-color: var(--red) var(--paper-2);
  scrollbar-width: thin;
}
.lang-panel[hidden] { display: none; }
.lang-opt {
  display: flex;
  gap: .7rem;
  align-items: baseline;
  width: 100%;
  background: transparent;
  border: 0;
  color: var(--ink);
  font-family: var(--font-b);
  font-size: 15px;
  text-align: start;
  padding: 10px 14px;
  cursor: pointer;
}
/* astralaika marks the current locale with its own glyph and the house
   red, and hovers on a tint of the paper rather than on a red plate: the
   list stays quiet while it is being read down. */
.lang-opt { border-block-end: 1px solid var(--line); transition: background .12s; }
.lang-opt:last-child { border-block-end: 0; }
.lang-opt:hover { background: var(--paper-2); }
.lang-opt[aria-selected="true"] { color: var(--red); }
.lang-opt[aria-selected="true"] .lang-code::before { content: "★ "; }
.lang-code {
  font-family: var(--font-m);
  font-size: 11.5px;
  letter-spacing: .1em;
  text-transform: uppercase;
  min-width: 2.1em;
}

@media (max-width: 700px) {
  /* The bar must not swallow the page: the section links fold away and the
     mark, the CV route and the picker stay. */
  .bar-nav { display: none; }
}
@media (max-width: 460px) {
  .bar-in { padding-inline: .75rem; gap: .5rem; }
  .mark { font-size: 16px; }
  .btn-cv { font-size: 11px; letter-spacing: .05em; padding: .45rem .55rem; }
  .lang-btn { font-size: 11px; letter-spacing: .05em; padding: .45rem .45rem; }
}
@media (max-width: 430px) {
  /* Below this the three pieces of the bar cannot share one line without
     one of them being cut off, so the bar takes a second line instead. */
  .bar-in { flex-wrap: wrap; padding-block: .5rem; row-gap: .5rem; min-height: 0; }
  .mark { width: 100%; }
  .bar-end { width: 100%; margin-inline-start: 0; justify-content: space-between; }
  /* The label is long once written out in full. Below the fold the plate
     gives up its generous padding rather than pushing the picker off. */
  .bar .btn-cv { padding: 11px 14px; font-size: 13px; letter-spacing: .04em; }
}

/* ── Layout ──────────────────────────────────────────────────────
   Every block of reading sits on its own opaque plate. The plate is
   carried on a red rule: the column is literally held up by the red.
   Between plates the paper is bare and the field runs the full width. */
.wrap { position: relative; z-index: 2; }
.column {
  position: relative;
  max-width: var(--col);
  margin-inline: auto;
  /* Ninety-two hundredths. Enough for the blurred emblem to be read through
     the sheet. Ninety-two was measured and invisible, three points of
     green, which no eye reads. Eighty-two with the ghost at a third was
     blotchy: the page went spotted and the plate read as tinted glass.
     Eighty-six with the ghost at a quarter is the middle of those two, and
     ink still measures 15:1 against a threshold of 4.5. */
  background: rgba(243, 237, 224, .86);
  border-inline-start: 2px solid var(--red);
  border-inline-end: 1px solid var(--line);
  /* The same figure above and below. */
  padding-block: var(--step);
  padding-inline: clamp(1.1rem, 4vw, 2.4rem);
}
/* The plate sets the same figure above and below, padding-block: var(--step), but that only holds if the last thing in the column stops where its box
   stops. Anything it carries below itself is added to the bottom padding and
   nothing balances it at the top, so the rhythm drifted section by section:
   measured at 1440px, the gap between the end of the reading and the next
   marker read 65.0 under a paragraph, 77.8 under a list (the last row's
   .8rem of padding) and 81.8 under the field caption (a paragraph's 1.05rem
   margin). Seventeen pixels of drift over seven sections, and the eye reads
   it as the sections being unevenly spaced.

   The chain is three deep because that is how deep the columns actually go:
   the last paragraph of the last beat of the last project. */
.column > :last-child,
.column > :last-child > :last-child,
.column > :last-child > :last-child > :last-child { margin-block-end: 0; }
/* The row padding is what separates a row from the rule under it. The last
   row has no rule under it, only the edge of the plate. */
.column > .list:last-child > li:last-child { padding-block-end: 0; }

/* The plate does not touch the band. It used to sit flush against the red,
   which glued the reading to the poster and left the ground, the whole
   point of the composition, visible only between sections. One figure,
   above and below, so every card is detached from its number in the same
   way. */
.marker + .column { margin-block-start: var(--plate-gap); }
.column { margin-block-end: var(--plate-gap); }
/* The approach block has no band: its plate hangs off the hero instead,
   and wants the same air. */
#approche .column { margin-block-start: var(--plate-gap); }

section { padding: 0; }
section + section, .void + section { margin-block-start: 0; }
#accueil .column { padding-block: clamp(3.4rem, 9vh, 6rem); }

/* The emptiness is part of the composition: a full-bleed span with nothing
   on it, so the field is seen alone between two plates. */
/* ═══ THE SLAB ═════════════════════════════════════════════════════
   One band of red clay, edge to edge, with the name pressed into it.
   The rules below are portfolio 23's, read from its own stylesheet
   rather than guessed from a screenshot, which is what the first two
   attempts at this were, and why they were not the same thing.

   The canvas carries the computed relief and sits behind everything.
   Without it the band is flat --red and the page still reads: every word
   in here is real text in the document. */
.hero {
  position: relative;
  height: calc(100vh - var(--bar-total));
  height: calc(100dvh - var(--bar-total));
  display: flex;
  flex-direction: column;
  /* The name at the top, the reading block filling everything under it. It
     used to be centred as one lump, which left a band of bare clay below the
     panel on a tall window. The bottom padding is the margin it stops at. */
  justify-content: flex-start;
  gap: clamp(1.2rem, 3vh, 2.4rem);
  /* More at the foot than at the head, and the difference is the cue's
     band. Pinned to the bottom, it was landing behind the reading panel:
     the grid row is as tall as the portrait's ceiling and only thirty-two
     pixels of clay were left under it. */
  padding: clamp(1.4rem, 4vh, 3rem) var(--gutter, clamp(18px, 4vw, 56px))
           clamp(74px, 10vh, 108px);
  background: var(--red);
  color: var(--paper);
  border-block-end: var(--rule-section) solid var(--ink);
  overflow: hidden;
}
[data-relief] > canvas {
  position: absolute; inset: 0;
  inline-size: 100%; block-size: 100%;
  display: block;
  pointer-events: none;
}
[data-relief] > *:not(canvas) { position: relative; z-index: 1; }

/* Display type is debossed. Once the generator has actually pressed a
   word it sets `is-cut` on it and the flat copy is hidden, leaving the
   impression on the canvas. The flat copy stays in the document and in
   the accessibility tree either way, and it is what the canvas measured
   itself against, so it may be hidden, never removed. */
/* The flat copy does not vanish, it hands over. Without a transition the
   swap is a snap in the middle of the reading, the word is there, then a
   different word is there. A quarter of a second is enough to read as one
   thing settling. */
[data-relief-type] { transition: opacity .28s linear; }
/* The flat copy steps aside once its impression exists, the emblem the
   same way the letterforms do. Until then it is simply there, in paper,
   which is also what a reader without scripts keeps. */
[data-relief-type].is-cut,
[data-relief-svg].is-cut { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
  [data-relief-type] { transition: none; }
}

.hero-top {
  inline-size: 100%;
  max-width: 1360px;
  margin-inline: auto;
}

/* The size here is the fallback for a page whose script never runs.
   hero.js measures the type and sets the real one, because no fixed
   clamp can be right for "Chrétienneau", "Хретьенно" and "크레티에노"
   at once. */
.hero-name {
  font-family: var(--font-d);
  font-weight: var(--font-d-weight);
  text-transform: uppercase;
  line-height: 1.04;
  letter-spacing: .004em;
  font-size: clamp(2.2rem, 10vw, 7rem);
  color: var(--paper);
  margin: 0;
}
.hero-name > span { display: block; }

/* A chip is the only small text allowed to sit on the clay: a run of
   type over a computed field cannot be trusted for contrast, because a
   lit highlight can pass behind it. So it carries its own opaque ground, paper on ink is 15.88:1, and is registered as an impression, which
   is also what gives it its pressed edge.

   Not scoped to the hero: every slab on the page needs it, and the
   section markers are slabs. */
.chip {
  display: inline-block;
  background: var(--ink);
  color: var(--paper);
  padding: 7px 12px;
  margin: 0 0 clamp(14px, 2.4vh, 26px);
  font-size: 12px;
  letter-spacing: .14em;
  text-transform: uppercase;
}

/* ═══ THE FLAT PLANES ══════════════════════════════════════════════
   All prose lives here, on its own opaque ground, for the same reason
   the chip does. They butt together with three pixels of slab showing
   between them. */
.hero-grid {
  /* Grows into whatever the name leaves. Its two cells stretch with it, so
     the panel and the photograph lengthen together and stay equal. */
  flex: 1 1 auto;
  min-block-size: 0;
  inline-size: 100%;
  max-width: 1360px;
  margin-inline: auto;
  display: grid;
  gap: 3px;
}
@media (min-width: 900px) {
  .hero-grid { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); }
}
.hero .panel {
  background: var(--paper);
  color: var(--ink);
  padding: clamp(18px, 2.6vw, 32px);
  min-inline-size: 0;
  /* The same cartoon edge as the photograph beside it and as every plate
     on the site: two pixels of solid ink, --rule-strong, the LaikaSuite
     hub's own token. Both boxes are border-box, so the two rules meet at
     the three-pixel gap of the grid instead of one being drawn inside its
     box and the other around it. */
  border: var(--rule-strong) solid var(--ink);
}
.hero .panel--dark { background: var(--red-dark); color: var(--paper); }

.hero-line {
  max-width: 54ch;
  margin: 0 0 clamp(1rem, 2.2vh, 1.5rem);
  color: var(--ink-soft);
}

.hero-cta { display: flex; flex-wrap: wrap; gap: 12px; }

/* ── The portrait ────────────────────────────────────────────────
   A dark panel until he supplies the photograph. The width and height
   attributes reserve the box, so the file arriving later cannot move the
   composition, and neither can its absence. */

/* ── The portrait ────────────────────────────────────────────────
   Its own proportion, and a ceiling. Stretched to fill the cell it was
   cropped to whatever shape the reading panel beside it happened to be,
   a face at extreme zoom on a tall window. It keeps three by four now,
   and 44vh stops it from driving the height of the row: the row belongs
   to the reading, the photograph fits inside it. */
.hero .hero-portrait {
  position: relative;
  margin: 0;
  padding: 0;
  /* Stretched to the row, not given a shape of its own. It used to carry
     `aspect-ratio: 3/4` and a ceiling of 30vh, so its height was computed
     from its own width and had nothing to do with the panel beside it: the
     two boxes lined up at some window sizes and not at others, which is
     exactly what a zoom exposes. Stretching makes the panel set the height
     and the photograph follow it, they cannot disagree. The image is
     absolutely placed inside with object-fit: cover, so it fills whatever
     height the row turns out to be. */
  align-self: stretch;
  justify-self: end;
  inline-size: 100%;
}
.hero .hero-portrait img {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  /* Paper, not ink: the ground shows only while the file is in flight, and
     a cream rectangle belongs to this page in a way a black one does not. */
  background: var(--paper);
  /* The same cartoon edge as every plate on the site, two pixels of solid
     ink, --rule-strong, which is the LaikaSuite hub's own token. A
     photograph laid on the clay without it is the one thing here that is
     not cut out of something. */
  border: var(--rule-strong) solid var(--ink);
}

/* The hole in the composition: the slab's own top-right, empty while the
   name runs along the bottom-left. Paper on the clay, at the scale the
   space asks for, the same ink the section bands use, an order of
   magnitude larger. Absolutely placed so it takes part in no measurement.

   It goes when the layout stacks: below 900px the hero has no top-right
   corner to speak of. */
/* `.hero > .hero-art`, not `.hero-art`: the slab carries data-relief, and
   `[data-relief] > *:not(canvas)` sets every child to position: relative so
   the pressed content clears the relief canvas. That selector outranks a
   single class, and it was quietly turning this one back into a flow item,
   which is why it sat at the top left at the wrong size. The z-index keeps
   it on the right side of the canvas all the same. */
.hero > .hero-art {
  position: absolute;
  z-index: 1;
  aspect-ratio: 1;
  inset-block-start: clamp(4%, 7%, 10%);
  inset-inline-end: var(--gutter, clamp(18px, 4vw, 56px));
  inline-size: clamp(118px, 15vw, 246px);
  block-size: auto;
  fill: var(--paper);
  stroke: var(--paper);
  color: var(--paper);
  pointer-events: none;
}
@media (max-width: 900px) { .hero-art { display: none; } }

/* On red, paper is 4.58:1 and --ink-soft is 1.50:1, not a contrast, a
   rumour. The cue sits on the clay, so it is paper. */

@media (max-width: 880px) {
  .hero-portrait { display: none; }
}
@media (max-height: 620px) and (orientation: landscape) {
  .hero { height: auto; min-height: calc(100dvh - var(--bar-total)); overflow: visible; }
  .hero-portrait, .hero .cue { display: none; }
}

/* ═══ THE MARKER ═══════════════════════════════════════════════════
   Each section opens on its own strip of clay with its title debossed
   into it. Same slab, same light, same four tones as the hero. */
.marker {
  position: relative;
  inline-size: 100%;
  background: var(--red);
  color: var(--paper);
  display: flex;
  align-items: center;
  /* A strip, not a slab. It was 267px of red cutting the page every screen
     or two, and an effect used once is a signature while an effect used
     seven times is wallpaper. On one line it punctuates instead of
     interrupting: the red runs the length of the document and never takes
     the lead over the reading. */
  padding: clamp(14px, 2vh, 22px) var(--gutter, clamp(18px, 4vw, 56px));
  /* The cartoon edge, and the same one everything else on the page draws:
     three pixels of solid ink, --rule-section, which is what closes the
     hero, what the navigation bar ends on, and what the chips carrying 01
     and 02 are made of. A band that cuts the page has to be cut out of it,
     not laid on it.

     Top and bottom only: the band runs edge to edge, so those two are the
     whole of its visible outline. The relief canvas is absolutely
     positioned and fills the padding box, which stops inside the border,
     the edge is never painted over. */
  border-block: var(--rule-section) solid var(--ink);
  overflow: hidden;
}
/* Number and title on the same baseline. On a strip this deep there is no
   room for them to stack, and no need: the number is a label on the title,
   not a line of its own. */
/* ── The emblems ─────────────────────────────────────────────────
   One per section, on the left of the band, before the number and the
   title. Solid paper, not an outline: the band is a strip, the figure is
   forty pixels tall, and a hairline at that size closes up into a smudge.

   Paper on red is 4.58:1. That is a reading contrast, and this is not even
   asked to be read, but a figure that cannot be made out is worse than no
   figure, so it takes the strong tone rather than a discreet one. */
.marker-art {
  flex: none;
  inline-size: clamp(30px, 3.4vw, 44px);
  block-size: auto;
  fill: var(--paper);
  stroke: var(--paper);
  margin-inline-end: clamp(.8rem, 2vw, 1.4rem);
}
/* Below this width the band is the title and nothing else: an emblem and a
   long heading fighting over a phone's width is two things badly placed
   instead of one placed well. */
@media (max-width: 720px) { .marker-art { display: none; } }

.marker-head {
  min-inline-size: 0;
  display: flex;
  align-items: baseline;
  gap: clamp(.9rem, 2.5vw, 2rem);
}

.marker-n {
  margin: 0;
  font-size: 11px;
  letter-spacing: .18em;
  padding: 5px 10px;
  flex: none;
}
.marker-t {
  font-family: var(--font-d);
  font-weight: var(--font-d-weight);
  text-transform: uppercase;
  line-height: 1.0;
  font-size: clamp(1.4rem, 3.6vw, 2.6rem);
  color: var(--paper);
  margin: 0;
  /* The impression is drawn on the canvas behind, and the canvas is as
     wide as the strip: a title longer than the strip would be pressed
     outside it. Anton is narrow, but "Expérience professionnelle" in
     German is not. */
  overflow-wrap: break-word;
}

/* The cue: a label and a rule that keeps falling. */
/* On the red slab this label was --ink-soft at 1.50:1, which is not a
   contrast, it is a rumour. Paper is 4.58:1. */
/* ── The cue ─────────────────────────────────────────────────────
   Taken out of the flow. In the column it was the last item of a block
   already as tall as the screen, so the middle-aligned content pushed it
   past the bottom edge: measured at a 900px window, its own box ended
   fourteen pixels BELOW the hero, and the rule it drops falls another
   forty-two. `overflow: hidden` cut it in half.

   Pinned instead, on the gutter and clear of the bottom by enough for the
   whole of that fall. It costs the block nothing now. */
.hero .cue {
  position: absolute;
  inset-block-end: clamp(30px, 5vh, 58px);
  inset-inline-start: var(--gutter, clamp(18px, 4vw, 56px));
  z-index: 1;
  margin: 0;
  color: var(--paper);
}
.hero .cue::after { background: var(--paper); }
.hero .cue:hover { color: var(--card); }

.cue {
  align-self: flex-start;
  margin-block-start: clamp(1.4rem, 4vh, 2.6rem);
  color: var(--ink-soft);
  text-decoration: none;
  display: inline-flex;
  align-items: center;
  gap: .7rem;
}
.cue::after {
  content: "";
  inline-size: 1px;
  block-size: 28px;
  background: var(--red);
  transform-origin: top center;
  animation: cue-fall 2.4s cubic-bezier(.4, 0, .2, 1) infinite;
}
@keyframes cue-fall {
  0%   { transform: scaleY(0); opacity: 0; }
  35%  { transform: scaleY(1); opacity: 1; }
  75%  { transform: scaleY(1); opacity: 1; }
  100% { transform: scaleY(1) translateY(14px); opacity: 0; }
}
.cue:hover { color: var(--red-dark); }

/* The .void spacers are gone: an empty div between sections put all of a
   section's breathing on one side of it. The step is on the blocks
   themselves now, above and below, so the rhythm is the same wherever it
   is measured from. */


h1, h2, h3 {
  font-family: var(--font-d);
  font-weight: var(--font-d-weight);
  text-transform: uppercase;
  letter-spacing: .01em;
  line-height: 1.02;
  margin: 0;
}
h1 {
  font-size: clamp(2.6rem, 9vw, 5.2rem);
  color: var(--ink);
}
h2 {
  font-size: clamp(1.7rem, 4.6vw, 2.8rem);
  color: var(--red);          /* 4.58:1 on paper */
  margin-block-end: 1.6rem;
}
h3 {
  font-size: clamp(1.15rem, 2.8vw, 1.5rem);
  color: var(--ink);
  margin-block-end: .5rem;
}
p { margin: 0 0 1.05rem; max-width: 62ch; }
.lede { font-size: 1.12rem; }
.eyebrow {
  color: var(--ink-soft);     /* 6.86:1 on paper; --faded would be 1.98:1 */
  margin-block-end: .9rem;
  display: block;
}
.rule {
  block-size: 2px;
  background: var(--faded);
  border: 0;
  margin: clamp(16px, 2.4vw, 26px) 0;
}
[dir="rtl"] .rule { transform-origin: right center; }
.rule-faint { border-block-start-color: var(--line); }
.mt { margin-block-start: 1.8rem; }

.role {
  font-size: clamp(1.05rem, 2.6vw, 1.35rem);
  color: var(--ink-soft);     /* 6.86:1, a secondary voice, still readable */
  margin-block: 1.4rem 1.6rem;
}

/* ── Field caption, states plainly what the drawing is not ───── */
.caption {
  color: var(--ink-soft);
  font-family: var(--font-m);
  font-size: 12px;
  letter-spacing: .06em;
  line-height: 1.8;
  text-transform: none;
  max-width: 54ch;
  border-inline-start: 1px solid var(--line);
  padding-inline-start: .9rem;
  margin-block-start: 2.6rem;
}

/* ── The unknowns ─────────────────────────────────────────────── */
.todo { margin-block: 1.4rem; }
/* His own sentence, under a quote that is not his. It leads the method
   block rather than closing it: the reader meets the claim first and the
   evidence, NumPy without a library, a suite without a server, second. */
/* The ask, on the first screen. It borrows the red rule the method line
   uses further down, because it is the same kind of statement, one thing
   said plainly, on its own, and a recruiter should meet it before the
   quote and before the buttons. */
.hero-seek {
  margin-block: 0 1.2rem;
  padding-inline-start: 1rem;
  border-inline-start: var(--rule-strong) solid var(--red);
  color: var(--ink);
}

.approach-line {
  margin-block: 0 1.2rem;
  padding-inline-start: 1rem;
  border-inline-start: var(--rule-strong) solid var(--red);
  color: var(--ink);
  font-size: 1.12rem;
  line-height: 1.5;
}
.todo-tag {
  display: inline-block;
  background: var(--yellow);
  color: var(--ink);          /* 7.93:1 */
  font-family: var(--font-m);
  font-size: 11.5px;
  letter-spacing: .12em;
  text-transform: uppercase;
  padding: .18rem .5rem;
  margin-inline-end: .5rem;
}
.todo-text { color: var(--ink); }

/* ── Projects ─────────────────────────────────────────────────── */
/* Cards are separated, not stacked with a tail: the space belongs between
   two of them and nowhere else, so it is the same above the second as
   below the first, and there is none hanging off either end. */
.project + .project { margin-block-start: var(--step-card); }

/* ── The card answers ────────────────────────────────────────────
   Nothing on this page reacted to a pointer. The rule is drawn as a
   background rather than a border so that nothing moves: a border would
   push the title by two pixels and the whole column would flinch. */
/* Every heading in a reading column, not only the project ones. The rule is
   drawn as a background rather than a border so that nothing moves, a
   border would push the title two pixels and the column would flinch, and
   the box is inline-block, which hugs the text like an inline box while
   keeping the vertical margins an inline box drops. That last point is why
   the project titles used to sit closer to what follows them than the
   others did.

   A project answers when the pointer is anywhere on the card; a lone
   heading answers to itself. */
/* A heading of the column itself, not a project's, which sits inside its
   own article and stays tight to its number. A plate carries a top margin
   and no bottom one, so a heading following a picture had nothing between
   them at all: measured at minus fourteen pixels, the title overlapping the
   image. The figure is the same 1.8rem the skills headings already use. */
.column > h3 { margin-block-start: 1.8rem; }
/* block with a shrink-to-fit width, not inline-block. Both hug the text,
   which the background rule needs, but an inline-block sits on a baseline
   inside an anonymous line box, and that box swallowed part of the margin:
   the same heading measured 46 pixels below a paragraph and 15 below a
   picture. A block keeps its margins whatever precedes it. */
.column h3, .column .lead {
  display: block;
  inline-size: fit-content;
  background-image: linear-gradient(var(--red), var(--red));
  background-repeat: no-repeat;
  background-position: 0 100%;
  background-size: 0 2px;
  transition: background-size .32s ease;
}
.column h3:hover, .column .lead:hover,
.project:hover h3, .project:focus-within h3 { background-size: 100% 2px; }
.project:hover .project-no, .project:focus-within .project-no { color: var(--red); }
.project-no { transition: color .2s; }
@media (prefers-reduced-motion: reduce) { .column h3, .column .lead { transition: none; } }

/* ── Arriving ────────────────────────────────────────────────────
   Fourteen pixels and half a second, once, when the block first comes into
   view. Only opacity and transform, which the compositor owns: no layout is
   touched, so relief.js is never asked to press a slab again.

   The hiding is keyed on `.arrive`, a class nothing in the markup carries:
   it is put there by the very script that takes it away again. A stylesheet
   that hides on `[data-reveal]` would depend on a script it cannot see, and
   any failure in that script, an engine without the observer, an exception
   three lines earlier, would leave a block of the reading permanently
   invisible. Here the two halves cannot come apart. */
.arrive {
  opacity: 0;
  transform: translateY(14px);
  transition: opacity .55s ease, transform .55s ease;
}
.arrive.is-in { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  .arrive { opacity: 1; transform: none; transition: none; }
}
.project-no {
  /* --red on --ink is 3.41:1 and fails at this size, so the number is set
     in --faded (7.47:1) and the red does its work on the rules instead. */
  color: var(--ink-soft);
  font-family: var(--font-m);
  font-size: 12px;
  letter-spacing: .14em;
  display: block;
  margin-block-end: .35rem;
}
.beat { margin-block-start: 1.1rem; }
.beat-k {
  color: var(--ink-soft);
  display: block;
  margin-block-end: .2rem;
}
.beat p { margin-block-end: 0; }

/* ── Lists ────────────────────────────────────────────────────── */
.list { list-style: none; margin: 0; padding: 0; }
.list li {
  padding-block: .8rem;
  border-block-start: 1px solid var(--line);
}
.list li:first-child { border-block-start: 0; }
.li-k { display: block; color: var(--ink-soft); margin-block-end: .15rem; }
.li-v { display: block; }

/* Two grammars used to share these columns: a project opened with a title
   in ink, while a year of study opened with nothing but a grey date and a
   hairline. Same rank, two different signals, and the eye had to learn both.
   So a row whose key is only a date names its subject in the first words of
   its value, and those words take the weight a project title has. Rows whose
   key is already a word, Français, Recherche, Courriel, LaikaSuite, need
   none of this: their accroche is the key itself.

   The span is chosen by each dictionary, not by the markup, because the
   school's name does not fall in the same place in every language. */
.lead {
  display: block;
  font-family: var(--font-d);
  font-weight: var(--font-d-weight);
  text-transform: uppercase;
  letter-spacing: .01em;
  line-height: 1.02;
  color: var(--ink);
}
.column .lead { font-size: clamp(1.15rem, 2.8vw, 1.5rem); margin-block-end: .3rem; }

/* ── Outward links ───────────────────────────────────────────────
   A whole row is the target, not three words in the middle of it: these
   send the reader to another site, and a large target is what makes that
   an invitation rather than a trap. The arrow says the page will change,
   which the underline alone does not. */
.ext { display: block; text-decoration: none; color: inherit; }
.ext .li-k { display: flex; align-items: center; gap: 7px; transition: color .2s; }
.ext-go { font-size: .82em; opacity: .55; transition: transform .2s, opacity .2s; }
.ext:hover .li-k, .ext:focus-visible .li-k { color: var(--red); }
.ext:hover .ext-go, .ext:focus-visible .ext-go { opacity: 1; transform: translate(2px, -2px); }
.ext:focus-visible { outline: var(--rule-strong) solid var(--ink); outline-offset: 4px; }
@media (prefers-reduced-motion: reduce) {
  .ext-go { transition: none; }
  .ext:hover .ext-go { transform: none; }
}

.tags { list-style: none; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: .45rem; }
.tags li {
  font-family: var(--font-m);
  font-size: 12px;
  letter-spacing: .08em;
  border: 1px solid var(--line);
  padding: .3rem .6rem;
}

/* ── Photo plates ─────────────────────────────────────────────────
   Real black files at the final pixel size, so dropping a photograph in
   place of one moves nothing on the page. Each carries its own filename
   and dimensions, which is what a replacement needs to know. */
/* <figure> carries margin-inline: 40px from the UA stylesheet. */
/* ── The plates ──────────────────────────────────────────────────
   A photograph with its caption held back until it is asked for. The
   caption is not printed under the picture, where it would compete with
   the reading; it comes up ON the picture, over a wash that runs from
   nothing at the top to the site's own ink at the bottom, and it is set in
   paper, 15.88:1 on that ink at the point where the words sit.

   The wash covers the whole plate rather than a band at the foot: a band
   with a hard upper edge reads as a label stuck on, a wash reads as the
   picture dimming to be spoken over. */
.plate {
  position: relative;
  margin: 1.6rem 0 0;
  overflow: hidden;
}
.plate img {
  width: 100%;
  height: auto;
  display: block;
  background: var(--ink);
  /* The cartoon edge. Two pixels of solid ink, which is --rule-strong in
     the LaikaSuite hub's design system, read from its live stylesheet,
     not guessed. */
  border: var(--rule-strong) solid var(--ink);
}
.plate figcaption {
  position: absolute;
  inset: var(--rule-strong);
  /* Two lines now, the caption and the source under it, so the box stacks
     from the bottom instead of aligning a single line to it. */
  display: flex;
  flex-direction: column;
  align-items: stretch;
  justify-content: flex-end;
  padding: clamp(.7rem, 2vw, 1.2rem);
  font-family: var(--font-m);
  font-size: 11.5px;
  line-height: 1.45;
  letter-spacing: .04em;
  color: var(--paper);
  background: linear-gradient(to top,
    rgba(22, 19, 14, .92) 0%,
    rgba(22, 19, 14, .74) 34%,
    rgba(22, 19, 14, .28) 68%,
    rgba(22, 19, 14, 0) 100%);
  opacity: 0;
  transition: opacity .28s cubic-bezier(.16, .84, .3, 1);
}
/* focus-within, not only hover: a plate reached by the keyboard has to
   give up its caption too. */
.plate:hover figcaption,
.plate:focus-within figcaption { opacity: 1; }
/* A reader who asked for stillness gets the caption without the fade. */
@media (prefers-reduced-motion: reduce) {
  .plate figcaption { transition: none; }
}
/* Nothing hovers on a touch screen: there the caption is simply shown. */
@media (hover: none) {
  .plate figcaption { opacity: 1; }
}

.plate-row {
  display: grid;
  /* Two, not three. The row carries four plates now, and three columns
     left the fourth alone on a line of its own. Two columns also give each
     picture about 255 pixels instead of 127, twice the definition, on a
     page where every plate is a photograph and no longer a black slab. */
  grid-template-columns: repeat(2, 1fr);
  gap: .9rem;
  margin-block-start: 1.6rem;
}
.plate-row .plate { margin-block: 0; }

/* A mark, not a picture. Shown at the full width of the column it would read
   as an illustration of something; at three hundred pixels it reads as what
   it is, the sign the studio signs with. */
.plate--mark { max-inline-size: 300px; }

/* A tall photograph at the column's full width is 690 pixels of picture for
   518 of reading, it stops being an illustration and becomes a page of its
   own. Held to 380, it keeps its proportion and its place. */
.plate--tall { max-inline-size: 380px; }
/* Narrower again than `plate--tall`, for the one poster on the page. At 380
   it competed with the four game stills under it; at 300 it leads them, which
   is the order it should be read in: the cover, then what is inside. */
.plate--poster { max-inline-size: 300px; }
.plate-row figcaption { font-size: 9.5px; letter-spacing: .02em; }

/* Where the photograph comes from, under what it shows. A picture credit is
   not decoration: three of these images are the author's own, the rest were
   published by a school, a university or a stranger on Wikipedia, and each
   is named here rather than in a single sentence at the foot of the page
   that no one reads next to the image it is about. Quieter than the caption
   it follows, and never quiet enough to be unreadable: paper at seventy-two
   hundredths over the darkest end of the scrim measures 8.4:1. */
.plate .credit {
  margin-block-start: .45em;
  font-size: .88em;
  letter-spacing: .02em;
  opacity: .72;
}
@media (max-width: 560px) {
  /* A 900 × 1600 phone screenshot at full column width would be a 1200-pixel
   black column. It keeps its proportions inside a narrower box instead. */
.plate-tall { max-width: 300px; }
.plate-row { grid-template-columns: 1fr; }
}

/* ── Contact and foot ─────────────────────────────────────────── */
.contact-a { color: var(--ink); }
.print-only { display: none; }
/* ── Footer, astralaika's own ────────────────────────────────────
   Ink plate, a heavy red rule above it, the mark on the inline start and
   the links pushed to the end. The closing line is the house's, reversed:
   astralaika did not make this page, the person on it made astralaika. */
/* ── The colophon ────────────────────────────────────────────────
   LaikaSuite's, read from its own stylesheet rather than remembered: three
   tiers, the brand line, one column of reading, the edition line, each
   separated from the next by a single hairline of paper at 16%, on solid
   ink, inside 1180 pixels.

   Their six pixels of red at the top are gone, and their own comment gives
   the reason: the ground is enough. Solid ink under a paper column already
   separates the two; a red band between them added no information. */
.footer {
  position: relative;
  z-index: 2;
  background: var(--ink);
  color: var(--paper);
  padding: clamp(24px, 4vh, 40px) clamp(20px, 4.5vw, 64px) clamp(18px, 3vh, 26px);
}
/* The three tiers share one measure and one centre. .footer-grid was the
   name of the block this middle tier replaced; leaving it here left the
   colophon with no maximum and no centring at all, so it ran wider than
   the two rules above and below it. */
.footer-inner,
.footer-colophon,
.footer-foot { max-width: 1180px; margin-inline: auto; }

.footer-inner {
  display: flex;
  align-items: center;
  gap: 26px;
  flex-wrap: wrap;
  margin-block-end: clamp(24px, 4vh, 44px);
  padding-block-end: 20px;
  border-block-end: var(--rule, 1px) solid rgba(243, 237, 224, .16);
}
.brand { display: flex; align-items: center; gap: 12px; text-decoration: none; color: var(--paper); }
/* Astralaika's red, the same #C8321E the bar draws it in on paper. It is a
   mark, not a piece of reading, so the 3.41:1 it measures on ink is a
   contrast figure that does not apply to it. */
.brand-logo { inline-size: 34px; block-size: 34px; display: block; color: var(--red); }
.brand-name { font-family: var(--font-d); font-size: 15px; letter-spacing: .02em; }

.footer-links { display: flex; gap: 2px 20px; flex-wrap: wrap; margin-inline-start: auto; min-inline-size: 0; }
.footer-links a {
  /* Paper at three-quarter opacity, which is the studio's recipe and its
     mechanism both, not a pre-mixed muted colour. */
  color: var(--paper);
  opacity: .75;
  font-size: 12.5px;
  letter-spacing: .15em;
  text-transform: uppercase;
  text-decoration: none;
  transition: color .2s, opacity .2s;
}
.footer-links a:hover, .footer-links a:focus-visible { opacity: 1; color: var(--gold); }

/* The colophon. astralaika's register, mono, 12.5px, 0.1em of tracking,
   paper pulled back, and three columns of it rather than one line, which
   is what holds a 1180-pixel measure without shouting. Everything stated
   here is checkable in the source. */
.footer-colophon {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(230px, 1fr));
  gap: clamp(18px, 2.6vw, 40px);
  font-size: 12.5px;
  line-height: 1.7;
}
.footer-colophon p { margin: 0; max-width: 40ch; }
.fc-k {
  letter-spacing: .15em;
  text-transform: uppercase;
  color: var(--paper);
  opacity: .55;
  margin-block-end: 4px !important;
}
/* Lower case. The .mono class capitalises, which is right for a label and
   wrong for a sentence, astralaika's own footer note keeps its sentence in
   lower case for the same reason. */
.fc-v { color: var(--faded); letter-spacing: .02em; text-transform: none; }   /* 8.04:1 on ink */

.footer-foot { max-width: 1180px; margin-inline: auto; }

.footer-inner {
  display: flex;
  align-items: center;
  gap: 26px;
  flex-wrap: wrap;
  margin-block-end: clamp(24px, 4vh, 44px);
  padding-block-end: 20px;
  border-block-end: var(--rule, 1px) solid rgba(243, 237, 224, .16);
}
.brand { display: flex; align-items: center; gap: 12px; text-decoration: none; color: var(--paper); }
/* Astralaika's red, the same #C8321E the bar draws it in on paper. It is a
   mark, not a piece of reading, so the 3.41:1 it measures on ink is a
   contrast figure that does not apply to it. */
.brand-logo { inline-size: 34px; block-size: 34px; display: block; color: var(--red); }
.brand-name { font-family: var(--font-d); font-size: 15px; letter-spacing: .02em; }

.footer-links { display: flex; gap: 2px 20px; flex-wrap: wrap; margin-inline-start: auto; min-inline-size: 0; }
.footer-links a {
  /* Paper at three-quarter opacity, which is the studio's recipe and its
     mechanism both, not a pre-mixed muted colour. */
  color: var(--paper);
  opacity: .75;
  font-size: 12.5px;
  letter-spacing: .15em;
  text-transform: uppercase;
  text-decoration: none;
  transition: color .2s, opacity .2s;
}
.footer-links a:hover, .footer-links a:focus-visible { opacity: 1; color: var(--gold); }

/* One block of reading, and it is the only claim this page can make about
   itself that the source settles. */
.footer-grid .col { max-width: 62ch; }
.col-tag {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 10px;
  font-size: 12.5px;
  letter-spacing: .15em;
  text-transform: uppercase;
  color: var(--paper);
}
.col-star { inline-size: 11px; block-size: 11px; flex: none; color: var(--red); }
.col-claim {
  margin: 0 0 10px;
  font-size: clamp(1.15rem, 2.4vw, 1.6rem);
  line-height: 1.25;
  color: var(--paper);
}
.col-body {
  margin: 0;
  color: var(--faded);            /* 8.04:1 on ink */
  font-size: .95rem;
  line-height: 1.65;
}

.footer-foot {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: clamp(16px, 2vw, 32px);
  margin-block-start: clamp(28px, 4vh, 52px);
  padding-block-start: 20px;
  border-block-start: var(--rule, 1px) solid rgba(243, 237, 224, .16);
}
/* Against the start edge, including when the band wraps, two blocks on
   two rows want the same edge, not opposite ones. */
.footer-house { display: flex; flex-wrap: wrap; align-items: center; gap: clamp(12px, 1.6vw, 24px); margin-inline-end: auto; min-inline-size: 0; }
.signature {
  display: inline-flex;
  align-items: center;
  gap: 9px;
  color: var(--faded);
  font-family: var(--font-m);
  font-size: 12.5px;
  letter-spacing: .06em;
  text-decoration: none;
  transition: color .2s;
}
.signature img { display: block; flex: none; }
.signature:hover, .signature:focus-visible { color: var(--paper); }

.footer-legal-nav { display: flex; flex-wrap: wrap; gap: 4px 16px; align-items: center; }
.footer-legal {
  color: var(--faded);
  font-size: 12.5px;
  letter-spacing: .06em;
  text-decoration: none;
  transition: color .2s;
}
.footer-legal:hover, .footer-legal:focus-visible { color: var(--gold); }

/* ── The astralaika plate ────────────────────────────────────────
   The notched button of the house, declined for the ink ground of the
   footer: the outer box is the paper edge, the inset plate is the ink the
   footer already sits on, and the label is paper, 15.88:1. The site's
   own .btn fills that plate with paper, which on ink would be a slab of
   white at the foot of every page. */
/* ── The CV page ──────────────────────────────────────────────── */
.cv-page { padding-block: clamp(2rem, 6vh, 4rem); }
.cv-tools {
  max-width: 210mm;
  margin-inline: auto;
  padding: .6rem 1rem;
  background: var(--paper-2);
  display: flex;
  flex-wrap: wrap;
  gap: .8rem;
  align-items: center;
  margin-block-end: 1.2rem;
}
/* The outline variant, and the reason it is painted rather than bordered:
   `border` draws on the BOX and `clip-path` cuts afterwards, so the corner's
   diagonal would carry no line at all and the top edge would stop in mid-air.
   The outline is therefore an ink plate notched at 14px with a paper plate
   notched at 12px inset two pixels over it, the diagonal keeps its stroke
   like the four sides. astralaika hit the same problem and solved it the
   same way. */
.btn {
  position: relative;
  background: var(--ink);
  color: var(--ink);
  cursor: pointer;
}
.btn::before {
  content: "";
  position: absolute;
  inset: 2px;
  background: var(--paper);
  clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 0 100%);
  transition: background .15s;
}
/* The label has to be a positioned element of its own: an absolutely
   positioned ::before paints after in-flow inline content, so a bare text
   node would end up underneath the plate. */
.btn > span { position: relative; }
.btn:hover, .btn:focus-visible {
  color: var(--paper);
  transform: translate(-2px, -2px);
  box-shadow: 4px 4px 0 var(--red);
}
.btn:hover::before, .btn:focus-visible::before { background: var(--ink); }
.btn-solid { background: var(--red); color: var(--paper); }
.btn-solid::before { display: none; }
.btn-solid:hover, .btn-solid:focus-visible {
  background: var(--red-dark);
  color: var(--paper);
  box-shadow: 4px 4px 0 var(--ink);
}
/* Gold has no contrast on paper: on the sheet the focus ring turns red. */
.sheet :focus-visible { outline-color: var(--red-dark); }
.notice {
  max-width: 210mm;
  margin-inline: auto;
  padding: .8rem 1rem;
  background: var(--paper-2);
  border-inline-start: 2px solid var(--red);
  color: var(--ink-soft);
  font-family: var(--font-m);
  font-size: 12px;
  letter-spacing: .05em;
  line-height: 1.8;
  margin-block-end: 1.4rem;
}

/* The sheet is measured in millimetres so that what is read is what prints. */
.sheet {
  width: 210mm;
  min-height: 297mm;
  margin-inline: auto;
  background: var(--paper);
  color: var(--ink);
  padding: 12mm 14mm;
  display: grid;
  grid-template-columns: 58mm 1fr;
  column-gap: 9mm;
  align-content: start;
  /* The measure that fits one A4, applied on screen as well as on paper.
     It was a print-only compromise, which meant the page and the download
     were two different documents; the sheet is one document, and what is
     read is what prints. */
  font-size: 8.9pt;
  line-height: 1.24;
  /* The cartoon edge of the whole object, and the hard offset shadow that
     goes with it. Neither is printed: on paper the sheet IS the edge. */
  border: 2px solid var(--ink);
  box-shadow: 10px 10px 0 var(--red-dark);
}
/* ── The band ────────────────────────────────────────────────────
   A strip of the portfolio's clay across the whole top of the sheet. It
   was a block filling 62% of the width, with the role line floating in
   the paper beside it, a corner that leaves a hole is not a composition.
   The corner it is cut at is the paper's own.

   The two ways of reaching him live in here rather than in the side
   column: it is the first thing a recruiter looks for and the last thing
   that should be hunted for. */
.sheet-band {
  position: relative;          /* the reference box for the relief canvas */
  grid-column: 1 / -1;
  margin: -12mm -14mm 7mm;
  padding: 5mm 14mm 4mm;
  background: var(--red);
  color: var(--paper);
  overflow: hidden;
  /* The cartoon edge, on the one side that is not the paper's own. */
  border-block-end: .8mm solid var(--ink);
  /* Text and photograph, side by side. The right half of the band was
     empty and the photograph was a black rectangle alone at the top of a
     column; one grid settles both. */
  display: grid;
  grid-template-columns: 1fr 35mm;
  column-gap: 9mm;
  align-items: start;
}
.sheet-band-text { min-inline-size: 0; }
.sheet-band > *:not(canvas) { position: relative; z-index: 1; }
.sheet-band .chip {
  font-size: 7pt;
  letter-spacing: .16em;
  padding: 1mm 2.2mm;
  margin: 0 0 4mm;
}
/* Paper on red is 4.58:1, and this is display type far above the
   large-text threshold. It is not debossed: the generator refuses type
   under 68px because below that the walls of the impression are wider
   than the strokes of the letter, the 23's own sheet leaves its name
   flat for exactly this reason. */
.sheet-band h1 {
  color: var(--paper);
  font-size: 28pt;
  line-height: 1;
  margin: 0;
}
/* Not uppercased. The 23 sets its role in caps and can afford to, it is
   four words. This one is a full line of specialisation, and caps plus
   letterspacing over that length is a wall, not a subtitle. */
/* The monospaced face is named here rather than borrowed from the .mono
   utility: that class also uppercases, and fighting a utility with an
   override is how a stylesheet stops being readable. */
.sheet-role {
  font-family: var(--font-m);
  font-size: 8pt;
  letter-spacing: .05em;
  color: var(--paper);
  margin: 4.5mm 0 0;
  max-width: 118mm;
  line-height: 1.5;
}
.sheet-reach {
  margin-block-start: 4mm;
  display: flex;
  flex-wrap: wrap;
  gap: 1.5mm 7mm;
  font-size: 8.5pt;
}
.sheet-reach a, .sheet-reach span {
  color: var(--paper);
  text-decoration: underline;
  text-underline-offset: .8mm;
  text-decoration-thickness: .2mm;
}

/* Ten solid ink plates down one sheet is not a hierarchy, it is a fence.
   The 23's own heading: small caps in --red-dark (6.63:1 on paper), on a
   hairline of red. It carries at a glance and it lets the reading breathe, and it costs almost nothing to print. */
.sheet h2 {
  line-height: 1.35;           /* a two-line heading must clear its own rule */
  color: var(--red-dark);
  background: none;
  font-family: var(--font-m);
  font-size: 8pt;
  letter-spacing: .14em;
  text-transform: uppercase;
  font-weight: 400;
  display: block;
  padding: 0 0 1.2mm;
  margin: 0 0 2.2mm;
  border-block-end: .35mm solid var(--red);
}

/* Not run-in. Making the title inline and the paragraph after it inline
   merged all six projects into one continuous wall of text, a cure worse
   than the complaint. They stay separate items; what they needed was
   tighter spacing, not to be welded together. */
.sheet-main h3 { margin-block: 1.2mm .3mm; }
.sheet-main h3:first-of-type { margin-block-start: 0; }
/* The display face is spent once on this sheet, on the name in the red
   band, and nowhere else: twelve entry names in Anton read as twelve shouts
   down one column. What the names take instead is the reading face at its
   bold weight, one treatment for a project, a school and an employer
   alike, since they are the same rank. With no highlighter left on the
   page, this is the only emphasis the sheet has, which is reason enough to
   spend it on the names and on nothing else. */
.sheet h3, .sheet .lead {
  color: var(--ink);
  font-family: var(--font-b);
  font-weight: 700;
  font-size: 9pt;
  letter-spacing: 0;
  text-transform: none;
  line-height: 1.15;
  break-after: avoid;
}
.sheet h3 {
  margin-block: 1.1mm 0;
}
.sheet h3:first-of-type { margin-block-start: 0; }
.sheet .lead { margin-block: .2mm 0; }
.entries h3 {
  border-block-start: 1px solid var(--line);
  padding-block-start: .55mm;
  margin-block-start: .55mm;
}
.entries h3:first-of-type {
  border-block-start: 0;
  padding-block-start: 0;
  margin-block-start: 0;
}
.sheet p { margin: 0 0 .75mm; max-width: none; }
.sheet .mono { font-size: 8pt; letter-spacing: .09em; }
.sheet .todo { margin-block: 0 1.2mm; }
.sheet .todo-tag { font-size: 8pt; letter-spacing: .09em; padding: .3mm 1.2mm; margin-inline-end: 1.4mm; }
.sheet-block { margin-block-end: 1.2mm; }
.sheet-block:last-child { margin-block-end: 0; }
.sheet .list li { border-block-start-color: var(--line); padding-block: .45mm; }
.sheet .li-k { color: var(--ink-soft); }   /* 5.9:1 on paper */
.sheet .tags li { border-color: var(--line); font-size: 8.5pt; padding: .6mm 1.6mm; }
.sheet .todo-text { color: var(--ink); }
/* ── The field, borrowed by the sheet ──────────────────────────────
   Two bounded bands carry the portfolio's own drawing onto the CV, so a
   printed page visibly belongs to the site it came from. They sit behind
   the type and never under a paragraph, and they are lighter still on a
   sheet that has to go through somebody's printer. */
/* The name has to sit above its own band. */

/* ── The photograph ────────────────────────────────────────────────
   35 × 45 mm, the size a French CV photograph is printed at. The plate
   states its own filename and size; the caption disappears when printed,
   because it is a note to the owner, not part of the document. */
/* On the clay now, not on the paper. It keeps the passport format a
   European recruiter expects, and carries the same ink edge as every
   other plate on the site. */
/* ── No photograph on an English sheet ──────────────────────────
   A photograph on a curriculum vitae is expected in France, Germany,
   Spain and Italy, and is a liability in the English-speaking world: a
   great many recruiters there discard an application that carries one
   rather than expose themselves to a claim of discrimination, and the
   convention is cultural as much as legal. The sheet drops it when the
   page is read in English, and the band then gives the name the whole
   measure instead of leaving a hole where the picture was.

   The language is an approximation for the convention, and a coarse one:
   a Dutch or Indian reader may well be reading the English version. It
   errs on the safe side, which is the right side to err on — a missing
   photograph costs nothing anywhere, an unwanted one costs the whole
   application in five countries.

   The portfolio keeps its portrait in every language. A personal site is
   not a curriculum vitae, and nobody screens one. */
:root[lang="en"] .sheet-band { grid-template-columns: 1fr; }
:root[lang="en"] .cv-photo { display: none; }

.cv-photo { margin: 0; }
.cv-photo img {
  /* 35 × 45 mm, le format d'une photo de CV en France. Il avait été
     ramené à 34 mm pour gagner de la place sur la page : la photo devenait
     presque carrée, et le format normalisé se voit tout de suite sur un
     CV. La place se prend ailleurs. */
  inline-size: 35mm;
  block-size: 45mm;
  object-fit: cover;
  display: block;
  background: var(--ink);
  /* The same cartoon edge as every plate on the site, in the sheet's own
     unit: 2px at 96dpi is 0.53mm. */
  border: .53mm solid var(--ink);
}

.sheet-aside { border-inline-end: 1px solid var(--line); padding-inline-end: 7mm; }
.sheet-main { min-width: 0; }

@media (max-width: 240mm) {
  .sheet {
    width: 100%;
    max-width: 210mm;
    min-height: 0;
    padding: 7mm;
    grid-template-columns: 1fr;
    row-gap: 6mm;
  }
  .sheet-aside { border-inline-end: 0; padding-inline-end: 0; }
}

/* ── Print ────────────────────────────────────────────────────── */
@page { size: A4; margin: 0; }
@media print {
  /* Everything that belongs to the web page and not to the document. */
  .field, .bar, .cv-tools, .notice, .skip, .footer { display: none !important; }
  /* Printing the portfolio itself would otherwise put seven solid black
     plates and a column of empty ground on paper. */
  .plate, .plate-row, .void { display: none !important; }
  .column { background: none; border: 0; padding-inline: 0; }
  html, body {
    background: var(--paper);
    color: var(--ink);
    font-size: 10pt;
    /* Without this the browser drops the background and the seven
       millimetres of page below the sheet come out white. The sheet
       carries the property too, but it does not travel upwards. */
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
  }
  .cv-page { padding: 0; }

  .sheet {
    width: 210mm;
    /* min-height, never height. A definite height on a grid container makes
       its automatic rows share that height instead of taking their content
       size: measured, the head band fell from 234px to 102px and its own
       `overflow: hidden` sliced the name in half, on the printed page and
       in the downloaded PDF, while the screen was perfectly fine.

       And 290mm, not 297: the sheet was exactly as tall as the page it sits
       on, so a single pixel of rounding tipped it onto a second one. */
    min-height: 290mm;
    margin: 0;
    /* Less at the foot than at the head. Measured: the sheet came to 1137px
       against a page of 1123, fourteen pixels, and they produced a second,
       entirely blank page in the PDF. The column that overran is the side
       one, not the reading; six millimetres at the bottom buys twenty-three
       and nothing on the sheet moves. */
    padding: 12mm 14mm 2mm;
    grid-template-columns: 56mm 1fr;
    background: var(--paper);
    /* Without this the browser drops every background: the red band, the
       chips and the photograph's plate would all print as nothing. */
    -webkit-print-color-adjust: exact;
    print-color-adjust: exact;
    break-after: avoid;
    overflow: hidden;
    /* On paper the sheet IS the page. An edge drawn at the very border of
       the paper falls in the printer's non-printable margin and comes out
       looking like a fault, and a cast shadow has nothing to fall on. */
    border: 0;
    box-shadow: none;
  }

  /* ── The relief is a screen effect ────────────────────────────────
     On paper the name has to be text. A pressed name is a picture, and a
     recruiter's filter that cannot read a name rejects the CV outright,
     measured: with the canvas printed, the extracted text of this sheet
     began at "CURRICULUM VITÆ" and the name was simply absent.

     Hiding the canvas puts every slab back into the state the page has
     when no script runs: flat clay, real type. That is not a fallback
     invented here, it is the state the whole generator is built to leave
     the document in, and it costs a printer a great deal less ink. */
  [data-relief-type].is-cut,
  [data-relief-svg].is-cut {
    opacity: 1 !important;
    color: transparent !important;
    -webkit-text-fill-color: transparent !important;
  }

  /* The two details a recruiter needs on paper and that have no business on
     a public web page. */
  .print-only { display: block; }
  a { text-decoration: none; color: var(--ink); }
}


/* ── Reduced motion ───────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
  /* The field is still drawn, it is an image, not a movement. Only the
     scroll-driven displacement stops. */
  .field canvas,
  .cue::after { animation: none; }
}
