/* Mehdi Khalile , personal CV site. Warm paper, near-black ink, one electric blue. */

:root{
  --paper:#F2F1EC;
  --panel:#FFFFFF;
  --ink:#121211;
  --ink-2:#63625B;
  --ink-3:#8E8D84;
  --line:#E0DED4;
  --line-2:#EBE9E1;
  --accent:#2B4FFF;
  --accent-soft:rgba(43,79,255,.10);
  --glow:rgba(43,79,255,.11);
  --shadow:0 1px 2px rgba(18,18,17,.04),0 14px 36px -20px rgba(18,18,17,.26);

  --display:"Space Grotesk","Segoe UI",system-ui,sans-serif;
  --sans:"Instrument Sans","Segoe UI",system-ui,-apple-system,sans-serif;
  --mono:"JetBrains Mono",ui-monospace,"SFMono-Regular",Menlo,monospace;

  /* How far the figure falls back when the torch is not on it. No grayscale:
     he keeps his colour and only loses light. Two values, because that means
     opposite operations on the two grounds. On paper, lowering the luminance
     would make him a dark silhouette and more present, not less, so the light
     theme fades him toward the page instead. */
  --dim:1;--dimf:none;

  --ease:cubic-bezier(.22,1,.36,1);
  /* The page is a fixed panel and a scrolling column beside it. --side-w is the
     panel, --col is what is left, and --shell is measured against --col rather
     than the viewport: at 90vw the content was wider than the column it sits in
     and ran under the panel. Below 981 the panel stops being a column at all
     and all three fall back to the full width. */
  --rail-w:0px;
  --side-w:0px;
  --col:100vw;
  --shell:min(1280px,90vw);
  --bar-h:64px;
}

:root[data-theme="dark"]{
  --paper:#0C0C0B;
  --panel:#161614;
  --ink:#F3F2EC;
  --ink-2:#A2A199;
  --ink-3:#77766E;
  --line:#2A2A26;
  --line-2:#201F1D;
  --accent:#7E96FF;
  --accent-soft:rgba(126,150,255,.14);
  --glow:rgba(126,150,255,.13);
  --shadow:0 1px 2px rgba(0,0,0,.4),0 18px 44px -24px rgba(0,0,0,.85);
  /* brightness(.34) was the unlit half of the pull-cord effect. The cord is
     gone and the portrait is the panel, so a third-brightness photograph is
     just a dark photograph. */
  --dim:1;--dimf:none;
  color-scheme:dark;
}

*{margin:0;padding:0;box-sizing:border-box}
html{-webkit-text-size-adjust:100%;scroll-behavior:smooth;scroll-padding-top:calc(var(--bar-h) + 24px)}
body{
  font-family:var(--sans);font-size:17px;line-height:1.62;font-weight:400;
  background:var(--paper);color:var(--ink);
  -webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;
  overflow-x:hidden;
}
img{display:block;max-width:100%;height:auto}
a{color:inherit;text-decoration:none}
ul{list-style:none}
button{font:inherit;color:inherit;background:none;border:0;cursor:pointer}
::selection{background:var(--accent);color:#fff}
:focus-visible{outline:2px solid var(--accent);outline-offset:3px;border-radius:2px}

.shell{width:var(--shell);margin-inline:auto}
.mono{font-family:var(--mono);font-weight:500;font-size:.688rem;letter-spacing:.12em;text-transform:uppercase}
.dot{display:inline-block;width:3px;height:3px;border-radius:50%;background:currentColor;vertical-align:.28em;margin:0 .8em;opacity:.5}

/*  scroll progress  */
.progress{position:fixed;top:0;left:0;right:0;height:2px;z-index:60;pointer-events:none}
.progress i{display:block;height:100%;width:0;background:var(--accent)}

/*  top bar  */
.bar{
  position:fixed;top:0;right:0;z-index:50;height:var(--bar-h);
  display:flex;align-items:center;gap:24px;
  left:var(--side-w);
  padding-inline:max(24px,calc((var(--col) - var(--shell)) / 2));
  /* Opaque, not a backdrop-filter. A blur over the whole bar is re-rasterised
     on every scrolled frame and was the single most expensive thing on the
     page; the bar is a solid strip of the page colour and nobody can tell. */
  background:var(--paper);
  border-bottom:1px solid transparent;
  transition:border-color .3s var(--ease);
}
.bar.stuck{border-bottom-color:var(--line)}

.monogram{
  display:grid;place-items:center;width:34px;height:34px;flex:none;
  border:1px solid var(--line);border-radius:8px;font-size:.7rem;letter-spacing:.06em;
  transition:border-color .25s,color .25s,background .25s;
}
.monogram:hover{border-color:var(--accent);color:var(--accent);background:var(--accent-soft)}

.bar-nav{display:flex;gap:26px;margin-inline:auto}
.bn-i svg{width:100%;height:100%;fill:none;stroke:currentColor;stroke-width:1.6;
  stroke-linecap:round;stroke-linejoin:round}
.bn-i{display:none}
.bar-nav a{position:relative;font-size:.875rem;color:var(--ink-2);transition:color .25s var(--ease)}
.bar-nav a::after{
  content:"";position:absolute;left:0;right:0;bottom:-7px;height:1.5px;
  background:var(--accent);transform:scaleX(0);transform-origin:0 50%;
  transition:transform .32s var(--ease);
}
.bar-nav a:hover,.bar-nav a.on{color:var(--ink)}
.bar-nav a.on::after{transform:scaleX(1)}

.bar-tools{display:flex;align-items:center;gap:10px;margin-left:auto}

/*  menu button and overlay 
    The bar below 920 was a monogram, two tools and a filled CTA pressed edge to
    edge, and the five section links had simply gone: display:none with nothing
    put in their place. The CTA leaves the bar at that width, the button takes
    its slot, and both the links and the ask move into the overlay, so the bar
    is a monogram and three small controls at every size. */
/* The same pill as the language and theme controls. Bare text beside three
   bordered controls read as a fourth thing of a different kind, and it is the
   most important of the four on a phone. */
.menu-btn{
  display:none;align-items:center;height:34px;padding:0 12px;cursor:pointer;
  background:none;border:1px solid var(--line);border-radius:8px;color:var(--ink-2);
  font-size:.688rem;transition:border-color .25s,color .25s,background .25s;
}
.menu-btn:hover{border-color:var(--ink-3);color:var(--ink)}

/* Inverted: --ink and --paper swap with the theme, so this is a dark panel on
   the light page and a light one on the dark page without a second rule. */
.ovl{
  position:fixed;inset:0;z-index:45;background:var(--ink);color:var(--paper);
  display:flex;align-items:center;visibility:hidden;
  clip-path:inset(0 0 100% 0);
  transition:clip-path .62s var(--ease),visibility 0s .62s;
}
body.menu .ovl{
  clip-path:inset(0 0 0 0);visibility:visible;
  transition:clip-path .62s var(--ease);
}
.ovl-in{
  width:var(--shell);margin-inline:auto;
  display:grid;grid-template-columns:1.15fr .85fr;gap:clamp(2.4rem,6vw,6rem);
  align-items:center;
}
.ovl-links{display:flex;flex-direction:column;gap:2px}
.ovl-links a{
  display:block;overflow:hidden;padding:.22em 0;color:inherit;
  font-family:var(--display);font-weight:500;letter-spacing:-.026em;line-height:1.16;
  font-size:clamp(1.8rem,7vw,3.2rem);
}
.ovl-links a span{display:block;transform:translateY(115%);transition:transform .62s var(--ease)}
body.menu .ovl-links a span{transform:none}
body.menu .ovl-links a:nth-child(1) span{transition-delay:.14s}
body.menu .ovl-links a:nth-child(2) span{transition-delay:.20s}
body.menu .ovl-links a:nth-child(3) span{transition-delay:.26s}
body.menu .ovl-links a:nth-child(4) span{transition-delay:.32s}
body.menu .ovl-links a:nth-child(5) span{transition-delay:.38s}
.ovl-links a:hover span{color:var(--accent)}

.ovl-side{display:flex;flex-direction:column;align-items:flex-start;gap:4px}
.ovl-lab{opacity:.5;margin-bottom:12px}
.ovl-mail{color:inherit;font-size:1.05rem;padding:.2rem 0;border-bottom:1px solid transparent}
.ovl-mail:hover{border-bottom-color:var(--accent);color:var(--accent)}
.ovl-sec{color:inherit;opacity:.62;padding:.2rem 0;font-size:.95rem}
.ovl-sec:hover{opacity:1;color:var(--accent)}
.ovl-loc{opacity:.42;margin-top:14px}

/* The bar sits above the overlay so its own button can close it, which means
   the bar has to stop painting its strip of page colour the moment it does. */
body.menu .bar{background:transparent;border-bottom-color:transparent}
body.menu .bar .monogram,body.menu .bar .tool,body.menu .menu-btn{
  color:var(--paper);border-color:rgba(128,128,128,.42);
}
body.menu .bar .cta{display:none}
body.lock{overflow:hidden}
/* Only while the nav is actually in the bar. display:none does not stop an
   element being an adjacent sibling, so on mobile this went on cancelling the
   tools' auto margin and left them bunched against the monogram with 126px of
   empty bar to their right. */
@media (min-width:921px){ .bar-nav + .bar-tools{margin-left:0} }

.tool{
  display:flex;align-items:center;gap:7px;height:34px;padding:0 11px;
  border:1px solid var(--line);border-radius:8px;color:var(--ink-2);
  transition:border-color .25s,color .25s,background .25s;
}
.tool:hover{border-color:var(--ink-3);color:var(--ink)}
.tool .off{opacity:.42}
.tool-sep{width:1px;height:12px;background:var(--line);flex:none}
.tool-ico{padding:0;width:34px;justify-content:center}

.cta{
  display:inline-flex;align-items:center;height:34px;padding:0 15px;
  border-radius:8px;background:var(--ink);color:var(--paper);
  font-size:.82rem;font-weight:600;letter-spacing:-.005em;white-space:nowrap;
  transition:transform .25s var(--ease),background .25s;
}
.cta:hover{transform:translateY(-1px);background:var(--accent);color:#fff}

/*  hero 
   The figure is a cutout standing at the right edge, lit from behind and
   running off the bottom of the section rather than sitting in a frame. A
   portrait boxed in its own column beside the text is the layout every CV
   template ships with. */
.hero{
  position:relative;min-height:100svh;display:flex;align-items:center;
  padding:calc(var(--bar-h) + 5vh) 0 12vh;overflow:hidden;isolation:isolate;
}
.hero::before{
  content:"";position:absolute;z-index:0;right:-6vw;top:-10vh;width:74vw;height:120vh;
  background:radial-gradient(46% 42% at 62% 45%,var(--glow),transparent 72%);
  pointer-events:none;
}
/* Top is set against the bar rather than as a percentage of the hero, or a
   short viewport crops the top of his head. */
.figure{
  position:absolute;z-index:1;right:calc((100vw - var(--shell)) / 2 - 3vw);
  top:calc(var(--bar-h) + 10px);bottom:0;
  display:flex;align-items:flex-end;pointer-events:none;
}
.figure picture{display:contents}
.figure img{height:100%;width:auto;max-width:none}
.figure img.gone{display:none}

/* The figure rests dim and a spotlight above him turns on and off from a pull
   cord. Two copies of the cutout: the base, held back, and a full one shown
   through a mask that falls off downward, so the light reads as coming from
   over his head rather than from everywhere at once. */
.fig-base{display:block;height:100%}
.fig-base img{opacity:var(--dim);filter:var(--dimf)}

.torch{
  display:none;
  position:absolute;inset:0;pointer-events:none;opacity:0;
  transition:opacity .6s var(--ease);
  -webkit-mask-image:radial-gradient(125% 88% at 50% -8%,#000 0%,#000 40%,rgba(0,0,0,.42) 74%,transparent 96%);
          mask-image:radial-gradient(125% 88% at 50% -8%,#000 0%,#000 40%,rgba(0,0,0,.42) 74%,transparent 96%);
}
.torch img{height:100%;width:auto;max-width:none}
.lit-on .torch{opacity:1}

/* The beam itself. A cone narrowing to a point above the frame, so the source
   is off screen and only its light is in it. */
/* The light itself, as a soft pool falling from a source above the frame rather
   than a drawn cone. A clip-path cone has hard edges and reads as a grey
   polygon lying on the page; light has no outline. Two stops: a tight one at
   the source, near where the cord hangs, and a wide one over him. */
.beam{
  position:absolute;inset:0;z-index:0;pointer-events:none;opacity:0;
  transition:opacity .6s var(--ease);
  background:
    radial-gradient(30% 26% at 57% -6%, rgba(255,236,196,.20), transparent 70%),
    radial-gradient(52% 82% at 68% -14%, rgba(255,232,188,.13), transparent 68%);
}
:root[data-theme="dark"] .beam{
  background:
    radial-gradient(30% 26% at 57% -6%, rgba(255,240,205,.30), transparent 72%),
    radial-gradient(56% 88% at 68% -14%, rgba(255,236,196,.16), transparent 70%);
}
.lit-on .beam{opacity:1}


/* The cord. Two linked segments, not one rigid stick: the upper one carries the
   swing and the lower one follows a beat behind, so the line visibly bends at
   the join. The bead lives inside the lower segment, which is why it stays on
   the end of its own line. Rotating the bead alone around a point above itself
   also trailed, but it slid off the wire and hung in mid air. */
.cord{
  /* Hangs from just under the bar, not from the very top: half a cord behind an
     opaque header reads as a rendering fault, not as a ceiling. */
  position:absolute;z-index:6;top:calc(var(--bar-h) - 6px);left:56%;
  width:38px;height:280px;
  padding:0;background:none;border:0;cursor:pointer;
  transform-origin:50% 0;
}
/* Zero-size anchors: each segment is only a pivot, and its line hangs off it. */
.seg{position:absolute;left:50%;top:0;width:0;height:0}
.seg > i{position:absolute;left:-.5px;top:0;width:1px;background:var(--ink-3);opacity:.7}
.seg-up > i{height:134px}
.seg-low{top:134px;transform-origin:50% 0}
.seg-low > i{height:88px}
.seg-low > b{
  position:absolute;left:-5.5px;top:82px;width:11px;height:26px;border-radius:6px;
  background:var(--ink-3);
  transition:background .25s var(--ease),height .25s var(--ease);
}
.cord:hover b,.cord:focus-visible b{background:var(--accent);height:30px}
.cord:focus-visible{outline-offset:2px}

/* The invitation. It says what to do once and never again: after the first pull
   the cord has taught its own lesson. */
.cord-hint{
  position:absolute;right:calc(100% + 10px);top:196px;
  color:var(--ink-3);font-size:.6rem;white-space:nowrap;
  opacity:0;animation:hintIn .8s var(--ease) 1.9s forwards;
}
@keyframes hintIn{to{opacity:.9}}
.cord.used .cord-hint{animation:none;opacity:0;transition:opacity .4s var(--ease)}

/* Until it has been pulled once it breathes, so a still cord does not read as a
   drawn line. It stops for good on the first pull. */
@keyframes sway{
  0%,100%{transform:rotate(-1.1deg)}
  50%    {transform:rotate(1.1deg)}
}
.cord:not(.pull):not(.used){animation:sway 5.2s ease-in-out infinite}
.cord:not(.pull):not(.used):hover{animation-play-state:paused}

/* A damped pendulum: a short downward tug, then arcs that each lose about a
   quarter of the last. ease-in-out between the keyframes, because a pendulum is
   slowest at the ends of its travel and fastest through the middle; a single
   ease over the whole animation flattens all of that into a slide. */
@keyframes pull{
  0%  {transform:rotate(0) translateY(0)}
  5%  {transform:rotate(0) translateY(15px)}
  12% {transform:rotate(-1.5deg) translateY(0)}
  20% {transform:rotate(-9.6deg)}
  31% {transform:rotate(7.3deg)}
  42% {transform:rotate(-5.4deg)}
  53% {transform:rotate(3.9deg)}
  63% {transform:rotate(-2.8deg)}
  73% {transform:rotate(2deg)}
  82% {transform:rotate(-1.3deg)}
  90% {transform:rotate(.8deg)}
  96% {transform:rotate(-.4deg)}
  100%{transform:rotate(0)}
}
/* The lower segment, at a third of the amplitude and a beat behind. */
@keyframes lag{
  0%  {transform:rotate(0)}
  8%  {transform:rotate(0)}
  22% {transform:rotate(-3.4deg)}
  33% {transform:rotate(2.6deg)}
  44% {transform:rotate(-1.9deg)}
  55% {transform:rotate(1.4deg)}
  65% {transform:rotate(-1deg)}
  75% {transform:rotate(.6deg)}
  85% {transform:rotate(-.35deg)}
  100%{transform:rotate(0)}
}
.cord.pull{animation:pull 2.8s ease-in-out}
.cord.pull .seg-low{animation:lag 2.9s ease-in-out .07s}

/* No drop-shadow. On a 639x1250 cutout that is a per-frame filter pass while
   the figure is being parallaxed, and it is what made the hero scroll stutter.
   The radial glow behind it already separates the figure from the ground. */

/*  the standing panel 
    Fixed, so the portrait holds still and the CV moves past it. The window is
    still the scroll container, which is the whole reason this is position:fixed
    and not a pair of independently scrolling panes: every ScrollTrigger on the
    page and the bar's own progress read window.scrollY. */
.side{
  position:fixed;left:0;top:0;bottom:0;width:var(--side-w);z-index:40;
  overflow:hidden;background:var(--panel);
  display:flex;flex-direction:column;justify-content:flex-end;
}
.side-media{position:absolute;inset:0;z-index:0}
/* Out-positions the hero rules the figure used to carry. inset:0 is the whole
   override: a right:auto after it undoes the right edge inset:0 just set, and
   an absolutely positioned block with only a left edge and absolutely
   positioned children shrinks to nothing. It measured 0x900.
   The side inset is the margin the photograph stands in, so it never runs into
   the seam where the panel meets the column. */
.side .figure{
  /* Edge to edge. The margin and the contained fit left a band of empty panel
     around the photograph, which read as blankness rather than as space. */
  position:absolute;inset:0;display:block;
}
.side .fig-base{position:absolute;inset:0;height:auto}
/* cover now that the photograph has its own room around him. It crops 53px of
   width at 1440 and 70px of height at 1900, against the cutout's legs-off crop
   that made cover wrong in the first place. The anchor sits above centre so
   what a tall narrow panel loses is floor rather than face. */
.side .figure img{
  position:absolute;inset:0;width:100%;height:100%;max-width:none;
  object-fit:cover;object-position:center 22%;
}
/* The name sits on the photograph, so it needs its own ground under it. */
/* z-index 2, because the base .figure rule carries z-index:1 and this override
   did not reset it: at z-index auto the veil painted UNDERNEATH the portrait
   and the name sat on a bare photograph. Measured white-on-background contrast
   at 1.02:1 before this line. */
.side-veil{
  position:absolute;inset:0;z-index:2;pointer-events:none;
  background:linear-gradient(to top,rgba(8,8,7,.92) 0%,rgba(8,8,7,.66) 26%,
             rgba(8,8,7,.12) 52%,transparent 70%);
}
.side-id{color:#F7F6F1}
.side-name{
  font-family:var(--display);font-weight:700;letter-spacing:-.04em;line-height:1;
  font-size:clamp(1.7rem,2.5vw,2.5rem);
}
/* Each word is a window with the word inside it, so the reveal is a rise out of
   the type's own edge rather than a fade. The padding is what stops the mask
   cutting a descender; the equal negative margin gives the space back, or the
   name sits .12em off the role under it. */
.side-name .sn-w{
  display:inline-block;overflow:hidden;vertical-align:bottom;
  padding-bottom:.12em;margin-bottom:-.12em;
}
.side-name .sn-w > span{display:inline-block;will-change:transform}
/* A masthead device, and it only exists where the name is a masthead. On a
   phone the name is one centred line in the middle of the hero and a rule under
   it would be a third horizontal thing in a stack that already has the kicker
   and the role. */
.sn-rule{display:none}
.side-role{margin-top:10px;color:var(--accent)}

/* The two ways to reach him, as the services' own app icons. Word labels were
   naming something a logo already names, and a pill around a mark that carries
   its own tile is two containers for one thing. */
.side-reach{display:flex;flex-direction:column;align-items:flex-end;gap:11px}
.reach-lab{color:rgba(247,246,241,.62)}
.side-links{display:flex;gap:12px;margin:0}
.app{
  position:relative;display:grid;place-items:center;
  width:52px;height:52px;flex:none;color:#F7F6F1;
  /* 22.4% is the iOS ratio, and as a percentage it holds at any tile size. */
  border-radius:23%;
  /* Glass, not a fill: it is standing on a photograph and a flat panel would
     read as a sticker on it. The gradient alone is the whole appearance where
     backdrop-filter is unsupported, so nothing depends on the blur. */
  background:linear-gradient(rgba(255,255,255,.17),rgba(255,255,255,.055));
  -webkit-backdrop-filter:blur(12px) saturate(150%);
  backdrop-filter:blur(12px) saturate(150%);
  /* Off the photograph, not cut into it: a hairline to seat it, a tight shadow
     for the contact edge and a wide soft one for the lift. Both the tile and
     the panel are fixed, so this backdrop never moves and never re-rasterises
     on scroll. */
  box-shadow:0 0 0 .5px rgba(0,0,0,.34),
             0 2px 4px rgba(0,0,0,.3),
             0 8px 20px rgba(0,0,0,.4);
  transition:transform .3s var(--ease),box-shadow .3s var(--ease),
             background .3s var(--ease);
}
.app .ico{width:25px;height:25px}
/* The lit top edge has to be painted OVER the glyph: an inset shadow on the
   anchor is drawn under its own children. */
.app::after{
  content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;
  box-shadow:inset 0 1px 0 rgba(255,255,255,.34),
             inset 0 0 0 .5px rgba(255,255,255,.14);
}
.app:hover,.app:focus-visible{
  transform:translateY(-3px) scale(1.05);
  background:linear-gradient(rgba(255,255,255,.26),rgba(255,255,255,.1));
  box-shadow:0 0 0 .5px rgba(0,0,0,.34),
             0 4px 8px rgba(0,0,0,.32),
             0 14px 30px rgba(0,0,0,.46);
}
.app:active{transform:translateY(-1px) scale(.96);transition-duration:.09s}

/*  the ring 
    Desktop has no use for it: there the whole panel is the photograph. */
.avatar{display:none}

.hero-in{position:relative;z-index:2}

.kicker{color:var(--ink-2);display:flex;align-items:center;flex-wrap:wrap}
.live{
  display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--accent);
  margin-right:.85em;flex:none;
  animation:pulse 2.6s var(--ease) infinite;
}
@keyframes pulse{
  0%{box-shadow:0 0 0 0 var(--accent-soft)}
  70%{box-shadow:0 0 0 9px transparent}
  100%{box-shadow:0 0 0 0 transparent}
}

.name{
  font-family:var(--display);font-weight:700;
  font-size:clamp(3.9rem,13.1vw,11.4rem);
  line-height:.84;letter-spacing:-.048em;
  margin:clamp(16px,2.6vw,30px) 0 clamp(20px,2.6vw,30px);
}
/* Each line masks its own reveal, so the type slides out from behind a hard
   edge rather than fading in place. */
.name .line{display:block;overflow:hidden;padding-bottom:.06em}
.name .line > span{display:block}
.name .n2{
  color:transparent;-webkit-text-stroke:1.5px var(--ink);padding-left:.055em;
}
@supports not (-webkit-text-stroke: 1px black){ .name .n2{color:var(--ink)} }

.hero-h{
  font-family:var(--display);font-weight:700;letter-spacing:-.042em;line-height:.94;
  font-size:clamp(2.6rem,6vw,5.2rem);margin-bottom:clamp(18px,2vw,26px);
}
.role{
  color:var(--accent);letter-spacing:.2em;
  display:flex;align-items:center;gap:14px;
}
.role::after{content:"";height:1px;width:clamp(40px,7vw,110px);background:var(--accent);opacity:.45}

.lede{
  margin-top:22px;font-size:clamp(1.02rem,1.28vw,1.18rem);line-height:1.6;
  color:var(--ink-2);max-width:38ch;
}
html[lang="fr"] .lede{max-width:42ch}

.hero-act{display:flex;gap:12px;flex-wrap:wrap;margin-top:34px}


.btn{
  display:inline-flex;align-items:center;height:46px;padding:0 24px;border-radius:999px;
  border:1px solid var(--line);font-size:.94rem;font-weight:500;background:var(--paper);
  transition:border-color .25s,background .25s,color .25s,transform .25s var(--ease);
}
.btn:hover{transform:translateY(-2px);border-color:var(--ink-3)}

/* Buttons that carry an icon. The icon animates on hover only: a permanent
   loop on two things sitting next to each other reads as a page that has not
   finished loading. */
.btn-ico{gap:11px;padding-left:20px}
.ico{width:18px;height:18px;flex:none;overflow:visible}
/* SVG children default to the viewBox origin for transforms, so every rotation
   would pivot from the corner of the canvas. fill-box makes the shape its own
   reference. */
.ico *{transform-box:fill-box}

/* Four things set an icon going, and every rule below carries the same pair of
   selectors: a hero button, a footer icon link or a contact row that is a link,
   under pointer or keyboard focus; and a contact row as it arrives on screen,
   which app.js marks `.on` with a stagger. Entry and hover therefore play the
   same gesture, and each gesture is written once.

   The flap. A V under the top edge, flipped to a lambda: the envelope opens. */
.ico-mail .flap{
  transform-origin:50% 0;
  transition:transform .42s var(--ease);
}
:is(.btn-ico,.soc,.app,.reach li:has(a)):is(:hover,:focus-visible) .ico-mail .flap,
.reach li.on .ico-mail .flap{
  transform:scaleY(-1);
}

/* LinkedIn. The box holds still and the three parts of the glyph arrive inside
   it, the dot first because it is the one that can pop rather than rise. */
:is(.btn-ico,.soc,.app,.reach li:has(a)):is(:hover,:focus-visible) .ico-li .li-d,
.reach li.on .ico-li .li-d{animation:liPop .5s var(--ease)}
:is(.btn-ico,.soc,.app,.reach li:has(a)):is(:hover,:focus-visible) .ico-li .li-i,
.reach li.on .ico-li .li-i{animation:liRise .5s var(--ease) .06s both}
:is(.btn-ico,.soc,.app,.reach li:has(a)):is(:hover,:focus-visible) .ico-li .li-n,
.reach li.on .ico-li .li-n{animation:liRise .5s var(--ease) .14s both}
@keyframes liPop{0%{transform:scale(.2)}62%{transform:scale(1.3)}100%{transform:scale(1)}}
@keyframes liRise{from{transform:translateY(50%);opacity:0}to{transform:none;opacity:1}}

/* The pin drops onto its own shadow, which swells to meet it. The shadow is
   there at rest as well, or the icon changes shape rather than moves. */
.ico-pin .pin-sh{opacity:.22}
:is(.btn-ico,.soc,.reach li:has(a)):is(:hover,:focus-visible) .ico-pin .pin-b,
.reach li.on .ico-pin .pin-b{animation:pinDrop .62s var(--ease)}
:is(.btn-ico,.soc,.reach li:has(a)):is(:hover,:focus-visible) .ico-pin .pin-sh,
.reach li.on .ico-pin .pin-sh{animation:pinShade .62s var(--ease)}
@keyframes pinDrop{0%{transform:translateY(-40%)}62%{transform:translateY(5%)}100%{transform:none}}
@keyframes pinShade{0%{opacity:0;transform:scaleX(.35)}58%{opacity:.4;transform:scaleX(1.06)}100%{opacity:.22;transform:none}}

/* One sweep of the hour. transform-box is view-box here, not the fill-box every
   other icon child uses: a hand is a straight line, its fill box has zero width,
   and a percentage origin on a zero dimension cannot find the dial's centre. */
.ico-clock .ck-m,.ico-clock .ck-h{transform-box:view-box;transform-origin:12px 12px}
:is(.btn-ico,.soc,.reach li:has(a)):is(:hover,:focus-visible) .ico-clock .ck-m,
.reach li.on .ico-clock .ck-m{animation:sweep 1.05s var(--ease)}
:is(.btn-ico,.soc,.reach li:has(a)):is(:hover,:focus-visible) .ico-clock .ck-h,
.reach li.on .ico-clock .ck-h{animation:sweepH 1.05s var(--ease)}
@keyframes sweep{from{transform:rotate(0)}to{transform:rotate(360deg)}}
@keyframes sweepH{from{transform:rotate(0)}to{transform:rotate(90deg)}}
.btn-solid{background:var(--accent);border-color:var(--accent);color:#fff}
.btn-solid:hover{border-color:var(--accent);filter:brightness(1.08)}

.scroller{
  position:absolute;z-index:2;left:max(24px,calc((var(--col) - var(--shell)) / 2));
  bottom:30px;color:var(--ink-3);display:flex;align-items:center;gap:10px;
}
.scroller::after{
  content:"";width:1px;height:34px;background:currentColor;
  transform-origin:50% 0;animation:drop 2.4s var(--ease) infinite;
}
@keyframes drop{0%,100%{transform:scaleY(.25);opacity:.35}50%{transform:scaleY(1);opacity:1}}

/*  reveal  */
.js .rv{opacity:0;transform:translateY(22px)}

/*  sections  */
/* No rule between sections, Mehdi 2026-08-21. The space does the separating on
   its own, and a hairline across the page at every join is the same statement
   made five times. What is left of the line vocabulary is the one beside a
   heading, which belongs to that heading rather than dividing the page. */
.sec{position:relative;padding:clamp(60px,8vw,116px) 0}

/*  the ground under the column 
    Flat colour behind every band read as one slide after another. Two layers,
    both cheap and both static: a wash of the accent that moves corner to corner
    between sections so no two are lit the same way, and a grain tile over it so
    the surface has a texture instead of being a fill. The tile is an inline SVG
    data URI, so it costs no request and nothing to cache. */
.sec::before{
  content:"";position:absolute;inset:0;z-index:0;pointer-events:none;
  background:radial-gradient(52% 42% at 84% 8%,var(--glow),transparent 72%);
}
.sec:nth-of-type(even)::before{
  background:radial-gradient(48% 40% at 10% 88%,var(--glow),transparent 72%);
}
.hero::after,.sec::after{
  content:"";position:absolute;inset:0;z-index:0;pointer-events:none;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
  opacity:.05;
}
/* The pseudo-elements are positioned and the content is not, so without this the
   grain paints over the words rather than under them. */
.sec > .shell,.sec > .signoff,.sec-end > .shell{position:relative;z-index:1}
/* Print has no use for a texture and a wash, and both cost ink. */
@media print{
  .sec::before,.sec::after,.hero::after{display:none}
}

/* The section straight after the hero runs inverted, so the page opens light,
   turns over once, and comes back. It is a local override of the same variables
   everything else reads, which is why nothing inside it needs its own rule: the
   text, the hairlines, the accent and the panels all follow.
   The accent swaps too. #2B4FFF on near-black is 3.1:1 and unreadable at the
   sizes it is used at here. */
#profile{
  --paper:#121211;--panel:#1A1A18;
  --ink:#F3F2EC;--ink-2:#A2A199;--ink-3:#77766E;
  --line:#2C2C28;--line-2:#232320;
  --accent:#7E96FF;--accent-soft:rgba(126,150,255,.14);
  --shadow:0 1px 2px rgba(0,0,0,.4),0 18px 44px -24px rgba(0,0,0,.85);
  background:var(--paper);color:var(--ink);
}
/* The light inversion is gone. #profile's own block above already carries the
   dark values, so with nothing flipping them the section matches the rest of
   the page. */
/* One column, always. It was 240px of sticky heading beside the body, which
   worked when the body had the whole page. Beside a 38vw panel the body came
   out at 516px of a 1600px screen and the three project cards landed at 160px
   each with their titles overflowing. The heading sits above its section. */
.sec-grid{display:grid;grid-template-columns:minmax(0,1fr);gap:clamp(20px,2.4vw,32px)}
/* The same head as the wide sections: the title on the left and a hairline
   running out to the right of it. Centred, the rule grew on both sides and the
   two head styles on one page no longer matched. */
.sec-head{
  position:static;display:flex;align-items:baseline;
  gap:clamp(14px,2vw,26px);flex-wrap:nowrap;
}
/* The numeral is set in outline, the same device the surname uses in the hero.
   One idea stated twice is an identity; stated once it is a decoration. */
.sec-head .lab{display:block;color:var(--ink-3);margin-bottom:14px}
.sec-head .num{
  display:block;margin-bottom:0;font-family:var(--display);font-weight:700;
  font-size:clamp(2.8rem,4.4vw,4rem);line-height:.9;letter-spacing:-.04em;
  color:transparent;-webkit-text-stroke:1.2px var(--accent);margin-bottom:6px;
}
@supports not (-webkit-text-stroke: 1px black){ .sec-head .num{color:var(--accent)} }
.sec-head h2{
  font-family:var(--display);font-weight:500;
  font-size:clamp(1.6rem,2.7vw,2.25rem);line-height:1.06;letter-spacing:-.03em;
}
.sec-head .rule{
  flex:1 1 120px;height:1px;background:var(--line);align-self:center;
  transform:scaleX(0);transform-origin:0 50%;
}
.sec-body > * + *{margin-top:22px}

/*  wide sections 
   Profile, Skills and Contact do not use the two-column grid. They were three
   copies of one skeleton, and a page where every section is built the same way
   has no shape. Journey and Projects keep the grid, so it still reads as the
   spine rather than as the only idea. */
.sec-wide .shell > * + *{margin-top:clamp(30px,4vw,54px)}
.wide-head{
  display:flex;align-items:baseline;
  gap:clamp(14px,2vw,26px);flex-wrap:nowrap;
}
.wide-head .lab{color:var(--ink-3)}
.wide-head .num{
  font-family:var(--display);font-weight:700;font-size:clamp(2.2rem,3.4vw,3.1rem);
  line-height:1;letter-spacing:-.04em;color:transparent;
  -webkit-text-stroke:1.2px var(--accent);
}
@supports not (-webkit-text-stroke: 1px black){ .wide-head .num{color:var(--accent)} }
.wide-head h2{
  font-family:var(--display);font-weight:500;letter-spacing:-.03em;line-height:1;
  font-size:clamp(1.6rem,2.7vw,2.25rem);
}
.wide-head .rule{
  flex:1 1 120px;height:1px;background:var(--line);align-self:center;
  transform:scaleX(0);transform-origin:0 50%;
}

/* The statement is the section. app.js cuts it into words and scrubs their
   opacity against the scroll, so it is read at the pace it is scrolled rather
   than arriving all at once. It rests at .34 rather than near zero: any lower
   and a screenshot taken mid scroll looks like text that failed to load. */
.statement{
  font-family:var(--display);font-weight:500;
  font-size:clamp(1.42rem,3vw,2.6rem);line-height:1.18;letter-spacing:-.03em;
  max-width:31ch;color:var(--ink);
}
.statement .w{display:inline-block;white-space:pre}
.js .statement .w{opacity:.34}
html[lang="fr"] .statement{max-width:33ch}

/*  the strengths
    Four of them, drawn down a spine that fills with the scroll. This was the
    method until 2026-08-20, four process steps, and a process is what a firm
    sells rather than what a recruiter is hiring. The drawing is unchanged and
    the mt- classes are shared with the facts column beside it, so they keep
    their names; only this block's own class moved. */
.strengths{position:relative;padding-left:clamp(28px,4vw,64px)}
.mt-lab{color:var(--ink-3);margin-bottom:clamp(20px,2.6vw,32px)}

/* The spine sits on the container, not between the items, so its fill is one
   uninterrupted travel rather than four segments meeting at the padding. */
.mt-spine{
  position:absolute;left:0;top:calc(clamp(20px,2.6vw,32px) + 1.4em);bottom:0;
  width:1px;background:var(--line);
  /* The last dot sits near the top of the last step, so the line would otherwise
     run 150px past it and stop at nothing. Masked out instead of measured: the
     tail's length depends on how many lines that step wraps to. */
  -webkit-mask-image:linear-gradient(180deg,#000 78%,transparent 100%);
          mask-image:linear-gradient(180deg,#000 78%,transparent 100%);
}
.mt-spine i{
  position:absolute;inset:0;background:var(--accent);
  transform-origin:50% 0;
}

.mt-i{
  position:relative;display:grid;
  grid-template-columns:auto minmax(0,1fr) auto;
  gap:clamp(18px,2.4vw,34px);align-items:baseline;
  padding:clamp(20px,2.4vw,30px) 0;border-top:1px solid var(--line-2);
}
.mt-i:first-child{border-top:0}

/* Every step is written settled and `.js` is what dims it, the same way .rv
   works. Without JS, and under reduced motion, nothing ever adds `.on`, and a
   block whose resting state is four grey rows and four invisible chips would
   simply be broken there.

   The dot on the spine, which is why the numeral column is a fixed width: at
   auto the four numerals are the same width in English and not in French, and
   the dots would step in and out of the line. */
.mt-n{width:2.4em;color:var(--accent);transition:color .45s var(--ease)}
.mt-i::before{
  content:"";position:absolute;
  left:calc(clamp(28px,4vw,64px) * -1);top:calc(clamp(20px,2.4vw,30px) + .5em);
  width:7px;height:7px;border-radius:50%;margin-left:-3px;
  background:var(--accent);border:1px solid var(--accent);
  transition:background .45s var(--ease),border-color .45s var(--ease),transform .45s var(--ease);
}
.mt-b h3{
  font-family:var(--display);font-weight:500;letter-spacing:-.02em;line-height:1.14;
  font-size:clamp(1.16rem,1.85vw,1.5rem);
  color:var(--ink);transition:color .45s var(--ease);
}
.mt-b p{
  margin-top:9px;color:var(--ink-2);font-size:.99rem;line-height:1.55;max-width:46ch;
  transition:color .45s var(--ease);
}
/* The discipline the strength belongs to, so the four can be scanned down the
   right edge without reading a word of the copy. It arrives from the right as
   the row lights.

   It is nowrap and it sits in its own grid column, so its width comes straight
   off the copy beside it: COMMUNICATION was 160px wide and wrapped the sentence
   under it to four words a line. Nothing longer than about nine characters. */
.mt-out{
  color:var(--ink-2);white-space:nowrap;
  padding:6px 12px;border:1px solid var(--line);border-radius:999px;
  transition:opacity .5s var(--ease),transform .5s var(--ease),color .45s var(--ease);
}

.js .mt-n{color:var(--ink-3)}
.js .mt-i::before{background:var(--paper);border-color:var(--line)}
.js .mt-b h3,.js .mt-b p{color:var(--ink-3)}
.js .mt-out{opacity:0;transform:translateX(14px);color:var(--ink-3)}
.js .mt-spine i{transform:scaleY(0)}

.js .mt-i.on::before{background:var(--accent);border-color:var(--accent);transform:scale(1.28)}
.js .mt-i.on .mt-n{color:var(--accent)}
.js .mt-i.on .mt-b h3{color:var(--ink)}
.js .mt-i.on .mt-b p{color:var(--ink-2)}
.js .mt-i.on .mt-out{opacity:1;transform:none;color:var(--ink-2)}

/*  the profile body
    The facts on the left, the method on the right. They answer each other, what
    he has and how he works, and neither is a paragraph. The left column is the
    narrower of the two: its values are four short strings, the method's steps
    are a heading, a sentence and a chip. */
.profile-body{
  display:grid;grid-template-columns:minmax(0,.82fr) minmax(0,1.18fr);
  gap:clamp(30px,5vw,76px);align-items:start;
}
/* Both columns open on the same line, so their labels share a baseline and the
   two lists start together. The left one is a third the height of the right, so
   it rides down with the reading rather than leaving a hole: the same sticky the
   two-column sections already use for their heads. */
.pb-col{min-width:0}
.pb-col:first-child{position:sticky;top:calc(var(--bar-h) + 34px)}

/* Each fact carries a mark, so the row is an icon column and a stacked pair
   rather than a label column and a value column. The label sat on the value's
   baseline before, which reads as a table; a fact is not a cell. */
.sec-wide .facts{margin:0}
.sec-wide .facts li{
  display:grid;grid-template-columns:auto minmax(0,1fr);gap:18px;
  align-items:center;padding:16px 0;border-bottom:1px solid var(--line-2);
}
.sec-wide .facts .t{color:var(--ink-3)}
.sec-wide .facts .v{font-size:.99rem;max-width:none}
.fx-i{
  display:grid;place-items:center;width:40px;height:40px;border-radius:50%;
  border:1px solid var(--line);color:var(--ink-2);
}
.fx-i .ico{width:19px;height:19px}
.fx-b{display:flex;flex-direction:column;gap:5px;min-width:0}

/* The marks arrive with their rows. The cap's tassel swings, the same gesture
   the hero seal plays, which is the whole reason the same cap is down here. */
.facts li.on .fx-i{animation:fxRing .55s var(--ease)}
.facts li.on .ico-cap .tassel{animation:tSwing .95s var(--ease) .12s}
.facts li.on .ico-globe .gl-m{animation:glTurn .8s var(--ease) .1s}
.facts li.on .ico-aim .am-o{animation:amIn .6s var(--ease) .06s}
.facts li.on .ico-aim .am-i{animation:amIn .6s var(--ease) .16s}
.facts li.on .ico-aim .am-d{animation:amIn .6s var(--ease) .26s}
.facts li.on .ico-pin .pin-b{animation:pinDrop .62s var(--ease) .1s}
.facts li.on .ico-pin .pin-sh{animation:pinShade .62s var(--ease) .1s}
@keyframes fxRing{from{transform:scale(.7);opacity:0}to{transform:none;opacity:1}}
/* fill-box puts the origin on the tassel's own box, whose top centre is exactly
   where the cord meets the board. */
.ico-cap .tassel{transform-origin:50% 0}
@keyframes tSwing{
  0%{transform:rotate(15deg)}
  40%{transform:rotate(-7deg)}
  68%{transform:rotate(3deg)}
  100%{transform:rotate(0)}
}
/* The meridian is a flat oval at the halfway point, so the globe reads as
   turning rather than as a shape being squashed. */
@keyframes glTurn{
  0%{transform:scaleX(1)}
  50%{transform:scaleX(.12)}
  100%{transform:scaleX(1)}
}
@keyframes amIn{from{transform:scale(.35);opacity:0}to{transform:none;opacity:1}}


.big{font-size:clamp(1.14rem,1.85vw,1.5rem);line-height:1.46;letter-spacing:-.012em}
.sec-body p{color:var(--ink-2);max-width:66ch}
/* The lead paragraph is display type, so it takes a shorter measure than the
   body under it. It has to out-specify .sec-body p to get one. */
.sec-body .big{color:var(--ink);max-width:52ch}


/* No bullet glyph, no dash, no hairline under the item. Spacing separates the
   lines and the lead term in a heavier weight carries the eye down the column. */
.lines{display:grid;gap:12px}
.lines li{font-size:.95rem;line-height:1.52;color:var(--ink-2)}
.lines b{font-weight:600;color:var(--ink)}
.lines.tight{gap:9px;margin-top:14px}
.lines.tight li{font-size:.92rem}

/* The mark sits on the page with nothing behind it. A plate is a second shape
   competing with the logo's own, and two logos of different proportions never
   look like they belong in the same one. Each ships in two files instead: the
   dark variant lifts the black ink to the paper colour and leaves the brand
   colours alone, because black artwork on a dark ground with no plate under it
   is invisible. */
.mark{display:block;height:26px}
.mark img{height:100%;width:auto;max-width:none}
/* A mark that is one flat colour ships as inline SVG on var(--ink) instead,
   which is exactly what the dark raster variants are lifted to, and gets the
   print theme's pure black for free. No second file, nothing to keep in step. */
.mark svg{display:block;height:100%;width:auto;fill:var(--ink)}
.mark .dk{display:none}
:root[data-theme="dark"] .mark .lt{display:none}
:root[data-theme="dark"] .mark .dk{display:block}

/*  02 journey 
   A vertical line with the year in the gutter. No pin, no hijack: the section
   scrolls at page speed and the only thing tied to scroll is the accent fill
   growing down the line, which is a transform on one element. */
/* .sec-head is a baseline-aligned row: title, hairline, note. The 18px top
   margin is what dropped the note off that baseline, left over from when the
   head was a stack. */
.jr-note{display:block;color:var(--ink-3);margin-top:0;flex:none;white-space:nowrap}

.tl{position:relative}
/* The rule the whole journey hangs from, and the part already travelled. */
.tl-line{
  position:absolute;left:112px;top:8px;bottom:16px;width:1px;
  background:var(--line);pointer-events:none;
}
.tl-line i{
  display:block;height:100%;background:var(--accent);
  transform:scaleY(0);transform-origin:50% 0;
}

.tl-i{
  position:relative;display:grid;grid-template-columns:112px minmax(0,1fr);
  gap:42px;padding-bottom:44px;
}
.tl-i:last-child{padding-bottom:0}
.tl-y{
  color:var(--ink-3);font-size:.78rem;text-align:right;padding-right:26px;
  padding-top:.28em;transition:color .3s var(--ease);
}
.tl-i::before{
  content:"";position:absolute;left:107px;top:.42em;width:11px;height:11px;
  border-radius:50%;background:var(--paper);border:1px solid var(--line);z-index:2;
  transition:border-color .35s var(--ease),background .35s var(--ease),transform .35s var(--ease);
}
.tl-i.on .tl-y{color:var(--accent)}
.tl-i.on::before{border-color:var(--accent);background:var(--accent);transform:scale(1.3)}

.tl-c{min-width:0}
.tl-top{display:flex;align-items:center;justify-content:space-between;gap:20px;margin-bottom:12px}
.tl-i .kind{display:inline-block;color:var(--ink-3);margin-bottom:12px;
  transition:color .3s var(--ease)}
.tl-top .kind{margin-bottom:0}
.tl-i.on .kind{color:var(--accent)}
.tl-i h3{
  font-family:var(--display);font-weight:500;letter-spacing:-.024em;line-height:1.16;
  font-size:clamp(1.12rem,1.7vw,1.42rem);
}
.tl-i .who{margin-top:7px;font-size:.92rem;color:var(--ink-3)}
.tl-i .who a{border-bottom:1px solid var(--line)}
.tl-i .who a:hover{color:var(--accent);border-color:var(--accent)}
.tl-i .say{margin-top:14px;font-size:.95rem;line-height:1.55;color:var(--ink-2);max-width:52ch}
.tl-i .lines li{font-size:.94rem;line-height:1.55}
.tl-cta{margin-top:22px;height:44px;padding:0 22px;font-size:.9rem}

/* The work entries are the substance, so they carry a surface. The panel is
   .tl-c and not .tl-i, so the line and the node stay outside it. */
.is-work .tl-c{
  background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:22px 24px;
  transition:box-shadow .35s var(--ease),border-color .35s var(--ease);
}
.is-work:hover .tl-c,.is-work:focus-within .tl-c,.is-work.on .tl-c{
  box-shadow:var(--shadow);border-color:var(--accent);
}
/* The two degrees are achievements, so they carry a surface of their own: an
   accent wash rather than the work cards' panel, so the line reads as two kinds
   of thing rather than one kind repeated. The Baccalaureate stays plain text.
   Three awards on one line is none. */
.is-degree .tl-c{
  background:var(--accent-soft);border:1px solid transparent;border-radius:14px;
  padding:20px 22px;
  transition:border-color .35s var(--ease),box-shadow .35s var(--ease);
}
.is-degree.on .tl-c{border-color:var(--accent);box-shadow:var(--shadow)}
.is-degree .kind{color:var(--accent)}
.is-degree h3{font-size:clamp(1.18rem,1.9vw,1.55rem)}

.seal{display:block;width:30px;height:30px;color:var(--accent);flex:none}
.seal svg{width:100%;height:100%;overflow:visible}
.seal svg *{transform-box:fill-box;transform-origin:50% 50%}
/* It pops once, as the entry becomes the one being read. A medal that sits
   still is a bullet point with a picture on it. */
.is-degree.on .seal svg{animation:seal .7s var(--ease)}
@keyframes seal{
  0%  {transform:scale(1) rotate(0)}
  35% {transform:scale(1.18) rotate(-7deg)}
  62% {transform:scale(.97) rotate(4deg)}
  100%{transform:scale(1) rotate(0)}
}
.is-degree:hover .seal .rib{animation:ribbon .6s var(--ease)}
@keyframes ribbon{
  0%,100%{transform:translateY(0)}
  45%    {transform:translateY(2.2px)}
}

.is-work{padding-bottom:34px}
.is-degree{padding-bottom:34px}
.is-degree .tl-y{padding-top:1.4em}
.is-degree::before{top:1.55em}
.is-work .tl-y{padding-top:1.6em}
.is-work::before{top:1.75em}

.tl-i .hint{
  display:block;margin-top:14px;color:var(--ink-3);opacity:.85;
  transition:opacity .25s var(--ease);
}
.tl-i:hover .hint,.tl-i:focus-within .hint{opacity:0}
.tl-i .more .lines li:first-child{padding-top:6px}
/* A device with no pointer can never satisfy the hover, so it gets the detail
   outright and the invitation to hover is a lie there. */

/*  03 cases 
   Numbered rows on hairlines, not three boxes in a row. A CV lists its work;
   it does not merchandise it. The write-up opens on hover, the same gesture the
   journey uses, so the page has one way of asking for more rather than two. */
/*  03 cases 
    Three cards, the anatomy the engagements deck uses: an outlined numeral, the
    name, the client line in the accent, then the write-up. It was numbered rows
    on hairlines with the write-up folded behind a hover, which is a gesture a
    phone does not have, so the substance of the section was unreachable there.
    A card has nothing to open. */
/* auto-fit, not repeat(3). The column's width is a function of the panel beside
   it, so a fixed three across cannot know whether three will fit. min(100%,270px)
   is the guard: without it the track floors at 270px and overflows a container
   narrower than that. */
.cases{
  border-top:0;display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,270px),1fr));
  gap:clamp(14px,1.6vw,22px);
}
.case{
  position:relative;min-width:0;overflow:hidden;
  display:flex;flex-direction:column;
  padding:clamp(20px,2vw,30px) clamp(18px,1.8vw,28px) clamp(24px,2.4vw,34px);
  background:var(--panel);border:1px solid var(--line);border-radius:14px;
  transition:border-color .4s var(--ease),box-shadow .4s var(--ease),
             transform .4s var(--ease);
}
.case:hover,.case:focus-within{
  border-color:var(--accent);box-shadow:var(--shadow);transform:translateY(-4px);
}
/* In flow at the top rather than set large behind the text. The reference sets
   it bottom right over a photograph, which gives it somewhere to sit; there is
   no image on these cards, so behind the text it would simply collide with the
   last line of the write-up. */
.case-n{
  display:block;font-family:var(--display);font-weight:700;
  font-size:clamp(2.4rem,3.4vw,3.4rem);line-height:.8;letter-spacing:-.05em;
  color:transparent;-webkit-text-stroke:1.4px var(--accent);
  margin-bottom:clamp(14px,1.6vw,20px);
}
@supports not (-webkit-text-stroke: 1px black){ .case-n{color:var(--accent)} }
.case h3{
  font-family:var(--display);font-weight:500;letter-spacing:-.026em;line-height:1.16;
  font-size:clamp(1.12rem,1.5vw,1.45rem);
}
.case .who{margin-top:10px;font-size:.86rem;color:var(--accent)}
.case-p{margin-top:14px;font-size:.94rem;line-height:1.6;color:var(--ink-2)}

/*  04 writing
   One piece at a time, filling the column beside the portrait, paged sideways
   with dots under it.

   It was a strip of part-visible cards and both complaints about it had one
   cause. It opened with a card centred and nothing to its left, and the card
   closing behind you collapsed its text as it went. Showing several at once
   meant the open one had to be marked as different from its neighbours, and
   that marking is what looked wrong while it moved. One card at a time needs
   no open state, so there is nothing left to animate.

   The page is never pinned to do it. The journey was a pinned horizontal rail
   once and it went precisely because holding the viewport for a screen and a
   half is what "the scroll is slow" turned out to mean. */
.wr-wrap{margin-top:clamp(22px,2.6vw,34px)}
.wr-strip{
  display:flex;
  /* Every card is the height of the rail, and the rail is the height of the
     longest piece. Mehdi 2026-08-24, choosing blank space under the shorter
     ones over cards that changed size as he paged.

     It is not only a matter of looks. Sized to their own content the rail had
     to resize as each card arrived, and a scroll container that changes height
     under the finger has its snapping disturbed: this rail slipped into the
     card after the one you asked for, and the skills deck, whose four cards
     are all one height, never did. */
  align-items:stretch;
  /* Butted together the cards read as one long panel sliding past, because the
     only thing between two of them is a pair of hairlines meeting. The gap is
     what makes the next one arrive as a separate object. It does not disturb
     the paging: a card still fills the rail when it is snapped, and go() reads
     offsetLeft, which already counts it. */
  gap:clamp(12px,1.4vw,20px);
  overflow-x:auto;overflow-y:hidden;
  scroll-snap-type:x mandatory;
  /* The dots are the indicator, so the bar is not. It said the same thing
     worse and sat across the foot of a card somebody was reading. Both rules
     are needed: the standard property and the WebKit pseudo-element. */
  scrollbar-width:none;
}
.wr-strip::-webkit-scrollbar{display:none}
.wr-c{
  flex:0 0 100%;width:100%;min-width:0;
  scroll-snap-align:start;scroll-snap-stop:always;
  display:flex;flex-direction:column;
  padding:clamp(22px,2.4vw,34px) clamp(20px,2.2vw,34px);
  background:var(--panel);border:1px solid var(--line);border-radius:14px;
}
.wr-c:focus-visible{outline:2px solid var(--accent);outline-offset:3px}

/* The academic project card's colouring, because they are the same kind of
   object: an outline numeral in the accent, the subject line in the accent
   under the title, and the body in --ink-2. Without it the card was a white
   heading over grey text and was the only panel on the page carrying no accent
   at all.

   The numeral is this section's order and not the order the pieces were
   published, which is why it is allowed here. See writing.mjs. */
.wr-n{
  display:block;font-family:var(--display);font-weight:700;
  font-size:clamp(2rem,2.8vw,2.9rem);line-height:.8;letter-spacing:-.05em;
  color:transparent;-webkit-text-stroke:1.4px var(--accent);
  margin-bottom:clamp(12px,1.4vw,18px);
}
@supports not (-webkit-text-stroke: 1px black){ .wr-n{color:var(--accent)} }
.wr-c h3{
  font-family:var(--display);font-weight:500;letter-spacing:-.026em;line-height:1.16;
  font-size:clamp(1.16rem,1.6vw,1.5rem);
}
.wr-k{margin-top:10px;font-size:.86rem;color:var(--accent)}
.wr-t{margin-top:clamp(12px,1.4vw,18px)}
.wr-t p{margin-top:11px;font-size:.94rem;line-height:1.62;color:var(--ink-2)}
.wr-t p:first-child{margin-top:0}

/* Nothing sets the rail's height any more. align-items:stretch on a flex row
   makes every card the height of the tallest one for free, which is both what
   Mehdi asked for and what stops the rail resizing under a finger. Script
   measuring it was the thing that broke the paging. */

/* Instagram's dots. A count and a position, which is what the scrollbar was
   doing badly, and the only control the section has. */
.wr-dots{
  display:flex;justify-content:center;align-items:center;
  gap:9px;margin-top:clamp(16px,1.8vw,22px);
}
.wr-dot{
  width:7px;height:7px;padding:0;border:0;border-radius:50%;
  background:var(--line-2);cursor:pointer;
  transition:background .3s var(--ease),transform .3s var(--ease);
}
.wr-dot:hover{background:var(--ink-3)}
/* It grows as well as taking the accent, because a colour change on a 7px dot
   says nothing to a reader who cannot separate the two. */
.wr-dot.is-on{background:var(--accent);transform:scale(1.45)}
.wr-dot:focus-visible{outline:2px solid var(--accent);outline-offset:3px}

@media (max-width:760px){
  .wr-dots{gap:8px}
}

/*  05 skills
   Two rows gliding in opposite directions, holding on hover. Each row carries
   its set twice and travels exactly half its own width, which is the only way
   the loop has no seam. app.js sets --dur per row from its measured width so
   both rows move at the same speed whatever language they are in. */
/*  skills
    A deck of four cards side by side, one per group, opened by the pointer. It
    was two rows of pills scrolling in opposite directions, and a moving list
    cannot be read: the eye has to track it instead of scanning it, and the
    grouping only survived as one highlighted chip somewhere in the stream.

    The first card is open at rest and hovering moves the open state rather than
    creating it, so the section always shows one whole group. A deck that opens
    from nothing says four category names and nothing else, which is ten seconds
    of a reader's time spent finding out there is something behind it. */
.skx{
  /* The reset only clears markers off ul, and this is an ol: the groups are
     ordered and their numerals are the largest thing on each card. */
  list-style:none;
  margin-top:clamp(28px,3.4vw,44px);
  display:flex;gap:clamp(10px,1.1vw,16px);
  /* Sized off the open card, which is the tallest thing the deck holds: a French
     name over six terms is 277px at 1920 and 265px at 1024, and the clamp leaves
     each of those about 26px of floor. It came down from clamp(348,26vw,384)
     once the numeral moved out of flow, which left the open card standing on
     100px of nothing.

     Fixed height rather than min-height on purpose: the four lists are different
     heights in French, and letting the tallest set the row makes the whole deck
     jump every time the pointer moves. **If the terms or the type change, check
     the open card in French at 1920 and at 921**: it carries overflow:hidden, so
     it drops its last line silently. */
  height:clamp(306px,22vw,330px);
}
/* The page's own card: --panel on a hairline at 14px, the same as the language
   cards and the work entries in the journey. A fifth surface would be a fifth
   idea. */
.skx-c{
  position:relative;min-width:0;flex:1 1 0;overflow:hidden;
  display:flex;flex-direction:column;
  padding:clamp(18px,1.8vw,26px) clamp(16px,1.7vw,26px);
  background:var(--panel);border:1px solid var(--line);border-radius:14px;
  /* flex-grow is a layout property and this is four boxes, which is cheap; the
     alternative is a transform, and a transform would squash the type inside. */
  transition:flex-grow .5s var(--ease),border-color .4s var(--ease),
             box-shadow .4s var(--ease),background .4s var(--ease);
  outline-offset:-3px;
}

/* The numeral is the closed card's field, not a label above its name: at 3.4rem
   in the corner it left two thirds of a 348px card empty, and no amount of
   spacing makes an empty card look deliberate. Outlined, the device the section
   heads already use, and out of flow so its size costs the open card nothing. */
.skx-n{
  position:absolute;left:clamp(16px,1.7vw,26px);
  bottom:clamp(40px,4.4vw,54px);
  font-family:var(--display);font-weight:700;
  font-size:clamp(5.4rem,7.6vw,8.6rem);line-height:.76;letter-spacing:-.05em;
  color:transparent;-webkit-text-stroke:1.5px var(--ink-3);
  transition:opacity .42s var(--ease),transform .5s var(--ease),
             -webkit-text-stroke-color .4s var(--ease);
}
@supports not (-webkit-text-stroke: 1px black){ .skx-n{color:var(--ink-3)} }

/* Display face at the journey entry's size. At 1.02rem in the sans it was a
   label on a 240px card and the card read as empty around it; the name is the
   card's subject and it is set as one. */
.skx-h{
  position:relative;
  font-family:var(--display);font-weight:500;
  font-size:clamp(1.14rem,1.45vw,1.34rem);letter-spacing:-.022em;line-height:1.18;
  color:var(--ink-2);transition:color .4s var(--ease);
}

/* The one affordance the deck has: nothing else says the cards open. Absolute,
   so it sits on the card's floor without adding to the height the open card has
   to fit its terms into, and it is the same hint the journey and the projects
   already use. */
.skx-c .hint{
  position:absolute;left:clamp(16px,1.7vw,26px);bottom:clamp(16px,1.6vw,22px);
  font-size:.66rem;color:var(--ink-3);white-space:nowrap;
  transition:opacity .3s var(--ease);
}
.skx-c:first-child .hint,.skx-c:hover .hint,.skx-c:focus-within .hint{opacity:0}
.skx:is(:hover,:focus-within) .skx-c:first-child:not(:hover):not(:focus-within) .hint{opacity:1}

/* 0fr to 1fr, so the terms take no height at all when closed rather than being
   merely transparent. The wrapper exists because that trick needs exactly one
   child: grid-template-rows:0fr sets the first row only, and six list items
   create five more implicit rows at auto, so the list stayed its full height and
   the names floated at six different heights across the deck. */
.skx-l{
  display:grid;grid-template-rows:0fr;
  opacity:0;transition:grid-template-rows .5s var(--ease),opacity .38s var(--ease);
}
/* Margin, not zero: a 0fr row zeroes the item's height but a margin sits outside
   its box and still counts, so the closed card kept a gap under its name. The
   space the open list needs lives on its first item instead. */
.skx-l > ul{overflow:hidden;min-height:0;margin:0}
.skx-c ul li{
  padding:8px 0;border-top:1px solid var(--line-2);
  color:var(--ink-2);font-size:.9rem;line-height:1.36;
}
.skx-c ul li:first-child{border-top:0;padding-top:14px}

/*  open  */
/* THE WHOLE DECK MECHANISM IS FENCED INTO THE WIDTHS THAT HAVE A DECK, and
   that fence is the fix for a real bug rather than tidying. The give-up rule
   below is seven classes deep; every mobile reset was three or four, so it won
   everywhere the query did not reach. On a phone the cards take a tab stop, so
   TAPPING one made the deck :focus-within, and card 01, not being the tapped
   one, collapsed to its numeral and heading with no terms under it. That is
   exactly what Mehdi reported when paging back to 01.

   Fencing it beats adding specificity: the two states are now mutually
   exclusive with the block below, which is the same condition inverted, so
   neither has to out-shout the other. The README's note that every reset here
   must out-specify the open state no longer applies. */
@media (hover:hover) and (pointer:fine) and (min-width:921px){
  .skx-c:first-child,.skx-c:hover,.skx-c:focus-visible,.skx-c:focus-within{
    flex-grow:2.5;border-color:var(--accent);box-shadow:var(--shadow);
  }
  /* Open, it stands down: six terms over a 137px outlined glyph is two things
     competing for the same 500px. */
  .skx-c:first-child .skx-n,.skx-c:hover .skx-n,.skx-c:focus-within .skx-n{
    opacity:0;transform:translateY(14px);
  }
  .skx-c:first-child .skx-h,.skx-c:hover .skx-h,.skx-c:focus-within .skx-h{color:var(--ink)}
  .skx-c:first-child .skx-l,.skx-c:hover .skx-l,.skx-c:focus-within .skx-l{grid-template-rows:1fr;opacity:1}

  /* and the first one gives the open state up the moment the pointer or the
     keyboard takes it. :focus-within on the deck as well as :hover, or tabbing
     to a card opens a second one and two cards share the width. */
  .skx:is(:hover,:focus-within) .skx-c:first-child:not(:hover):not(:focus-within){
    flex-grow:1;border-color:var(--line);box-shadow:none;
  }
  .skx:is(:hover,:focus-within) .skx-c:first-child:not(:hover):not(:focus-within) .skx-n{
    opacity:1;transform:none;
  }
  .skx:is(:hover,:focus-within) .skx-c:first-child:not(:hover):not(:focus-within) .skx-h{color:var(--ink-2)}
  .skx:is(:hover,:focus-within) .skx-c:first-child:not(:hover):not(:focus-within) .skx-l{
    grid-template-rows:0fr;opacity:0;
  }
}

/* No pointer, no deck. Hover is the whole mechanism, so where it does not exist
   the four groups become four cards with everything showing. */
/* Every reset here has to out-specify the open state, which is written against
   .skx-c:first-child and wins on specificity whatever the media query says. The
   first card kept the accent border and lost its numeral entirely. */
@media (hover:none),(pointer:coarse),(max-width:920px){
  .skx{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));
       gap:clamp(12px,1.6vw,18px);height:auto}
  .skx .skx-c,.skx .skx-c:first-child{
    flex:none;border-color:var(--line);box-shadow:none;padding:22px;
  }
  /* Every card is open here, so the numeral returns to the corner at label size
     rather than standing behind the terms. */
  .skx .skx-c .skx-n,.skx .skx-c:first-child .skx-n{
    position:static;font-size:2.6rem;line-height:.9;margin-bottom:10px;
    -webkit-text-stroke-color:var(--accent);opacity:1;transform:none;
  }
  .skx .skx-c .skx-h,.skx .skx-c:first-child .skx-h{color:var(--ink)}
  .skx .skx-c .skx-l,.skx .skx-c:first-child .skx-l{grid-template-rows:1fr;opacity:1}
  /* Nothing to hover, and every card is open. */
  .skx-c .hint{display:none}
}
/* Off everywhere but a phone. The deck is a deck on the desktop and two
   columns on a tablet, and a row of dots under either is a control for
   something that is not paging.

   THIS RULE HAS TO SIT ABOVE THE MEDIA QUERY THAT TURNS IT ON. Both are one
   class, so at equal specificity the later one wins whatever the query says,
   and written the other way round the dots are display:none on the one screen
   they exist for. The same mistake is already recorded against .lang and .more
   in this file. */
.skx-dots{
  display:none;justify-content:center;align-items:center;
  gap:9px;margin-top:clamp(16px,4vw,22px);
}

/* On a phone the deck becomes the pager the writing strip uses, Mehdi
   2026-08-24. It was a single column of four tall cards, which is four screens
   of scrolling for something the desktop shows at a glance.

   620px and not the 920px the deck already breaks at: between the two it is
   two columns, and two columns uses that width better than one card at a time
   would. The number is also in app.js and the two have to move together.

   Everything that makes a card open is inherited from the block above, so
   nothing here reopens them. */
@media (max-width:620px){
  .skx{
    display:flex;align-items:stretch;
    gap:clamp(12px,3vw,18px);
    overflow-x:auto;overflow-y:hidden;
    scroll-snap-type:x mandatory;
    scrollbar-width:none;
    grid-template-columns:none;
  }
  .skx::-webkit-scrollbar{display:none}
  .skx .skx-c,.skx .skx-c:first-child{
    flex:0 0 100%;width:100%;min-width:0;
    scroll-snap-align:start;scroll-snap-stop:always;
  }
  .skx-dots{display:flex}
}

/*  languages  */
.langs{margin-top:clamp(34px,4vw,52px)}
.langs .wide-head{margin-bottom:clamp(20px,2.6vw,30px)}
/* Five, not auto-fit: at this width auto-fit lands four and leaves Spanish on
   its own row. */
.lang-grid{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:14px}
@media (max-width:1080px){ .lang-grid{grid-template-columns:repeat(3,minmax(0,1fr))} }
.lang{
  display:flex;flex-direction:column;align-items:flex-start;gap:14px;
  background:var(--panel);border:1px solid var(--line);border-radius:14px;
  padding:22px 22px 24px;
  transition:transform .35s var(--ease),box-shadow .35s var(--ease),border-color .35s var(--ease);
}
.lang:hover{transform:translateY(-3px);box-shadow:var(--shadow);border-color:var(--ink-3)}
.lang .l{font-family:var(--display);font-size:1.14rem;font-weight:500;letter-spacing:-.02em}
.lang .lvl{color:var(--ink-3);font-size:.62rem;line-height:1.5;margin-top:-6px}
/* The proficiency meter, mobile only. A rule that draws to the level as the row
   arrives: the same gesture the timeline, the name and the spine all make, so
   it belongs to the page rather than being a widget dropped into it. The WORD
   is the claim and the bar only echoes it, which is why the bar is never alone.
   scaleX on the compositor, one transition, nothing per frame. */
.lmeter{display:none}


/*  languages on a phone
    One per row rather than a stacked box: flag, name, level at the right edge,
    and the meter under all three. Five tall cards was 137px each of mostly
    empty panel, which is what Mehdi reported.

    THIS BLOCK HAS TO SIT AFTER THE BASE .lang RULES. It was written above them
    and lost the cascade at equal specificity, so `flex-direction:column` won and
    the phone kept the stacked cards the block existed to replace. The same trap
    the .more rule carries a warning about further up this file. */
@media (max-width:620px){
  .lang-grid{grid-template-columns:minmax(0,1fr);gap:10px}
  /* Two columns and three rows: the flag stands beside the name, the level sits
     UNDER the name, the meter runs the full width beneath both.

     The level was out at the right edge on the same line, which works in
     English and collides in French: `MAÎTRISE PROFESSIONNELLE COMPLÈTE` ran
     straight into `Arabe` at 390px. French is the binding case on this page,
     and a level whose length depends on the word is a level that has to own a
     line. */
  .lang{
    display:grid;grid-template-columns:auto minmax(0,1fr);align-items:center;
    column-gap:14px;row-gap:0;padding:14px 16px 15px;border-radius:12px;
  }
  .lang .flag{width:32px;height:21.33px;grid-row:1 / span 2;align-self:center}
  .lang .l{grid-column:2;min-width:0;font-size:1rem;line-height:1.2}
  .lang .lvl{grid-column:2;margin-top:4px;text-align:left;line-height:1.35}
  .lang:hover{transform:none}
  .lmeter{
    display:block;grid-column:1 / -1;margin-top:12px;height:2px;border-radius:2px;
    background:var(--line-2);overflow:hidden;
  }
  .lmeter i{
    display:block;height:100%;background:var(--accent);border-radius:inherit;
    transform:scaleX(0);transform-origin:0 50%;
    transition:transform .95s var(--ease);
  }
  /* --l is set per row in the markup. The stagger is a delay on the transition,
     not five animations: the rows arrive together and fill one after another. */
  .lang.on .lmeter i{transform:scaleX(var(--l,1))}
  .lang:nth-child(2) .lmeter i{transition-delay:.08s}
  .lang:nth-child(3) .lmeter i{transition-delay:.16s}
  .lang:nth-child(4) .lmeter i{transition-delay:.24s}
  .lang:nth-child(5) .lmeter i{transition-delay:.32s}
}

/* Inline SVG, not emoji. Windows renders a regional-indicator pair as the two
   letters, so an emoji flag is a GB or FR box on the machine most likely to
   open this. The hairline is inset rather than a border so it survives the
   rounded corner. */
.flag{
  display:block;width:46px;height:30.67px;border-radius:4px;overflow:hidden;
  box-shadow:0 0 0 1px rgba(18,18,17,.14) inset;
}
:root[data-theme="dark"] .flag{box-shadow:0 0 0 1px rgba(255,255,255,.18) inset}
.flag svg{display:block;width:100%;height:100%}

/*  no travel 
   Under reduced motion and on paper the row wraps and shows its set once; a
   second copy of every skill is noise once nothing is moving. */

/*  shared reveal 
   0fr to 1fr rather than max-height: it animates to the content's real height,
   whatever that turns out to be. The child needs overflow:hidden and min-height:0
   or the row collapses to nothing while the text still paints. */
.more{
  display:grid;grid-template-rows:0fr;opacity:0;
  transition:grid-template-rows .42s var(--ease),opacity .3s var(--ease);
}
.more > *{overflow:hidden;min-height:0}
/* On the first item, not on the list. min-height:0 lets the clipped child shrink
   to nothing but padding is not min-height, so 18px on the list itself is 18px
   the collapsed row can never give back. */
.more .lines li:first-child{padding-top:18px}
/* The hint holds its space when it fades, so the journey card only needs
   the difference, not the full gap again. */
.tl-i:hover .more,.tl-i:focus-within .more{grid-template-rows:1fr;opacity:1}

/*  the tool grid
    A still grid, not the marquee this replaced on 2026-08-21: a row that moves
    cannot be read at a glance, and a shelf of tools is something you scan. The
    doubled list, the -50% loop, the edge mask and the hover-to-pause all went
    with it, and so did the arithmetic tying the animation's duration to the
    length of the list.

    auto-fit with a minimum, so the columns are decided by the width available
    and adding a tool never means re-balancing anything. At the widths this
    section actually gets it lands on four across, three on a tablet, two on a
    phone. */
.tk{margin-top:clamp(60px,7vw,96px)}
.tk .wide-head{margin-bottom:clamp(20px,2.6vw,30px)}

/* The contact head costs 20px that the end of the page does not have. It and
   the footer must fit one phone screen, and at 375x460 there were 7px spare
   before this title existed, so the head has to pay for itself rather than the
   block growing. Its own spacing gives the 20px back: the section already
   carries the padding that separates it from Tools above, so a full gap under
   the title as well is the same distance stated twice.

   Desktop keeps the normal spacing. Every rule here is inside the query. */
#contact .wide-head{margin-bottom:clamp(18px,2.4vw,28px)}

/* The head costs 35px the end of the page did not have spare, so it pays for
   itself out of soft spacing rather than the block being allowed to grow. The
   section already carries the padding that separates it from Tools above, so a
   full gap under the title as well states the same distance twice.

   Written against #contact.sec-end and not .sec-end: the base rule sits later
   in this file and a bare class loses to it wherever it is declared. */
@media (max-width:760px){
  #contact.sec-end{padding-top:10px}
  #contact .wide-head{margin-bottom:8px}
  #contact .end-in{padding-bottom:8px}
}
/* Shorter than an SE with its browser furniture, where the decoration has
   already gone and spacing is the last thing left to give. */
@media (max-width:760px) and (max-height:540px){
  #contact.sec-end{padding-top:6px}
  #contact .wide-head{margin-bottom:4px}
  #contact .end-in{padding-bottom:4px}
  #contact .wide-head h2{font-size:1.42rem}
}
.tk-g{
  list-style:none;margin:0;padding:0;display:grid;gap:clamp(10px,1.1vw,14px);
  grid-template-columns:repeat(auto-fit,minmax(178px,1fr));
}
/* A tile, and this is the one place on the page that gets one. The reference
   Mehdi sent is a panel per logo, and it earns it here: without a boundary,
   fourteen marks of different weights and colours on an open ground read as
   scattered rather than as a set. The fill is the panel colour at a fraction,
   not a border alone, or the grid reads as a table. */
.tk-g li{
  display:flex;align-items:center;justify-content:center;gap:12px;
  min-height:74px;padding:14px 16px;border-radius:12px;
  background:rgba(255,255,255,.028);border:1px solid var(--line-2);
  transition:background .3s var(--ease),border-color .3s var(--ease),transform .3s var(--ease);
}
.tk-g li:hover{background:rgba(255,255,255,.055);border-color:var(--line);transform:translateY(-2px)}
.tk-n{
  font-family:var(--display);font-weight:500;font-size:clamp(.94rem,1.05vw,1.05rem);
  letter-spacing:-.015em;color:var(--ink-2);line-height:1.2;
  /* The tile already centres the mark and the name as a pair, measured at 0px
     off centre at every width. What was not centred is a name that wraps: the
     block is as wide as its longest line and the short one sat against the
     left of it, so "Adobe" hung off the left edge of "Illustrator" and the
     tile read as lopsided. Four of the fourteen wrap at the width this is
     usually seen at. */
  text-align:center;
}
.tk-g li:hover .tk-n{color:var(--ink)}
.tk-i{width:26px;height:26px;flex:none;display:grid;place-items:center}
.tk-i svg{width:100%;height:100%;display:block}
/*  every mark moves the way its own software behaves
    One gesture per tool, not a shared set. Mehdi 2026-08-21, after a first pass
    grouped fourteen tools into seven families: the point of the motion is that
    it says something about the tool, and a shared gesture says only that two
    tools are alike.

    Transform and opacity only, so the compositor owns all fourteen and they
    cost nothing on scroll. NOTHING LOOPS: a mark that moves forever reads as a
    page that has not finished loading.

    Hover and arrival are MUTUALLY EXCLUSIVE, split by pointer rather than by
    width. Re-applying an identical animation to an element that already carries
    it does not restart it, so a mark that had played on entry would sit still
    for the rest of the visit under the pointer. */

/* The nib tilts into a stroke and lifts off the page. */
@keyframes gIllustrator{
  35%{transform:rotate(-16deg) translate(-1.5px,-2px)}
  70%{transform:rotate(6deg) translate(1px,0)}
  100%{transform:none}
}
/* An element dropped onto the canvas, overshooting its place and settling. */
@keyframes gCanva{
  0%{transform:scale(1)}
  30%{transform:scale(1.16) rotate(-5deg)}
  60%{transform:scale(.95) rotate(2deg)}
  100%{transform:none}
}
/* A wave travelling through the squiggle rather than the whole mark rocking. */
@keyframes gHiggsfield{
  20%{transform:translateX(-2.5px) skewX(-8deg)}
  50%{transform:translateX(2.5px) skewX(8deg)}
  75%{transform:translateX(-1px) skewX(-3deg)}
  100%{transform:none}
}
/* The slide advances: this one leaves and the next arrives behind it. */
@keyframes gPowerpoint{
  0%{transform:none;opacity:1}
  45%{transform:translateX(-14px);opacity:0}
  46%{transform:translateX(14px);opacity:0}
  100%{transform:none;opacity:1}
}
/* Down the rows, and mechanically: steps(), because a spreadsheet does not
   ease from one cell to the next. */
@keyframes gExcel{
  0%{transform:none}
  30%{transform:translateY(4px)}
  60%{transform:translateY(8px)}
  100%{transform:none}
}
/* Two keys struck. */
@keyframes gWord{
  15%{transform:translateY(3px) scale(.93)}
  35%{transform:none}
  55%{transform:translateY(3px) scale(.93)}
  100%{transform:none}
}
/* The burst radiates: it opens outward and turns an eighth as it goes. */
@keyframes gClaude{
  40%{transform:scale(1.3) rotate(22deg)}
  75%{transform:scale(.96) rotate(-4deg)}
  100%{transform:none}
}
/* A caret blinking in a terminal, twice, with the tiny step a character takes.*/
@keyframes gClaudecode{
  0%{opacity:1;transform:none}
  20%{opacity:.25}
  35%{opacity:1;transform:translateX(2px)}
  55%{opacity:.25}
  70%{opacity:1;transform:translateX(4px)}
  100%{opacity:1;transform:none}
}
/* A turn taken in a conversation: half a rotation, not a full spin. */
@keyframes gChatgpt{
  100%{transform:rotate(180deg)}
}
/* A packet travelling the workflow: it gathers, crosses, arrives. */
@keyframes gN8n{
  25%{transform:translateX(-4px) scale(.9)}
  60%{transform:translateX(5px) scale(1.1)}
  100%{transform:none}
}
/* A scan darting across the market and coming back with the answer. */
@keyframes gSemrush{
  30%{transform:translateX(7px) scale(1.05)}
  45%{transform:translateX(7px) scale(1.05)}
  100%{transform:none}
}
/* The loop flips over. perspective is inside the transform because the parent
   is a 26px box and a perspective property on it would be absurd. */
@keyframes gMeta{
  100%{transform:perspective(220px) rotateY(360deg)}
}
/* The bars grow out of the floor, which is why the origin is the floor. */
@keyframes gAnalytics{
  0%{transform:scaleY(.3)}
  65%{transform:scaleY(1.12)}
  100%{transform:scaleY(1)}
}
/* The ring turns once. */
@keyframes gWorkspace{
  100%{transform:rotate(360deg)}
}
.tk-g li[data-g="analytics"] .tk-i{transform-origin:50% 100%}

@media (prefers-reduced-motion:no-preference){
  @media (hover:hover) and (pointer:fine){
    .tk-g li[data-g="illustrator"]:hover .tk-i{animation:gIllustrator .66s var(--ease)}
    .tk-g li[data-g="canva"]:hover       .tk-i{animation:gCanva .6s var(--ease)}
    .tk-g li[data-g="higgsfield"]:hover  .tk-i{animation:gHiggsfield .8s var(--ease)}
    .tk-g li[data-g="powerpoint"]:hover  .tk-i{animation:gPowerpoint .62s var(--ease)}
    .tk-g li[data-g="excel"]:hover       .tk-i{animation:gExcel .56s steps(1,end)}
    .tk-g li[data-g="word"]:hover        .tk-i{animation:gWord .5s var(--ease)}
    .tk-g li[data-g="claude"]:hover      .tk-i{animation:gClaude .7s var(--ease)}
    .tk-g li[data-g="claudecode"]:hover  .tk-i{animation:gClaudecode .72s steps(1,end)}
    .tk-g li[data-g="chatgpt"]:hover     .tk-i{animation:gChatgpt .7s var(--ease)}
    .tk-g li[data-g="n8n"]:hover         .tk-i{animation:gN8n .62s var(--ease)}
    .tk-g li[data-g="semrush"]:hover     .tk-i{animation:gSemrush .66s var(--ease)}
    .tk-g li[data-g="meta"]:hover        .tk-i{animation:gMeta .95s var(--ease)}
    .tk-g li[data-g="analytics"]:hover   .tk-i{animation:gAnalytics .6s var(--ease)}
    .tk-g li[data-g="workspace"]:hover   .tk-i{animation:gWorkspace .9s var(--ease)}
  }
  @media (hover:none){
    .tk-g li.on[data-g="illustrator"] .tk-i{animation:gIllustrator .66s var(--ease) both}
    .tk-g li.on[data-g="canva"]       .tk-i{animation:gCanva .6s var(--ease) both}
    .tk-g li.on[data-g="higgsfield"]  .tk-i{animation:gHiggsfield .8s var(--ease) both}
    .tk-g li.on[data-g="powerpoint"]  .tk-i{animation:gPowerpoint .62s var(--ease) both}
    .tk-g li.on[data-g="excel"]       .tk-i{animation:gExcel .56s steps(1,end) both}
    .tk-g li.on[data-g="word"]        .tk-i{animation:gWord .5s var(--ease) both}
    .tk-g li.on[data-g="claude"]      .tk-i{animation:gClaude .7s var(--ease) both}
    .tk-g li.on[data-g="claudecode"]  .tk-i{animation:gClaudecode .72s steps(1,end) both}
    .tk-g li.on[data-g="chatgpt"]     .tk-i{animation:gChatgpt .7s var(--ease) both}
    .tk-g li.on[data-g="n8n"]         .tk-i{animation:gN8n .62s var(--ease) both}
    .tk-g li.on[data-g="semrush"]     .tk-i{animation:gSemrush .66s var(--ease) both}
    .tk-g li.on[data-g="meta"]        .tk-i{animation:gMeta .95s var(--ease) both}
    .tk-g li.on[data-g="analytics"]   .tk-i{animation:gAnalytics .6s var(--ease) both}
    .tk-g li.on[data-g="workspace"]   .tk-i{animation:gWorkspace .9s var(--ease) both}
    /* One pass down the grid rather than fourteen at once, which would read as
       the page glitching. --i is set in app.js. */
    .tk-g li .tk-i{animation-delay:calc(var(--i, 0) * .05s)}
  }
}

/* The marks are the only brand colour on the site, so they are held back a
   little at rest and come up under the pointer. */
.tk-i{opacity:.86;transition:opacity .3s var(--ease)}
.tk-g li:hover .tk-i{opacity:1}
/* Two tiles a row on a phone, and the name is allowed to wrap there: at 178px
   minimum a single column would leave a 320px screen with one enormous tile. */
@media (max-width:620px){
  .tk-g{grid-template-columns:repeat(auto-fit,minmax(136px,1fr));gap:9px}
  .tk-g li{min-height:64px;padding:12px;gap:9px}
  .tk-n{font-size:.86rem}
  .tk-i{width:22px;height:22px}
}

/* No pointer, no hover, so the write-up has to be open from the start. This
   block has to sit AFTER the base .more rule above: it was written 230 lines
   higher, lost the cascade at equal specificity, and every project description
   and journey detail was invisible on a phone with no way to reach it. */
@media (hover:none),(pointer:coarse){
  .more{grid-template-rows:1fr;opacity:1}
  .more > *{overflow:visible}
  .tl-i .hint{display:none}
  /* A card lifts on hover and a touch device has no hover to leave, so the
     transform would stick on the last one tapped. */
  .case:hover,.case:focus-within{transform:none;box-shadow:none}
}

/*  06 contact
   The ask on the left, the ways to reach him on the right, and the name once
   across the foot. It was centred, which said "this is the end", but it put the
   four details in a strip under the buttons where they read as a footer the page
   had already finished with. */
.sec-end{position:relative;overflow:hidden;padding:clamp(76px,10vw,140px) 0 0}
.end-in{
  display:grid;grid-template-columns:minmax(0,1.04fr) minmax(0,.96fr);
  gap:clamp(34px,6vw,92px);align-items:start;
  padding-bottom:clamp(56px,7vw,92px);
}
.end-say{min-width:0}

.statement-c{max-width:18ch;margin-top:clamp(20px,2.6vw,30px)}

.mail{
  display:inline-block;margin-top:clamp(18px,2.4vw,28px);
  font-family:var(--display);font-weight:500;
  /* Against the column, not the viewport. At 3vw it was sized for a full-width
     page and broke "gmail.com" across two lines inside the narrower column. */
  font-size:clamp(1.32rem,calc(var(--col) * .031),1.95rem);
  letter-spacing:-.04em;line-height:1.06;
  background-image:linear-gradient(var(--accent),var(--accent));
  background-size:0% 2px;background-position:0 100%;background-repeat:no-repeat;
  transition:background-size .5s var(--ease),color .3s;
  word-break:break-word;
}
.mail:hover{color:var(--accent);background-size:100% 2px}

.cta-row{display:flex;gap:12px;flex-wrap:wrap;margin-top:clamp(28px,3.4vw,40px)}

/* A column beside the ask, not a strip beneath it, so it takes the same shape
   the profile's facts already have: mark, then the pair. */
.reach{width:100%;display:grid;gap:0;border-top:1px solid var(--line)}
.reach li{
  display:grid;grid-template-columns:auto minmax(0,1fr);gap:18px;
  align-items:center;min-width:0;
  padding:16px 0;border-bottom:1px solid var(--line-2);
}
.reach .t{color:var(--ink-3)}
.rc-b{display:flex;flex-direction:column;gap:5px;min-width:0}
.rc-b > :last-child{font-size:.99rem}
.reach a{
  background-image:linear-gradient(var(--accent),var(--accent));
  background-size:0% 1px;background-position:50% 100%;background-repeat:no-repeat;
  transition:background-size .35s var(--ease),color .25s;
}
.reach a:hover{color:var(--accent);background-size:100% 1px}

/* The name, once, across the foot. Two stacked copies: an outline underneath and
   a solid one over it, clipped from the right and opened by the scroll, so the
   page ends on the name filling in rather than on a watermark behind the text.

   clip-path, not a background-position sweep on background-clip:text. The sweep
   needs the fill to be a gradient, which loses the accent as a flat colour, and
   it cannot be composited off the main thread. */
.signoff{
  /* It sits under the colophon now rather than at the end of a section, so it
     has to buy its own separation from the Back to top line above it. */
  margin-top:clamp(22px,4vw,40px);
  position:relative;line-height:.82;
  padding:0 0 clamp(10px,1.6vw,22px);
  font-family:var(--display);font-weight:700;letter-spacing:-.045em;
  /* Sized to run the width of the column rather than of the shell: it is the
     only thing here that is not inside the measure, and a name stopping short of
     both margins reads as a heading that failed to fit. Against 15.2vw it was
     measuring the viewport, which is 418px wider than the column it now sits
     in, and his name ran off the right edge of the page. */
  font-size:clamp(2.4rem,calc(var(--col) * .152),15rem);
  text-align:center;white-space:nowrap;user-select:none;
}
.signoff .so-l{color:transparent;-webkit-text-stroke:1.4px var(--line)}
/* Without text-stroke the outline copy is invisible and the fill has nothing to
   open over, so the whole thing collapses to one solid line. */
@supports not (-webkit-text-stroke: 1px black){
  .signoff .so-l{color:var(--line)}
  .signoff .so-f{display:none}
}
.signoff .so-f{
  position:absolute;left:0;right:0;top:0;color:var(--ink);
  clip-path:inset(0 0 0 0);
}
.js .signoff .so-f{clip-path:inset(0 100% 0 0)}
/* French is the same string here, so no measure to re-check: it is his name. */
html[lang="fr"] .signoff{letter-spacing:-.045em}

/* The icon disc. A hairline ring rather than a fill: four filled discs in a row
   would out-weigh the mail address above them, which is the actual action. */
.rc-i{
  display:grid;place-items:center;width:42px;height:42px;border-radius:50%;
  border:1px solid var(--line);color:var(--ink-2);margin-bottom:2px;
  transition:border-color .3s var(--ease),color .3s var(--ease);
}
.rc-i .ico{width:19px;height:19px}
/* Only the two rows that are links respond to a pointer. A location that
   lights up under the cursor promises something it cannot do. */
.reach li:has(a):hover .rc-i{border-color:var(--accent);color:var(--accent)}

/* footer */
.foot{padding:26px 0 34px}
.foot-in{
  display:flex;flex-direction:column;align-items:center;
  gap:clamp(14px,2.4vw,20px);text-align:center;
  color:var(--ink-3);
}
.foot a:hover{color:var(--accent)}
.foot-c{white-space:nowrap}

.foot-soc{display:flex;gap:8px}
.soc{
  display:grid;place-items:center;width:38px;height:38px;border-radius:50%;
  border:1px solid var(--line);color:var(--ink-2);
  transition:border-color .3s var(--ease),color .3s var(--ease),transform .3s var(--ease);
}
.soc:hover,.soc:focus-visible{border-color:var(--accent);color:var(--accent);transform:translateY(-2px)}
.soc .ico{width:17px;height:17px}

/* Back to top. The label keeps its underline; the arrow is the moving part. */
.foot-top{display:inline-flex;align-items:center;gap:9px;white-space:nowrap}
.foot-top .ft-t{
  background-image:linear-gradient(currentColor,currentColor);
  background-size:0% 1px;background-position:0 100%;background-repeat:no-repeat;
  transition:background-size .35s var(--ease);
}
.foot-top:hover .ft-t,.foot-top:focus-visible .ft-t{background-size:100% 1px}
/* A fixed box the height of one arrow, holding two stacked. Sliding the pair up
   by exactly one arrow swaps them, and the box's own overflow is what hides the
   one leaving and the one waiting. */
.ft-a{display:block;width:15px;height:15px;overflow:hidden}
.ft-a .ico{
  width:15px;height:15px;display:block;
  transition:transform .42s var(--ease);
}
.foot-top:hover .ft-a .ico,.foot-top:focus-visible .ft-a .ico{transform:translateY(-100%)}

/*  responsive  */
/*  two columns 
    The panel only earns a column of its own when there is room for one beside a
    readable measure. 981 is where the existing hero rules already hand over. */
@media (min-width:981px){
  :root{
    --rail-w:76px;
    --side-w:clamp(360px,38vw,680px);
    --col:calc(100vw - var(--rail-w) - var(--side-w));
    --shell:min(1080px,calc(var(--col) * .88));
  }
  main,.foot{margin-left:calc(var(--rail-w) + var(--side-w))}

  /* The identity block lives in the hero's markup now so a phone can order it
     against the kicker and the summary. On desktop it belongs on the panel, so
     it is lifted out of the column and fixed over it. */
  /* The name runs down the foot of the photograph in two lines rather than
     across it in one. One line had to stay small enough to clear the two tiles
     on the same row, which is why it read as a caption on a 680px panel; broken
     at the space it answers to its own column instead and takes twice the size,
     and the stagger between the two words finally reads as two lines arriving
     rather than as one line jittering. */
  .side-name{
    font-size:clamp(2.1rem,4vw,3.5rem);line-height:.94;letter-spacing:-.045em;
  }
  .side-name .sn-w{display:block}
  /* Drawn, not faded. The same gesture every rule on this page makes, and the
     one accent mark in the block: the role under it is already accent, so the
     two read as one group rather than as two ideas. */
  .sn-rule{
    display:block;width:clamp(56px,7vw,96px);height:2px;background:var(--accent);
    margin-top:clamp(14px,1.6vh,20px);
    transform:scaleX(0);transform-origin:0 50%;
  }
  .side-role{margin-top:clamp(11px,1.4vh,15px)}

  .side-id{
    position:fixed;left:var(--rail-w);bottom:0;width:var(--side-w);z-index:41;
    padding:0 clamp(20px,2vw,32px) clamp(24px,3vh,40px);
    /* The name reads along the bottom of the photograph and the two icons stand
       at the end of that line. flex-end, so the pair grows upward off the same
       baseline the role sits on rather than pushing the name up. */
    display:flex;align-items:flex-end;justify-content:space-between;gap:20px;
  }
  .side-who{min-width:0}
  .side{left:var(--rail-w)}

  /*  the header stands on its side 
      Same element and the same controls, turned through 90 degrees: one nav,
      one language button, one theme button, one set of ids, so nothing in
      app.js changes. Below 981 it is the horizontal bar again, which is what
      the menu overlay is built against. */
  .bar{
    left:0;top:0;bottom:auto;height:100vh;width:var(--rail-w);
    flex-direction:column;align-items:center;gap:0;
    padding:22px 0;
    border-bottom:0;border-right:1px solid var(--line);
  }
  .bar.stuck{border-bottom-color:transparent}
  .bar-nav{
    flex-direction:column;gap:6px;margin:auto 0;
  }
  .bar-nav a{
    display:grid;place-items:center;width:44px;height:44px;border-radius:10px;
    transition:color .25s var(--ease),background .25s var(--ease);
  }
  /* The word is the accessible name and the tooltip; the rail shows the mark. */
  .bar-nav .bn-t{
    position:absolute;width:1px;height:1px;overflow:hidden;
    clip:rect(0 0 0 0);clip-path:inset(50%);white-space:nowrap;
  }
  .bar-nav .bn-i{display:block;width:21px;height:21px}
  .bar-nav a:hover{background:var(--accent-soft);color:var(--accent)}
  .bar-nav a.on{color:var(--accent);background:var(--accent-soft)}
  /* The sliding underline was a horizontal device and has nothing to sit under. */
  .bar-nav a::after{display:none}
  .bar-tools{flex-direction:column;gap:8px;margin-left:0;margin-top:auto}
  /* 76px cannot hold EN | FR, so the rail shows the live language only, the
     same reduction the bar already makes below 352px. */
  .bar .tool-sep,.bar .tool .off{display:none}
  .bar .tool{width:44px;padding:0;justify-content:center}
  /* The panel carries Email and the rail carries Contact. A third mail icon in
     the same 76px column is the same action three times. */
  .cta{display:none}
}

/*  one column 
    No room for a panel beside the text, so it becomes the top of the page and
    scrolls away. It keeps the name on the photograph, which is what makes this
    work on a phone: the first screen is his face and his name, and the lede and
    the buttons are directly under it rather than a screen down. */
@media (max-width:980px){
  /* relative, NOT static. .side-media is absolutely positioned, so a static
     panel gives it no containing block to resolve against: it escaped to the
     page and painted the cutout over the Profile section below, with the hero
     text sitting on top of the photograph. overflow:hidden only clips what the
     box actually contains. */
  .side{
    position:relative;width:auto;height:clamp(360px,54svh,560px);
    padding-top:var(--bar-h);
  }
  .side-media{inset:var(--bar-h) 0 0 0}
  .side .figure img{object-position:center 18%}
  /*  the photograph is the hero 
      Full bleed behind everything, with the words at the foot of the screen so
      nothing sits over his face. background-image rather than the panel's <img>
      because the panel is a desktop arrangement; image-set keeps the WebP and
      falls back to the JPEG, which a background cannot get from <picture>. */
  .side{display:none}
  .hero{
    /* background-origin and -clip on content-box, so the photograph is laid out
       and painted BELOW the padding rather than behind it. The bar is opaque and
       exactly --bar-h tall, so it covered the top of his head; there is no
       vertical slack to shift with background-position, because cover already
       fits the frame's height almost exactly at these ratios. */
    min-height:100svh;padding:var(--bar-h) 0 0;
    /* origin content-box, clip BORDER-box, and the picture lifted 24px above
       that origin. The origin is what sizes and frames it, so cover still
       measures against the box under the bar and the crop is the one that was
       chosen; the clip is what decides where it may paint, and letting it paint
       into the padding means the top 24px of the photograph sits BEHIND the bar
       rather than stopping at its edge.

       Clipping to the content box made the alignment exact and therefore
       fragile: it depended on the bar being exactly --bar-h and the padding
       being exactly --bar-h, and anything that moved either by a pixel opened a
       band of paper between the two. Mehdi reported that band twice, on a phone
       I could not reproduce at any emulated size. There is now 24px of
       photograph to absorb it, so the seam cannot open whatever the two figures
       actually come out as on a given device.

       The missing 24px at the FOOT of the picture is covered: .hero::before is
       solid --paper for its first 4%, which is 28px at 700 and grows from
       there. */
    background-origin:content-box;background-clip:border-box;
    display:flex;flex-direction:column;justify-content:flex-end;
    background-image:-webkit-image-set(url("img/mehdi-portrait.webp") type("image/webp"),
                                       url("img/mehdi-portrait.jpg") type("image/jpeg"));
    background-image:image-set(url("img/mehdi-portrait.webp") type("image/webp"),
                               url("img/mehdi-portrait.jpg") type("image/jpeg"));
    background-size:cover;background-position:center -24px;background-repeat:no-repeat;
  }
  /* The hero's bottom spacing moved here: a padding-bottom on .hero would clip
     the photograph short of the screen and leave a hard edge above the veil. */
  .hero-in{padding-bottom:clamp(26px,5svh,44px)}
  /* The glow is a desktop flourish and this is where the veil has to live: the
     grain already owns ::after. Stops well short of his face. */
  .hero::before{
    content:"";position:absolute;inset:0;z-index:1;pointer-events:none;
    right:auto;top:auto;left:0;bottom:0;width:100%;height:100%;
    background:linear-gradient(to top,var(--paper) 4%,rgba(12,12,11,.94) 26%,
               rgba(12,12,11,.72) 42%,rgba(12,12,11,.28) 56%,transparent 68%);
  }
  .hero-in{position:relative;z-index:2;display:flex;flex-direction:column;text-align:center}
  /* display:contents so the three identity lines become items of the hero's own
     column and can be ordered against the kicker and the summary. */
  /* display:contents flattens one level, so the two groups the panel needs have
     to declare it as well or the hero cannot order their contents against the
     kicker and the summary. */
  .side-id,.side-who,.side-reach{display:contents}
  .kicker{order:1;justify-content:center;margin-bottom:clamp(10px,2.4vw,16px)}
  .side-name{order:2;font-size:clamp(2rem,9vw,2.9rem);line-height:1.02}
  .side-role{order:3;margin-top:8px}
  .lede{order:4;margin-top:clamp(14px,3.4vw,20px);max-width:none;color:var(--ink-2)}
  /* Stacked on the panel, side by side under the summary: a phone has the width
     and not the height, and two 56px tiles in a column would cost 124px of a
     screen that is already tight. */
  .reach-lab{order:5;text-align:center;margin-top:clamp(20px,4vw,26px)}
  .side-links{order:6;justify-content:center;gap:16px;margin-top:12px}
  .app{width:56px;height:56px}
  .app:hover{transform:none}
  /* The role is under his name; the hero's own heading and buttons would say
     both a second time. */
  .hero-h,.hero-act,.scroller{display:none}
  /* The veil, the photograph it sat on and the hidden links all belonged to the
     full-width photo band this replaced. The links are the card's two actions
     now, styled above. */
}

/*  where the sections stack 
    These were all keyed to 920px of VIEWPORT, which stopped meaning anything
    once the rail and the panel took 640px of it before the content started. At
    1024 the section grid still gave its head a fixed 240px and left the journey
    244px, inside which .tl-i's own 112px year column left 90px for the text and
    every achievement bullet ran off the right of the page. They stack on the
    column's behalf now. The bar's own rules stay at 920: that is a question
    about the header, not about the measure. */
@media (max-width:1180px){
  .profile-body{grid-template-columns:minmax(0,1fr);gap:40px}
  .pb-col:first-child{position:static}
  .end-in{grid-template-columns:minmax(0,1fr);gap:38px}
  .statement-c{max-width:22ch}
  /* 112px of year column and a 42px gap is 154px the narrow column cannot give. */
  .tl-i{grid-template-columns:minmax(0,1fr);gap:0;padding-left:30px}
  .tl-y{text-align:left;padding-right:0;padding-top:0;margin-bottom:10px}
  .tl-line{left:5px;top:6px}
  /* .is-degree has to be named on both halves or the node and its date come
     apart. Stacked, the year is the first line of the entry and the node sits
     against it; the desktop rule pads the year down by 1.4em to clear the card
     it stands beside, and .is-degree .tl-y outweighs the flat .tl-y above, so
     the date dropped 17px while the node stayed where it was. That is the node
     floating above the card on the two degrees and level with the year
     everywhere else. */
  .tl-i::before,.is-work::before,.is-degree::before{left:0;top:.28em}
  .is-work .tl-y,.is-degree .tl-y{padding-top:0}
}
@media (max-width:1080px){
  .bar-nav{gap:20px}
  .bar-nav a{font-size:.82rem}
}
/* A 720px laptop cannot hold the hero at full padding: measured at 817px tall,
   which put the two buttons below the fold on the first screen. */
/* 761 to 980 wide and short with it: a landscape phone, a small tablet, a
   restored window. The photograph is the hero's background from 980 down, so
   this tier had the same bug the phone tier did, adding 2vh to the padding that
   positions the top of the picture. Same answer: the height comes off the
   content. .name and .hero-act are both dead below 980. */
@media (min-width:761px) and (max-height:800px){
  .hero-in{padding-bottom:clamp(20px,4svh,36px)}
  .lede{margin-top:16px}
}
@media (min-width:981px) and (max-height:800px){
  .name{font-size:clamp(3.4rem,10.4vw,8rem)}
  .hero-act{margin-top:24px}
}
@media (max-width:980px){
  .lede{max-width:34ch}
}
@media (max-width:760px){
  /* A 112px gutter plus a 42px gap is 154px the phone does not have. The year
     stacks above its entry and the line moves to the very edge. */
  .tl-line{left:5px;top:6px}
  .tl-i{grid-template-columns:minmax(0,1fr);gap:0;padding-left:30px}
  .tl-y{text-align:left;padding-right:0;padding-top:0;margin-bottom:10px}
  /* .is-degree has to be named on both halves or the node and its date come
     apart. Stacked, the year is the first line of the entry and the node sits
     against it; the desktop rule pads the year down by 1.4em to clear the card
     it stands beside, and .is-degree .tl-y outweighs the flat .tl-y above, so
     the date dropped 17px while the node stayed where it was. That is the node
     floating above the card on the two degrees and level with the year
     everywhere else. */
  .tl-i::before,.is-work::before,.is-degree::before{left:0;top:.28em}
  .is-work .tl-y,.is-degree .tl-y{padding-top:0}
  .is-work .tl-c{padding:20px}
}
@media (max-width:920px){
  .bar-nav{display:none}
  /* The links have left the bar, so the button that reaches them arrives in the
     same breath. The CTA goes with them: it is the overlay's job now, and it is
     what made the bar four controls wide on a 320px screen. */
  .menu-btn{display:flex}
  .cta{display:none}
  .ovl-in{grid-template-columns:minmax(0,1fr);gap:clamp(2rem,6svh,3rem);align-items:start}
  .ovl-side{padding-top:clamp(1rem,3svh,1.6rem);border-top:1px solid rgba(128,128,128,.3);width:100%}
}
@media (max-width:760px){
  /* The hero's own sizing is set in the 980 tier, where the photograph became
     its background. This block used to reset min-height to 0 for a hero that
     was a text block under a panel, and it still won the cascade: the content
     stayed at the top of the screen, over his face. */
  .scroller{display:none}
}
/* The same trade the 761-to-800px laptop tier makes, on a phone. At 375x667 the
   text block alone was 519px of a 667px screen and everything left went to the
   portrait. This returns about 80px of it. */
@media (max-width:760px) and (max-height:740px){
  /* The hero's padding-top is NOT spare space any more: since the photograph
     became the hero's background clipped to the content box, that padding is
     what positions the top edge of the picture, and the bar is exactly --bar-h
     tall. Adding 1.6svh to it pushed the picture 11px below the bar and painted
     a band of paper between the two, which is the gap Mehdi reported. Measured
     at 393x700: padding-top 75.2px against a 64px bar. Nothing but the bar's
     own height may go in here.

     The height this tier exists to reclaim comes off the content instead. .name
     and .hero-act were already dead at this width, the first because the hero's
     heading is display:none from 980 down and the identity block uses
     .side-name, the second because the buttons are .side-links here. */
  .hero-in{padding-bottom:clamp(18px,3svh,30px)}
  .side-name{font-size:clamp(1.85rem,8.4vw,2.5rem)}
  .lede{margin-top:14px;font-size:1rem}
  .side-links{margin-top:16px}
}
@media (max-width:620px){
  body{font-size:16px}
  /* The rule used to be dropped here, so Profile, Skills, Languages and Contact
     lost the hairline while Journey kept its own and the heads stopped matching
     on the one screen where they are read one after another. It survives on a
     320px phone: flex-basis 120px is a floor on a flex-shrink item, not a
     minimum, and the title is short. */
  .lang{padding:18px}
  :root{--shell:min(1280px,92vw)}
  .name .n2{-webkit-text-stroke-width:1px}
  .cta{padding:0 12px;font-size:.78rem}
  /* Three deep steps and a nowhere-near-them chip is not a row. The chip moves
     under the pair and sits with the text it belongs to. */
  .mt-i{grid-template-columns:auto minmax(0,1fr);gap:14px}
  .mt-out{grid-column:2;justify-self:start;margin-top:12px}
  /* The icon stays beside the pair, not above it. Stacked, one fact is three
     lines with a disc floating over them and the column reads as four orphans
     rather than four facts. */
  .sec-wide .facts li{grid-template-columns:auto minmax(0,1fr);gap:15px;padding:14px 0}
  .fx-i{width:36px;height:36px}
  .fx-i .ico{width:17px;height:17px}
  .foot-in{flex-wrap:wrap;row-gap:20px}
  .foot-soc{order:-1;flex-basis:100%;justify-content:center}
  /* A phone browser draws its own toolbar over the foot of the page, and it
     expands from its collapsed state the moment you reach the end of the
     document, which is the one moment the signoff is on screen. 34px of padding
     put his name underneath it: measured at 390x664, the name ran 571 to 630
     against a document ending at 664, so the bottom half of it sat behind
     Safari's bar. This clears a 56pt toolbar and the home indicator under it.

     env() resolves to 0 in a normal tab, since the viewport meta deliberately
     does not carry viewport-fit=cover and iOS therefore insets the web view for
     us. It is here for the standalone case, where the page is added to the home
     screen, the browser chrome goes away and the indicator does not. */
  .foot{padding-bottom:calc(68px + env(safe-area-inset-bottom, 0px))}
  /* The name is the last thing on the page and the only thing that cannot be
     scrolled past, so it buys a little of that clearance for itself rather than
     sitting hard against the padding. */
  .signoff{margin-top:clamp(18px,3.4vw,28px)}
  .lede{max-width:none}
  .big{max-width:none}
}
/*  the contact block, centred 
    On a phone the ask and the four details are one column under one another,
    and left-aligning a 18ch statement, a centred sign-off and a row of two
    buttons produced three different axes in one screen. Everything in it sits
    on the middle now, the rows included: the mark and its pair travel together
    rather than the marks holding a left gutter the text no longer uses. */
@media (max-width:760px){
  .end-say{text-align:center}
  .statement-c{margin-inline:auto}
  .end-lab{justify-content:center}
  .cta-row{justify-content:center}
  /*  the end of the page is one screen
      Mehdi 2026-08-21: the contact block and the footer together have to fit a
      phone screen when you reach the bottom. They were 1046px against a 664px
      viewport, and the three stacked reach rows were 408px of it.

      So the mark goes back BESIDE its pair rather than above it. Stacking was
      done to keep the block on one centre line, and it cost 136px a row for a
      row that holds one short fact. Beside it, the three discs share an x and
      the list reads as a ledger, which is what the rest of the page does
      anyway. The list is capped and centred as a block, so the axis survives.*/
  .reach{max-width:300px;margin-inline:auto}
  .reach li{grid-template-columns:auto minmax(0,1fr);justify-items:start;gap:13px;padding:8px 0}
  .rc-b{align-items:flex-start;text-align:left;gap:2px}
  .rc-b > :last-child{font-size:.92rem}
  .rc-i{width:34px;height:34px;margin-bottom:0}
  .rc-i .ico{width:16px;height:16px}

  /* The two social discs are the same two links as the buttons directly above
     them, and this screen has no room to say anything twice. */
  .foot-soc{display:none}

  .sec-end{padding-top:clamp(28px,5vw,48px)}
  .end-in{gap:20px;padding-bottom:22px}
  .statement-c{margin-top:14px}
  .mail{margin-top:14px}
  .cta-row{margin-top:16px}
  .foot{padding-top:14px;padding-bottom:calc(56px + env(safe-area-inset-bottom, 0px))}
  .foot-in{row-gap:11px}
  .signoff{margin-top:12px;font-size:clamp(2rem,10.6vw,2.9rem)}

  /* A short screen gives the decoration up before it gives up an address. The
     signoff is aria-hidden already, it is the name repeated a fourth time, and
     Back to top is reachable by scrolling, which is what you were doing. */
  @media (max-height:540px){
    .signoff{display:none}
    .foot-top{display:none}
    /* And the block itself tightens rather than the page scrolling twice for
       one screen's worth of contact details. */
    .sec-end{padding-top:20px}
    .reach li{padding:6px 0}
    .rc-i{width:30px;height:30px}
    .end-in{gap:14px;padding-bottom:16px}
    .statement-c{margin-top:10px}
    .mail{margin-top:10px}
    .cta-row{margin-top:12px}
    .foot{padding-top:10px}
  }

  /*  and every section title with them
      Mehdi 2026-08-21. The heads are a baseline row of title, hairline and
      sometimes a note, which reads correctly beside a wide column and reads as
      a title shoved against the left margin on a phone. Stacked and centred,
      with the rule under the title at full width.

      The rule is drawn by GSAP from scaleX 0, and its origin is CSS, so
      centring the title means moving the origin to the middle: it opens
      outward from the centre instead of arriving from the left edge, which is
      the gesture a centred title asks for. */
  .wide-head,.sec-head{
    flex-direction:column;align-items:center;text-align:center;
    gap:clamp(11px,2.6vw,15px);
  }
  .wide-head .rule,.sec-head .rule{
    flex:none;width:100%;align-self:stretch;transform-origin:50% 50%;
  }
  /* It is nowrap so it cannot break the baseline row it used to sit in. That
     row is gone here, and "2018 to today" has the whole width to itself. */
  .jr-note{white-space:normal}
}

/* Wrapped, the separator was left hanging at the end of the first line, so
   below the width where it wraps the line break is the separator and the dot
   has nothing to do. Two thresholds, because the French is the longer line and
   wraps first: at a flat 430 the dot went on a 393px iPhone that was still
   setting the whole thing on one line and wanted it. */
@media (max-width:430px){
  html[lang="fr"] .kicker{gap:0 .9em}
  html[lang="fr"] .kicker .dot{display:none}
}
@media (max-width:384px){
  .kicker{gap:0 .9em}
  .kicker .dot{display:none}
}
@media (max-width:352px){
  .bar{gap:10px}
  .tool-sep,.tool .off{display:none}
}

/*  desktop opens on Profile 
    The hero column repeated the role the panel already states and then held a
    screen of empty space under it. It collapses rather than display:none,
    because .side-id lives inside it and is what paints the name onto the panel.
    This has to sit after the short-laptop tier, which re-applies the hero's own
    padding at max-height:800px and was leaving 136px of it behind. */
@media (min-width:981px){
  /* isolation and the overflow go with the collapse. .hero carried
     isolation:isolate and .hero-in a z-index, so the two of them were nested
     stacking contexts at z-index auto and 2 inside the root, and .side-id's
     own z-index:41 could only ever compete INSIDE them. The panel is 40 at the
     root, so the name, the role and the two buttons were laid out correctly at
     the foot of the photograph and painted underneath it: measured at the right
     coordinates, invisible on screen. Neutralising both puts .side-id back in
     the root context where 41 beats 40. */
  .hero{min-height:0;padding:0;background:none;overflow:visible;isolation:auto}
  .hero-in{position:static;z-index:auto}
  .hero > .shell > *:not(.side-id),.hero .scroller{display:none}
  .hero::before,.hero::after{display:none}

  /* The hero is gone here, so Profile is the first thing on the page and its
     own section padding was a screen-width band of empty paper above the first
     word. The border goes with it: a rule under nothing is not a divider. */
  #profile{padding-top:clamp(28px,3.4vw,52px)}
}

/*  the panel introduces itself 
    One gesture, four beats, and nothing on it moves again. The two words rise
    out of their own masks, the role follows, then the tiles arrive. Everything
    is transform and opacity, so the compositor owns all of it and a busy main
    thread on first load cannot stutter it; and it is CSS rather than GSAP for
    the same reason, since the clock starts when the element is styled instead
    of when a script has finished parsing two libraries.

    Desktop only. Below 981 the same elements are the hero's own content, they
    arrive with the page rather than on a panel, and a name sliding up over a
    photograph on a phone is motion for its own sake. */
@media (min-width:981px){
  .side-name .sn-w > span{
    transform:translateY(108%);
    animation:snRise .95s var(--ease) forwards;
  }
  .side-name .sn-w:nth-child(1) > span{animation-delay:.18s}
  .side-name .sn-w:nth-child(2) > span{animation-delay:.32s}
  .sn-rule{animation:snRule .85s var(--ease) .56s forwards}
  .side-role{animation:snIn .8s var(--ease) .66s both}
  .reach-lab{animation:snIn .8s var(--ease) .76s both}
  /* The tiles land the way an app icon does, from slightly under and slightly
     small, so the two of them read as objects arriving rather than as a block
     fading up. */
  .side-links .app{animation:appIn .7s var(--ease) both}
  .side-links .app:nth-child(1){animation-delay:.84s}
  .side-links .app:nth-child(2){animation-delay:.93s}
}
@keyframes snRise{to{transform:translateY(0)}}
@keyframes snRule{to{transform:scaleX(1)}}
@keyframes snIn{from{opacity:0;transform:translateY(9px)}to{opacity:1;transform:none}}
@keyframes appIn{from{opacity:0;transform:translateY(14px) scale(.86)}to{opacity:1;transform:none}}

/*  reduced motion  */
@media (prefers-reduced-motion:reduce){
  /* The level still shows, it just does not travel to get there. */
  .lmeter i{transform:scaleX(var(--l,1));transition:none}
  html{scroll-behavior:auto}
  :root,:root[data-theme="dark"],:root[data-theme="light"]{--dim:1;--dimf:none}
  .torch{display:none}
  .js .rv{opacity:1;transform:none}
  /* Nothing ever adds .on here, so the method has to be settled outright. */
  .js .mt-n,.js .mt-i.on .mt-n{color:var(--accent)}
  .js .mt-i::before{background:var(--accent);border-color:var(--accent)}
  .js .mt-b h3{color:var(--ink)}
  .js .mt-b p{color:var(--ink-2)}
  .js .mt-out{opacity:1;transform:none;color:var(--ink-2)}
  .side-name .sn-w > span,.side-role,.reach-lab,.side-links .app,.sn-rule{
    animation:none;opacity:1;transform:none;
  }
  .js .mt-spine i{transform:scaleY(1)}
  .js .signoff .so-f{clip-path:inset(0 0 0 0)}
  *{animation:none !important;transition-duration:.01ms !important}
}

/*  print 
   A clean black-on-white CV out of Ctrl+P. The journey unrolls: GSAP leaves a
   pin-spacer and an inline transform behind, and CSS !important is what beats
   an inline style that carries none. */
@media print{
  .lmeter{display:none}
  /* On paper the grid tightens to four columns and loses its fills: a tinted
     tile per tool is fourteen grey boxes on a page that is otherwise white. */
  .tk{margin-top:14pt;padding-top:10pt}
  .tk-g{grid-template-columns:repeat(4,1fr);gap:6pt}
  .tk-g li{min-height:0;padding:5pt 6pt;background:none;border-color:#ddd;border-radius:6pt}
  .tk-n{font-size:8.6pt;color:#000}
  .tk-i{width:14pt;height:14pt;opacity:1}
  /* The dark-theme rule is :root[data-theme="dark"], which out-specifies a bare
     :root whatever the media query. Print has to match that specificity or a
     reader in dark mode prints near-white ink onto white paper. */
  :root,:root[data-theme="dark"],:root[data-theme="light"]{
    --paper:#fff;--panel:#fff;--ink:#000;--ink-2:#2E2E2E;--ink-3:#6B6B6B;
    --line:#C9C9C9;--line-2:#E4E4E4;--accent:#000;--shadow:none;--glow:transparent;
    --dim:1;--dimf:none;
    color-scheme:light;
  }
  @page{margin:12mm}
  body{font-size:9.6pt;line-height:1.38;background:#fff;color:#000}
  .cord,.beam,.torch,
  .progress,.bar,.foot a,.hero-act,.scroller,.live,.tl-cta,
  .tl-i .hint,.skx-c .hint,.sec-head .lab{display:none}
  .case,.lang{break-inside:avoid}
  .cases{grid-template-columns:repeat(3,1fr);gap:10pt}
  .case{padding:9pt;border-radius:6pt}
  .case-n{font-size:18pt;margin-bottom:6pt;-webkit-text-stroke:0;color:#000}
  .case-p{padding-top:0;font-size:9.4pt}
  /* Titles only. Ten pieces at 140 words each is about three extra pages, and
     a CV that prints to seven pages because of its reading list is a CV nobody
     reaches the end of. The scroller flattens to a plain stack.

     Written against .js as well as the bare class. The screen rule that sets
     the card height is .js-prefixed, so a bare selector here loses to it and
     the sheet keeps a 505px scroller holding one piece of ten. Same
     specificity trap the skills deck hit on :first-child. */
  /* height carries !important because app.js writes the strip's height as an
     inline style to follow the card on screen, and an inline style beats a
     stylesheet rule however specific it is. Without it the sheet prints one
     card's worth of height with ten cards stacked inside it. */
  .js .wr-strip,.wr-strip{
    display:block;height:auto !important;overflow:visible;padding:0;margin-top:6pt;
  }
  .js .wr-c,.wr-c{
    display:block;width:auto;background:none;box-shadow:none;
    border:0;border-radius:0;padding:0;margin:0 0 4pt;break-inside:avoid;
  }
  .wr-c h3{font-size:10pt}
  /* Same treatment the project numeral takes on paper: solid, small and black.
     An outline numeral in the accent is a screen device and prints as a hollow
     grey shape twice the size of the title under it. */
  .wr-n{font-size:12pt;margin-bottom:2pt;-webkit-text-stroke:0;color:#000}
  .wr-k{font-size:8.4pt;color:#000;margin-top:1pt}
  /* The pieces themselves go, and the dots with them: a dot is a control for
     paging through something the paper has already laid flat. */
  .wr-t{display:none}
  .wr-dots{display:none}
  /* Paper has no pointer, so the deck is four columns with everything open, and
     the card surfaces flatten to a rule each. display and height have to be
     reset explicitly: the screen rule is a flex row of a fixed height and print
     inherits it otherwise. */
  .skx{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10pt;
       margin-top:6pt;height:auto}
  /* Same specificity problem as the stacked layout: the open state is written
     against :first-child and beats a bare .skx-c here too. */
  .skx .skx-c,.skx .skx-c:first-child{
    display:block;padding:5pt 0 0;background:none;border:0;border-radius:0;
    border-top:1px solid #C9C9C9;box-shadow:none;
  }
  .skx .skx-c .skx-n,.skx .skx-c:first-child .skx-n{
    position:static;font-size:13pt;-webkit-text-stroke:0;color:#000;
    line-height:1;opacity:1;transform:none;margin:0;
  }
  .skx .skx-c .skx-h,.skx .skx-c:first-child .skx-h{font-size:9pt;margin:2pt 0;color:#000}
  .skx .skx-c .skx-l,.skx .skx-c:first-child .skx-l{grid-template-rows:1fr;opacity:1}
  .skx-l > ul{overflow:visible;margin:0}
  .skx-c ul li{padding:3pt 0;font-size:8.2pt;line-height:1.3}
  /* Paper lays all four groups out side by side, so a pager's dots are a
     control for something that is not paging. */
  .skx-dots{display:none}
  .lang-grid{grid-template-columns:repeat(5,minmax(0,1fr));gap:6pt}
  .lang{border:1px solid #DDD;padding:7pt;gap:6pt;box-shadow:none}
  .lang .l{font-size:10pt}
  .lang .lvl{font-size:6.6pt}
  .sec-head .num{font-size:16pt;-webkit-text-stroke:0;color:#000;margin-bottom:2pt}
  .cta-row{display:none}
  /* Paper cannot hover. The detail is the substance of a work card and has
     to be on the page. */
  .more{grid-template-rows:1fr !important;opacity:1 !important}
  .more > *{overflow:visible}
  .js .rv{opacity:1 !important;transform:none !important}
  .hero::before{display:none}

  .hero{min-height:0;padding:0 0 10pt;display:block;overflow:visible;position:relative}
  .hero-in{width:72%}
  .name{font-size:29pt;margin:5pt 0 8pt;letter-spacing:-.03em}
  .name .line{display:inline-block;overflow:visible;padding:0}
  .name .line:first-child{margin-right:.2em}
  .name .n2{color:#000;-webkit-text-stroke:0}
  .name .line > span{transform:none !important;opacity:1 !important}
  .role{color:#000;margin-bottom:6pt}
  /* The seal is positioned off a measurement of the on-screen cutout, which the
     print layout moves. Education is a fact row in section 01 and prints there. */
  .lede{font-size:10.5pt;max-width:none;margin-top:0}
  .figure{position:absolute;right:0;top:0;bottom:auto;opacity:1;z-index:0;display:block}
  .figure img{height:150pt}
  .torch{display:none}

  .sec,.journey{padding:10pt 0;border-top:1px solid #C9C9C9;break-inside:avoid}
  /* The inverted band is a screen device; on paper it is a black slab. */
  #profile,:root[data-theme="dark"] #profile{
    --paper:#fff;--panel:#fff;--ink:#000;--ink-2:#2E2E2E;--ink-3:#6B6B6B;
    --line:#C9C9C9;--line-2:#E4E4E4;--accent:#000;--shadow:none;
    background:#fff;color:#000;border-top-color:#C9C9C9;
  }
  .sec-grid{grid-template-columns:104px 1fr;gap:14pt}
  /* A4 is 794px wide, inside the 920px breakpoint, so the section head is
     still the stacked mobile flex row here and the title runs over the body. */
  .sec-head{position:static;display:block}
  .sec-head .num{margin-bottom:3pt}
  .sec-head h2{font-size:11.5pt;line-height:1.14}
  .sec-head .rule,.wide-head .rule,.signoff{display:none}
  .wide-head{gap:10pt;margin-bottom:6pt}
  .wide-head .num{font-size:16pt;-webkit-text-stroke:0;color:#000}
  .wide-head h2{font-size:11.5pt}
  .sec-wide .shell > * + *{margin-top:8pt}
  .statement{font-size:11.5pt;max-width:none;line-height:1.3}
  .statement .w{opacity:1 !important}
  .profile-body{grid-template-columns:.9fr 1.1fr;gap:16pt}
  /* Paper drops every icon disc: a ring per row is screen furniture, and on a
     CV the four labels want to be a column the eye can run down. The pair goes
     back to label beside value, which is what the row was before the marks. */
  .fx-i,.rc-i{display:none}
  .sec-wide .facts li{padding:4pt 0;grid-template-columns:minmax(0,1fr);gap:0}
  .fx-b{display:grid;grid-template-columns:76px minmax(0,1fr);gap:10pt;align-items:baseline}
  .sec-end{padding:10pt 0 0;text-align:left;overflow:visible}
  /* The spine and its dots are a scroll device; on paper the four steps are
     four short entries and the numerals already order them. */
  .mt-spine{display:none}
  .mt-i::before{display:none}
  .strengths{padding-left:0}
  .mt-i{padding:5pt 0;grid-template-columns:auto minmax(0,1fr) auto;gap:10pt}
  .mt-b h3{font-size:10.5pt}
  .mt-b p{font-size:9.4pt;margin-top:2pt;max-width:none}
  .mt-out{padding:2pt 6pt;font-size:7pt}
  .mt-lab{margin-bottom:6pt}
  .end-in{grid-template-columns:1fr 1fr;gap:14pt;padding-bottom:0}
  .rc-i{display:none}
  .reach li{padding:4pt 0;grid-template-columns:minmax(0,1fr)}
  /* Wider than the facts' 76px: LOCALISATION is the longest label on the page
     and at 76 it ran into its own value. */
  .rc-b{display:grid;grid-template-columns:92px minmax(0,1fr);gap:10pt;align-items:baseline}
  .mail{background-position:0 100%}
  .sec-body > * + *{margin-top:8pt}
  .big{font-size:11pt}

  /* The timeline is already vertical; print only has to keep entries whole and
     open the details, since paper cannot hover. */
  .tl-line{background:#C9C9C9}
  .tl-line i{display:none}
  /* The line and the node keep the screen's 112px offset unless they are told
     otherwise, and at print widths that lands the node on top of the text. */
  .tl-line{left:88px}
  .tl-i{grid-template-columns:70px minmax(0,1fr);gap:26pt;padding-bottom:10pt;break-inside:avoid}
  .tl-y{padding-right:0;font-size:8.6pt}
  .tl-i::before,.is-work::before{left:83px;background:#fff;border-color:#999;transform:none !important}
  .is-work .tl-c{border:1px solid #DDD;padding:9pt;box-shadow:none}
  .is-degree .tl-c{background:none;border:0;border-left:2pt solid #000;padding:0 0 0 8pt}
  .seal{width:20px;height:20px;color:#000}
  .tl-i h3{font-size:11pt}
  .tl-cta,.say{}
  .lines{gap:5pt}
  .lines li{font-size:9.4pt;line-height:1.36}
  .mark{height:22px}
  .mark .dk{display:none !important}
  .mark .lt{display:block !important}
  .langs{margin-top:14pt;padding-top:12pt}
  .flag{width:30px;height:20px;box-shadow:0 0 0 1px rgba(0,0,0,.2) inset}
  .reach{padding-top:0}
  .cta-row{display:none}
  .langs li{break-inside:avoid}
  .mail{font-size:14pt;background-image:none}
  .foot{padding:10pt 0 0}
  
  a{color:#000;border-bottom:0 !important}
}
