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

/* The hero inside an Elementor container.
   -------------------------------------------------------------------------
   On mobile the static hero's used height is one viewport, driven by
   min-height on the stage. Its MAX-CONTENT height is about 2476px, because at
   an unconstrained width the monitor's aspect ratio grows and the editor text
   stops wrapping. As a plain block in <main> that never mattered: only the
   used height is laid out. But an Elementor container is a column flex box,
   and a flex item with flex-basis:auto is sized by its content-based
   contribution, which is the max-content figure. The hero therefore reserved
   2476px instead of 844px and left roughly 1600px of dead scroll before
   Mes Projets — about 13% of the mobile document.

   The hero is a full-bleed scene that sizes itself, so it does not need to
   participate in flex layout at all. Reproduced identically in Chromium,
   Firefox and WebKit, and fixed identically in all three. Where :has() is
   unsupported the rule simply does not apply, which is today's behaviour. */
.e-con:has(> .elementor-widget-hariri-hero),
.e-con:has(> .elementor-element > .elementor-widget-hariri-hero) { display: block; }
.elementor-widget-hariri-hero { min-height: 0; }
.hero{position:relative}
.hero__track{height:1000vh}
.hero__stage{position:sticky;top:0;height:100vh;overflow:hidden;container-type:inline-size;background:var(--canvas)}
.lay{position:absolute;inset:0;pointer-events:none}

/* The scene wrapper generates no box on desktop, so the avatar and the monitor
   keep resolving against .hero__stage and every measurement stays as it was.
   The phone composition turns it into a real frame. */
.hero__scene{display:contents}

/* The identity block belongs to the phone composition only. */
.hero__ident{display:none}

/* the room, drawn: falls away into darkness at every edge */
.lay--room{background:
  radial-gradient(58% 62% at 64% 40%,rgba(var(--c-room-3-rgb),.115),transparent 62%),
  radial-gradient(120% 92% at 50% 50%,rgba(var(--c-room-4-rgb),0) 22%,rgba(var(--c-room-5-rgb),.93) 100%),
  linear-gradient(178deg,var(--c-room) 0%,var(--c-room-2) 46%,var(--deep) 100%)}
/* the monitor's spill, the scene's only light source */
.lay--spill{left:40%;top:13%;right:auto;bottom:auto;width:50%;aspect-ratio:16/10;
  background:radial-gradient(closest-side,rgba(var(--c-spill-rgb),.30),rgba(var(--c-spill-2-rgb),.10) 58%,transparent 78%);
  filter:blur(9cqw);opacity:var(--spill,.95)}
.lay--desk{top:79%;inset-inline:0;bottom:0;
  background:linear-gradient(180deg,var(--c-desk) 0%,var(--c-desk-2) 34%,var(--c-desk-3) 100%)}
.lay--desk::before{content:"";position:absolute;inset-inline:0;top:0;height:1px;
  background:linear-gradient(90deg,transparent,rgba(var(--c-desk-sheen-rgb),.30) 34%,rgba(var(--c-desk-sheen-rgb),.16) 72%,transparent)}
.lay--desk::after{content:"";position:absolute;left:36%;right:6%;top:0;height:56%;
  background:linear-gradient(180deg,rgba(var(--c-desk-bloom-rgb),.13),transparent 74%);filter:blur(1.6cqw)}
.lay--stand{left:63.5%;top:68.6%;right:auto;bottom:auto;width:2.6%;height:11%;
  background:linear-gradient(90deg,var(--c-stand),var(--c-stand-2) 42%,var(--c-stand-3));border-radius:0 0 2px 2px}
.lay--stand::after{content:"";position:absolute;left:-190%;right:-190%;bottom:0;height:26%;border-radius:44%;
  background:linear-gradient(180deg,var(--c-stand-base),var(--c-stand-base-2))}
.lay--keyboard{left:8%;top:84.5%;right:auto;bottom:auto;width:26%;height:6.4%;border-radius:3px;
  background:linear-gradient(180deg,var(--c-keys),var(--c-keys-2));
  outline:1px solid rgba(var(--c-keys-3-rgb),.12);outline-offset:-1px}
.lay--keyboard::before{content:"";position:absolute;inset:12% 6%;border-radius:2px;
  background:repeating-linear-gradient(90deg,rgba(var(--c-keys-sheen-rgb),.10) 0 1.5%,transparent 1.5% 4.6%)}
.lay--keyboard::after{content:"";position:absolute;left:-8%;right:-8%;bottom:-38%;height:38%;
  background:linear-gradient(180deg,rgba(var(--c-keys-shadow-rgb),.34),transparent 62%)}
.lay--dust,.lay--frag{width:100%;height:100%}
.lay--frag{z-index:2}
/* the fragment field's inner surface. Mounted inside .screen by fragments.js, so
   the screen's own overflow does the clipping and the bezel reads as a threshold
   rather than a lid. Sized and offset from JS to share the stage's coordinates. */
.frag-in{position:absolute;inset:0;width:100%;height:100%;z-index:6;pointer-events:none;max-width:none}
.lay--scrim{background:radial-gradient(ellipse 122% 94% at 52% 46%,rgba(var(--c-scrim-rgb),0) 36%,rgba(var(--c-scrim-rgb),.66) 100%);z-index:4}

/* the avatar: the only generated asset. Its own layer, so it parallaxes and
   breathes independently. Position comes from measured constants in JS. */
.lay--avatar{z-index:3;will-change:transform;
  -webkit-mask-image:linear-gradient(180deg,#000 0%,#000 92%,transparent 100%);
          mask-image:linear-gradient(180deg,#000 0%,#000 92%,transparent 100%)}
/* the grade and the contact shadow both come from the avatar's own config, so a
   different asset does not inherit corrections tuned for the previous one */
.lay--avatar img,.lay--avatar canvas{width:100%;height:auto;display:block}
/* The figure is grounded by the mask fade above, not by a cast shadow. */

/* the monitor */
.lay--monitor{left:40%;top:13%;right:auto;bottom:auto;width:50%;aspect-ratio:16/10;z-index:5;
  pointer-events:auto;transform:perspective(1400px) rotateY(-6deg);transform-origin:80% 50%}
/* The hardware lives on a pseudo-element so the whole frame (gradient, ring,
   shadow, radius) can fade out independently while the screen keeps its content
   above it. Padding and radius are driven so the bezel collapses to nothing. */
.mon{position:relative;width:100%;height:100%;padding:var(--mon-pad,clamp(3px,.4cqw,8px))}
.mon::before{content:"";position:absolute;inset:0;z-index:0;pointer-events:none;
  border-radius:var(--mon-radius,7px);opacity:var(--mon-bezel,1);
  background:linear-gradient(158deg,var(--c-mon-bezel),var(--c-mon-bezel-2));
  /* The lit edge of the bezel, as an outline: geometry, not a shadow. */
  outline:1px solid rgba(var(--c-mon-bezel-3-rgb),.16);outline-offset:-1px;
  /* The panel's own emission. A tight core so the screen reads as backlit right
     at its edge, then a wide low stop carrying the falloff into the room with
     only a slight downward bias -- light on the desk, no rectangular footprint.
     opacity:var(--mon-bezel) fades this with the bezel during the portal
     expansion, which is correct: once the screen fills the frame there is no
     outside left to light. */
  box-shadow:0 0 18px rgba(var(--c-mon-bezel-4-rgb),.26),
             0 8px 60px rgba(var(--c-mon-bezel-4-rgb),.13)}
.screen{position:relative;z-index:1;width:100%;height:100%;overflow:hidden;
  border-radius:var(--screen-radius,3px);background:var(--c-screen);display:flex;flex-direction:column}

/* While the screen is taking over the viewport it has to sit above the room,
   the desk, the dust and the avatar, and the wordmark has to stay above it. */
.hero__stage.is-portal .lay--monitor{z-index:7}
.hero__stage.is-portal .wordmark{z-index:9}
.screen__tabs{display:flex;background:var(--c-tabs);border-bottom:1px solid var(--panel-2);flex:none;overflow:hidden}
/* Type inside the monitor is sized so the editor reads like a real editor at
   any stage width, rather than scaling to billboard size on a wide screen. */
.tab{font-family:var(--mono);font-size:clamp(9.5px,.86cqw,17px);padding:.62em .95em;color:var(--c-tab);
  border-right:1px solid var(--c-tab-2);white-space:nowrap;transition:color .3s var(--ease),background .3s var(--ease)}
/* the active tab's 1px marker, as a background rule rather than an inset shadow */
.tab.is-on{color:var(--c-tab-on);
  background:linear-gradient(var(--accent),var(--accent)) top/100% 1px no-repeat,var(--c-tab-on-2)}
.tab.is-done{color:var(--c-tab-done)}
.screen__body{flex:1;position:relative;min-height:0}
.editor{position:absolute;inset:0;display:flex;font-family:var(--mono);
  font-size:clamp(10px,.95cqw,19px);line-height:1.64;
  opacity:var(--edop,1);transition:opacity .35s var(--ease)}
.editor__gutter{flex:none;padding:.9em .8em 0;color:var(--c-gutter);text-align:right;
  background:var(--c-gutter-2);border-right:1px solid var(--c-gutter-3);white-space:pre;font-variant-numeric:tabular-nums}
.editor__code{flex:1;padding:.9em 1.2em;min-width:0;overflow:hidden;color:var(--tok-att);white-space:pre}
.editor__code .l{display:block;white-space:pre}
.k{color:var(--tok-key)}.s{color:var(--tok-str)}.c{color:var(--tok-com)}.n{color:var(--tok-num)}
.t{color:var(--tok-tag)}.a{color:var(--tok-att)}.p{color:var(--tok-pun)}.f{color:var(--tok-fn)}
.car{display:inline-block;width:.5em;height:1.02em;background:var(--accent);vertical-align:-.15em;
  animation:car 1.05s steps(1,end) infinite}
@keyframes car{0%,49%{opacity:1}50%,100%{opacity:0}}

/* the integrated terminal: capabilities arrive as output and stay, so it is a
   log rather than a carousel */
/* Vertical padding lives on the children, so a collapsed terminal is genuinely
   zero pixels tall rather than a padded sliver showing its first line. */
.term{position:absolute;left:0;right:0;bottom:0;background:var(--c-term);border-top:1px solid var(--c-term-2);
  padding:0;font-family:var(--mono);font-size:clamp(9.5px,.88cqw,17px);line-height:1.74;
  height:var(--termh,0%);overflow:hidden;transition:none}
.term__cmd{margin:.9em 1.4em .3em;color:var(--tok-att)}
.term__p{color:var(--ok)}
.term__list{list-style:none;margin:0;padding:0 1.4em .9em}
.term__list li{color:var(--ok);white-space:nowrap;overflow:hidden;
  opacity:var(--o,0);transform:translateX(calc((1 - var(--o,0)) * -.6em));
  clip-path:inset(0 calc((1 - var(--w,0)) * 100%) 0 0)}
.term__list li::before{content:"✓ ";color:var(--accent)}

/* the identity card */
.idcard{position:absolute;inset:0;display:flex;flex-direction:column;justify-content:center;
  padding:0 3cqw;opacity:var(--idop,0);pointer-events:none;background:linear-gradient(180deg,var(--c-idcard),var(--c-idcard-2))}
.idcard.is-live{pointer-events:auto}
.idcard__kick{font-family:var(--mono);font-size:clamp(9px,.8cqw,15px);letter-spacing:.14em;
  color:var(--accent);margin:0 0 1.1em;opacity:var(--k1,0)}
.idcard__name{font-weight:800;font-variation-settings:'wdth' 112;
  font-size:clamp(22px,3.1cqw,62px);line-height:1;letter-spacing:-.03em;margin:0 0 .22em}
.idcard__role{font-family:var(--mono);font-size:clamp(11px,1.05cqw,21px);color:var(--text-2);
  margin:0 0 1.8em;opacity:var(--k2,0);transform:translateY(calc((1 - var(--k2,0)) * .5em))}
.idcard__act{display:flex;gap:clamp(7px,.7cqw,14px);flex-wrap:wrap;
  opacity:var(--k3,0);transform:translateY(calc((1 - var(--k3,0)) * .8em))}
.idcard__act .btn{font-size:clamp(10.5px,.82cqw,15px);padding:1em 1.7em;border-radius:.7em}

/* the wordmark, formed in front of the monitor from the code that compiled */
/* Deliberately wider than the screen that produced it: the glyphs left the
   monitor, so the wordmark reads as being in front of it, not inside it. */
.wordmark{position:absolute;left:52%;top:41%;transform:translate(-50%,-50%);margin:0;z-index:6;
  font-weight:800;font-variation-settings:'wdth' 118;font-size:clamp(46px,10.4cqw,205px);
  line-height:1;letter-spacing:-.045em;
  white-space:nowrap;opacity:var(--wm,0);pointer-events:none}
.wordmark .ch{display:inline-block;
  opacity:var(--kc,0);
  transform:translate(calc((1 - var(--kc,0)) * var(--jx,0px)),calc((1 - var(--kc,0)) * var(--jy,0px)))
            rotate(calc((1 - var(--kc,0)) * var(--jr,0deg)))}
/* The dot in hariri.ch is the seed of the fil d'exécution. The glyph carries the
   colour; the pseudo-element marks the ignition. It is a hard-edged ring that
   opens with --seed rather than a bloom, so the narrative beat survives without
   a glow. */
.wordmark .dot{color:var(--ember);position:relative}
.wordmark .dot::after{content:"";position:absolute;left:30%;top:67%;width:.50em;height:.50em;
  margin:-.17em 0 0 -.17em;border-radius:50%;pointer-events:none;opacity:var(--seed,0);
  border:2px solid var(--ember);
  transform:scale(calc(.5 + .5 * var(--seed,0)))}

/* the caption bands, in the calm column left of the figure */
.bands{position:absolute;left:4%;top:46%;width:29%;z-index:6}
.band{position:absolute;left:0;top:0;width:100%;opacity:var(--o,0);
  pointer-events:none}
/* Peak alpha is tuned against the WORST pixel behind the text, not the average.
   The avatar's light sweater stripe grazes the caption box, which took the
   measured worst case to 3.3:1. Deepened until it clears the 3.5:1 floor with
   real margin. The gradient still dies out by 76 %, so the corners stay live. */
.band::before{content:"";position:absolute;inset:-16% -20%;z-index:-1;pointer-events:none;
  opacity:calc(.25 + .75 * var(--k,1));
  background:radial-gradient(ellipse 78% 66% at 50% 50%,rgba(var(--c-band-veil-rgb),.82) 0%,rgba(var(--c-band-veil-rgb),.58) 48%,rgba(var(--c-band-veil-rgb),0) 78%)}
.band__k{font-family:var(--mono);font-size:clamp(10px,.82cqw,15px);letter-spacing:.14em;
  text-transform:uppercase;color:var(--accent);margin:0 0 1em}
.band__h{font-weight:700;font-variation-settings:'wdth' 104;font-size:clamp(25px,2.15cqw,46px);
  line-height:1.08;letter-spacing:-.022em;margin:0 0 .42em;color:var(--text)}
.band__s{font-size:clamp(14px,1.02cqw,19px);line-height:1.45;color:var(--c-band-sub);margin:0}
/* per-character and per-word assembly, transform and opacity only */
.band__h .w{display:inline-block}
.band__h .ch{display:inline-block;
  --kc:clamp(0,(var(--k,0) - var(--th,0)) * 2.6,1);
  opacity:var(--kc);
  transform:translate(calc((1 - var(--kc)) * var(--jx,0px)),calc((1 - var(--kc)) * var(--jy,0px)))
            rotate(calc((1 - var(--kc)) * var(--jr,0deg)))}

/* the designed mouse */
.mouse{position:absolute;right:15%;bottom:5.5%;width:6.6%;z-index:8;pointer-events:none}
.mouse svg{width:100%;height:auto}
/* Contact with the desk, tightened to read as occlusion rather than a halo.
   Scoped away from the floating mouse below, which rests on nothing. */
.mouse::after{content:"";position:absolute;left:-6%;right:-6%;bottom:-3%;height:9%;z-index:-1;
  background:radial-gradient(ellipse 50% 50% at 50% 50%,rgba(var(--c-mouse-shadow-rgb),.34),transparent 70%)}
.mouse--fixed{position:fixed;right:26px;bottom:26px;width:62px;z-index:60;opacity:0;
  /* opacity only. The body's transform is written every frame from the scroll
     position, so a transition on it would lag the scroll and break reversal. */
  transition:opacity .5s var(--ease);will-change:transform}
.mouse--fixed.is-on{opacity:.92}
.mouse--fixed.is-quiet{opacity:.4}
.mouse--fixed::after{display:none}
@media (max-width:900px),(max-height:620px){.mouse--fixed{display:none}}

.cue{position:absolute;left:50%;bottom:3%;transform:translateX(-50%);margin:0;z-index:8;
  font-family:var(--mono);font-size:11px;letter-spacing:.2em;text-transform:uppercase;color:var(--text-2);
  display:flex;flex-direction:column;align-items:center;gap:9px;opacity:var(--cue,1);transition:opacity .4s var(--ease)}
.cue i{display:block;width:1px;height:34px;background:linear-gradient(180deg,var(--accent),transparent);
  animation:cue 2.4s var(--ease-soft) infinite}
@keyframes cue{0%{transform:scaleY(.2);transform-origin:50% 0}50%{transform:scaleY(1);transform-origin:50% 0}
  51%{transform:scaleY(1);transform-origin:50% 100%}100%{transform:scaleY(.2);transform-origin:50% 100%}}
@media (max-height:620px){.cue{display:none}}

/* ============================ the fil d'exécution ============================ */
.rail{position:absolute;left:0;top:0;width:100%;height:100%;z-index:0;pointer-events:none;overflow:visible}
.rail__base{stroke:var(--line);stroke-width:1.4;vector-effect:non-scaling-stroke}
.rail__live{stroke:url(#railgrad);stroke-width:2;vector-effect:non-scaling-stroke;
  stroke-linecap:round}

/* ============================ PROJETS ============================ */
.sec--projects{max-width:none;padding-inline:0}
.sec--projects .sec__head{max-width:var(--maxw);margin-inline:auto;padding-inline:var(--gut)}
.projects{display:flex;flex-direction:column;gap:clamp(70px,9vw,140px)}
.project{max-width:var(--maxw);margin:0 auto;width:100%;padding:0 var(--gut);
  display:grid;gap:clamp(22px,3vw,46px);align-items:center;
  grid-template-columns:minmax(0,1.15fr) minmax(0,.85fr)}
.project:nth-child(even){grid-template-columns:minmax(0,.85fr) minmax(0,1.15fr)}
.project:nth-child(even) .project__view{order:2}
.project:nth-child(even) .project__data{order:1}

/* the route to the full index, under the last card. It carries the section's
   own gutter because it sits outside .projects, which has none of its own. */
.projects__more{max-width:var(--maxw);margin:clamp(46px,5.5vw,86px) auto 0;
  padding:0 var(--gut);display:flex;justify-content:center}

/* the browser frame, whose real full-page capture scrolls inside it */
.project__view{perspective:1800px}
.frame{position:relative;border-radius:11px;overflow:hidden;background:var(--panel);border:1px solid var(--line);
  transform:rotateY(calc(var(--tilt,0) * 1deg)) rotateX(calc(var(--tiltx,0) * 1deg));
  transform-style:preserve-3d}
.frame__bar{display:flex;align-items:center;gap:9px;padding:9px 12px;background:var(--c-frame-bar);
  border-bottom:1px solid var(--line-soft)}
.frame__dots{display:flex;gap:5px;flex:none}
.frame__dots i{width:8px;height:8px;border-radius:50%;background:var(--c-frame-dot);display:block}
.frame__url{flex:1;min-width:0;font-family:var(--mono);font-size:11px;color:var(--text-2);
  background:var(--c-frame-url);border:1px solid var(--line-soft);border-radius:20px;padding:4px 12px;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* A real scroll container. Native semantics are kept for touch and keyboard;
   the scrollbar is hidden, never the scrolling itself. The capture fills the
   width at its natural ratio and is never cropped by object-fit. */
.frame__win{position:relative;aspect-ratio:16/10;background:var(--c-frame-win);
  overflow-y:auto;overflow-x:hidden;scrollbar-width:none;-ms-overflow-style:none;
  /* a lazy capture arriving must not shove the inner position around */
  overflow-anchor:none}
.frame__win::-webkit-scrollbar{width:0;height:0}
.frame__win:focus-visible{outline:2px solid var(--accent);outline-offset:-3px}
.frame__page{display:block}
.frame__page img{display:block;width:100%;height:auto}
/* small screens show the real mobile capture, which is portrait */
@media (max-width:900px){ .frame__win{aspect-ratio:4/5} }

.frame__glare{position:absolute;inset:0;pointer-events:none;z-index:2;
  background:linear-gradient(103deg,rgba(var(--c-frame-glare-rgb),.10),transparent 42%)}

.frame__hint{position:absolute;left:50%;bottom:14px;transform:translateX(-50%);margin:0;z-index:3;
  font-family:var(--mono);font-size:11.5px;letter-spacing:.05em;color:var(--text-2);white-space:nowrap;
  background:rgba(var(--c-frame-hint-rgb),.96);border:1px solid var(--line);border-radius:20px;padding:7px 14px;
  pointer-events:none;opacity:1;transition:opacity .45s var(--ease)}
.frame.is-used .frame__hint{opacity:0}
.hint--coarse{display:none}
@media (pointer:coarse){ .hint--fine{display:none} .hint--coarse{display:inline} }

.project__data{display:flex;flex-direction:column;gap:14px}
.project__n{font-family:var(--mono);font-size:11.5px;letter-spacing:.16em;color:var(--ember)}
.project__t{font-weight:700;font-variation-settings:'wdth' 108;font-size:clamp(24px,2.7vw,36px);
  line-height:1.06;letter-spacing:-.022em;margin:0}
.project__meta{display:flex;flex-wrap:wrap;gap:6px}
.chip{font-family:var(--mono);font-size:10.5px;letter-spacing:.05em;padding:4px 10px;border-radius:20px;
  border:1px solid var(--line);color:var(--text-2);white-space:nowrap}
.chip--role{border-color:var(--accent-mute);color:var(--accent-hi);background:var(--accent-mute)}
.project__d{color:var(--text-2);margin:0;font-size:clamp(14.5px,1.05vw,16px)}
/* With JavaScript this block is only a source: panel.js clones it into the fixed
   dialog. Without JavaScript the trigger is a plain link to it and :target opens
   it in place, so the control is never dead. */
.project__fold{display:grid;grid-template-rows:0fr;transition:grid-template-rows .5s var(--ease)}
.project__fold:target{grid-template-rows:1fr;margin-top:18px}
.project__foldin{overflow:hidden;min-height:0}
.project__list{list-style:none;margin:14px 0 0;padding:0;display:grid;gap:7px}
.project__list li{position:relative;padding-left:17px;font-size:14px;color:var(--text-2)}
.project__list li::before{content:"";position:absolute;left:0;top:.62em;width:7px;height:1px;background:var(--accent)}
.project__act{display:flex;gap:9px;flex-wrap:wrap;margin-top:6px}

@media (max-width:900px){
  .project,.project:nth-child(even){grid-template-columns:1fr}
  .project:nth-child(even) .project__view{order:0}
  .project:nth-child(even) .project__data{order:0}
  .frame{transform:none !important}
}

/* ============================ SERVICES ============================ */
.rack{list-style:none;margin:0;padding:0;position:relative;display:grid;gap:clamp(14px,2vw,26px)}
.rack::before{content:"";position:absolute;left:50%;top:0;bottom:0;width:1px;background:var(--line);transform:translateX(-.5px)}
.mod{position:relative;display:grid;grid-template-columns:auto minmax(0,1fr);gap:clamp(14px,2vw,24px);
  align-items:start;background:var(--panel);border:1px solid var(--line);border-radius:14px;
  padding:clamp(18px,2.2vw,28px);width:calc(50% - clamp(18px,2.4vw,34px));
  opacity:0;transform:translateY(24px);
  transition:opacity .6s var(--ease),transform .6s var(--ease),border-color .35s var(--ease)}
.mod:nth-child(even){margin-left:auto}
.mod.is-in{opacity:1;transform:none}
.mod:hover{border-color:var(--accent-mute)}
/* the hairline that docks each module onto the thread */
.mod::before{content:"";position:absolute;top:clamp(28px,3vw,40px);width:clamp(18px,2.4vw,34px);height:1px;background:var(--line)}
.mod:nth-child(odd)::before{right:calc(-1 * clamp(18px,2.4vw,34px))}
.mod:nth-child(even)::before{left:calc(-1 * clamp(18px,2.4vw,34px))}
.mod::after{content:"";position:absolute;top:calc(clamp(28px,3vw,40px) - 3.5px);width:7px;height:7px;border-radius:50%;
  background:var(--canvas);border:1px solid var(--line);transition:background .4s var(--ease),border-color .4s var(--ease)}
.mod:nth-child(odd)::after{right:calc(-1 * clamp(18px,2.4vw,34px) - 3.5px)}
.mod:nth-child(even)::after{left:calc(-1 * clamp(18px,2.4vw,34px) - 3.5px)}
/* an outline, not a shadow: the same ring, stated as the indicator it is */
.mod.is-in::after{background:var(--accent);border-color:var(--accent);
  outline:4px solid var(--accent-mute)}
.mod__glyph{width:clamp(40px,4vw,54px);height:clamp(40px,4vw,54px);flex:none;
  border:1px solid var(--line);border-radius:11px;background:var(--c-mod-glyph);display:grid;place-items:center}
.mod__glyph svg{width:60%;height:60%;stroke:var(--accent);fill:none;stroke-width:1.5;
  stroke-linecap:round;stroke-linejoin:round}
.mod__t{font-weight:600;font-variation-settings:'wdth' 104;font-size:clamp(16.5px,1.5vw,20px);
  margin:0 0 8px;letter-spacing:-.014em}
.mod__d{margin:0;color:var(--text-2);font-size:clamp(14px,1vw,15.5px)}
@media (max-width:820px){
  /* One column docked to a spine on the left. width:auto matters: the desktop
     rule sets an explicit width, and keeping it here alongside the indent made
     each card its full container width PLUS the 34px margin, so every one ran
     off the right edge and was clipped. A grid item stretches on its own. */
  .rack::before{left:16px}
  /* The alternation is off here, so both halves of it are reset together: the
     even rule that pushes cards right on desktop outranks a bare .mod. */
  .mod,.mod:nth-child(even),.mod:nth-child(odd){width:auto;margin-left:34px;margin-right:0}
  .mod::before{left:-34px !important;right:auto !important;width:34px}
  .mod::after{left:calc(-34px - 3.5px) !important;right:auto !important}
}

/* ============================ TECHNOLOGIES ============================ */
.graph{position:relative;aspect-ratio:1000/540;min-height:360px}
.graph__label{position:absolute;transform:translate(-50%,-50%);font-family:var(--mono);font-size:11px;
  letter-spacing:.16em;text-transform:uppercase;color:var(--muted);white-space:nowrap;pointer-events:none}
.graph__edges{position:absolute;inset:0;width:100%;height:100%}
.graph__edges path{stroke:var(--line);stroke-width:1;fill:none;vector-effect:non-scaling-stroke;
  stroke-dasharray:var(--len,200);stroke-dashoffset:calc(var(--len,200) * (1 - var(--draw,0)))}
.graph__nodes{list-style:none;margin:0;padding:0;position:absolute;inset:0}
/* The cluster wrappers carry the small-screen layout. Here they generate no
   boxes, so every node still positions against .graph__nodes as before. */
.graph__cluster,.graph__row{display:contents}
.graph__group{display:none}
.node{position:absolute;transform:translate(-50%,-50%);display:flex;flex-direction:column;align-items:center;gap:7px;
  transition:transform .6s var(--ease),opacity .6s var(--ease)}
.node__box{width:clamp(40px,4.4vw,58px);height:clamp(40px,4.4vw,58px);border-radius:13px;
  border:1px solid var(--line);background:var(--panel);display:grid;place-items:center;
  transition:border-color .3s var(--ease),background .3s var(--ease),transform .3s var(--ease)}
.node__box svg{width:52%;height:52%;fill:var(--text-2);transition:fill .3s var(--ease)}
.node__box svg.stroked{fill:none;stroke:var(--text-2);stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
.node__n{font-family:var(--mono);font-size:11px;color:var(--text-2);white-space:nowrap;
  transition:color .3s var(--ease)}
.node:hover .node__box,.node:focus-within .node__box{border-color:var(--accent);background:var(--accent-mute);transform:translateY(-3px)}
.node:hover .node__box svg,.node:focus-within .node__box svg{fill:var(--accent-hi)}
.node:hover .node__box svg.stroked,.node:focus-within .node__box svg.stroked{fill:none;stroke:var(--accent-hi)}
.node:hover .node__n,.node:focus-within .node__n{color:var(--text)}
.graph__legend{display:flex;flex-wrap:wrap;gap:10px 22px;margin:26px 0 0;padding:0;list-style:none;
  font-family:var(--mono);font-size:11.5px;color:var(--muted)}
.graph__legend b{color:var(--text-2);font-weight:400}
@media (max-width:860px){
  /* Grouped rows instead of a constellation. The ring coordinates mean nothing
     at this width, so the edges go and the nodes return to normal flow, gathered
     under their cluster's name. Two per row on a phone, more as room allows. */
  .graph{aspect-ratio:auto;min-height:0}
  .graph__edges{display:none}
  .graph__label{display:none}                 /* the absolute desktop caption */
  .graph__nodes{position:static;display:grid;gap:clamp(20px,5vw,30px)}

  .graph__cluster{display:block}
  .graph__group{display:block;font-family:var(--mono);font-size:11px;letter-spacing:.14em;
    text-transform:uppercase;color:var(--accent);margin:0 0 10px;
    padding-bottom:8px;border-bottom:1px solid var(--line)}

  /* Chips size to their own name and wrap, so "PHP" costs a third of what
     "Frappe Framework" does instead of both filling an identical grid cell.
     Nothing here is interactive -- no link, no control -- so these are labels,
     not touch targets, and they do not need the 44px minimum. */
  .graph__row{display:flex;flex-wrap:wrap;gap:7px}

  .node{position:static !important;transform:none !important;flex-direction:row;
    align-items:center;justify-content:flex-start;gap:8px;
    padding:6px 11px 6px 8px;background:var(--panel);
    border:1px solid var(--line);border-radius:999px}
  .node__box{width:22px;height:22px;border-radius:6px;border-color:transparent;background:transparent}
  .node__box svg{width:100%;height:100%}
  .node__n{font-size:11.5px;white-space:nowrap;line-height:1}
}

/* ============================ FLUX DE TRAVAIL ============================ */
.pipe{list-style:none;margin:0;padding:0;position:relative;display:grid;gap:clamp(16px,2.4vw,30px)}
.pipe::before{content:"";position:absolute;left:clamp(19px,2.2vw,27px);top:12px;bottom:12px;width:1px;background:var(--line)}
.pipe::after{content:"";position:absolute;left:clamp(19px,2.2vw,27px);top:12px;width:1px;
  height:calc(var(--flow,0) * 100%);max-height:calc(100% - 24px);
  background:linear-gradient(180deg,var(--ember),var(--accent) 30%,var(--accent))}
.step{position:relative;display:grid;grid-template-columns:auto minmax(0,1fr);gap:clamp(16px,2.4vw,30px);
  align-items:start;padding-left:0;opacity:.42;transition:opacity .5s var(--ease)}
.step.is-live{opacity:1}
.step__n{width:clamp(38px,4.4vw,54px);height:clamp(38px,4.4vw,54px);flex:none;border-radius:50%;
  display:grid;place-items:center;font-family:var(--mono);font-size:14px;
  background:var(--canvas);border:1px solid var(--line);color:var(--text-2);position:relative;z-index:1;
  transition:border-color .45s var(--ease),color .45s var(--ease),background .45s var(--ease),outline-color .45s var(--ease)}
.step.is-live .step__n{border-color:var(--accent);color:var(--text);background:var(--panel);
  outline:5px solid var(--accent-mute)}
.step__b{padding-top:clamp(4px,.8vw,10px)}
.step__t{font-weight:600;font-variation-settings:'wdth' 104;font-size:clamp(17px,1.6vw,22px);
  margin:0 0 8px;letter-spacing:-.014em}
.step__d{margin:0;color:var(--text-2);max-width:58ch;font-size:clamp(14px,1vw,15.5px)}

/* ============================ BLOG ============================ */
.posts{display:grid;gap:clamp(18px,2.4vw,30px);grid-template-columns:repeat(auto-fit,minmax(min(100%,290px),1fr))}
.posts .post{display:flex;flex-direction:column;background:var(--panel);border:1px solid var(--line);
  border-radius:14px;overflow:hidden;opacity:0;transform:translateY(20px);
  transition:opacity .55s var(--ease),transform .55s var(--ease),border-color .3s var(--ease)}
.posts .post.is-in{opacity:1;transform:none}
.posts .post:hover{border-color:var(--accent-mute)}
.post__img{aspect-ratio:16/9;background:linear-gradient(150deg,var(--c-thumb),var(--c-thumb-2));position:relative;overflow:hidden}
.post__img img{width:100%;height:100%;object-fit:cover}
.post__img.is-ph::after{content:"";position:absolute;inset:0;
  background:radial-gradient(58% 62% at 66% 34%,rgba(var(--accent-rgb),.20),transparent 68%),
             repeating-linear-gradient(115deg,rgba(var(--c-thumb-mark-rgb),.05) 0 2px,transparent 2px 12px)}
.post__b{padding:clamp(16px,2vw,22px);display:flex;flex-direction:column;gap:10px;flex:1}
.post__date{font-family:var(--mono);font-size:11px;letter-spacing:.1em;color:var(--ember);margin:0}
.post__t{font-weight:600;font-variation-settings:'wdth' 102;font-size:clamp(16px,1.35vw,19px);
  line-height:1.28;letter-spacing:-.014em;margin:0}
.post__t a{color:var(--text);text-decoration:none}
.post__t a:hover{color:var(--accent-hi)}
.post__x{margin:0;color:var(--text-2);font-size:14px}
.post__more{margin-top:auto;font-family:var(--mono);font-size:12px;color:var(--accent-hi);
  text-decoration:none;display:inline-flex;align-items:center;gap:7px}
.post__more::after{content:"→";transition:transform .3s var(--ease)}
.post__more:hover::after{transform:translateX(4px)}
.posts__all{margin:clamp(26px,3vw,40px) 0 0}

/* ============================ CONTACT ============================ */
.form{display:grid;gap:clamp(14px,1.8vw,22px);grid-template-columns:1fr 1fr;max-width:860px}
.fld{display:flex;flex-direction:column;gap:8px;min-width:0}
.fld--wide{grid-column:1/-1}
.fld label{font-family:var(--mono);font-size:12px;letter-spacing:.05em;color:var(--text-2)}
.fld input,.fld textarea{
  font-family:var(--display);font-size:16px;color:var(--text);
  background:var(--c-field);border:1px solid var(--line);border-radius:10px;padding:13px 15px;
  transition:border-color .25s var(--ease),background .25s var(--ease)}
.fld textarea{resize:vertical;min-height:150px;line-height:1.55}
.fld input:hover,.fld textarea:hover{border-color:var(--c-field-hover)}
.fld input:focus,.fld textarea:focus{outline:none;border-color:var(--accent);background:var(--c-field-focus)}
/* The border swap above is the form's own language; keyboard focus additionally
   gets a real outline, matching the global :focus-visible treatment. */
.fld input:focus-visible,.fld textarea:focus-visible{
  outline:2px solid var(--accent);outline-offset:2px}
.fld.is-bad input,.fld.is-bad textarea{border-color:var(--bad)}
.err{margin:0;min-height:0;font-family:var(--mono);font-size:11.5px;color:var(--bad);
  max-height:0;overflow:hidden;transition:max-height .3s var(--ease)}
.fld.is-bad .err{max-height:3em}
/* Visually hidden without leaving the layout: no negative offset that could
   widen the page, no blank vertical space, and still submitted by a bot. */
.hp{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;
  clip-path:inset(50%);white-space:nowrap;border:0}
.form__foot{flex-direction:row;align-items:center;gap:16px;flex-wrap:wrap}
.btn--send{position:relative;min-width:150px;padding-block:14px}
.btn--send .spin{width:17px;height:17px;display:none;animation:spin .9s linear infinite}
.form.is-sending .btn--send .spin{display:block}
.form.is-sending .btn--send{pointer-events:none;opacity:.75}
@keyframes spin{to{transform:rotate(360deg)}}
.form__note{margin:0;font-family:var(--mono);font-size:11.5px;color:var(--muted);max-width:44ch}
/* The region carries its padding and border-width at all times, with the border
   transparent while empty, so the answer only changes colour -- the contact
   section keeps exactly the same height before and after a submission. */
.form__live{grid-column:1/-1;margin:0;font-family:var(--mono);font-size:13px;
  padding:14px 16px;border:1px solid transparent;border-radius:10px;
  /* line-height pinned so the empty box and the filled box are the same height */
  line-height:1.5;min-height:calc(1.5em + 30px)}
/* On a narrow column the answer wraps onto a second line, so the reservation has
   to allow for two. Measured at 390px, where a single line was 20px short. */
@media (max-width:620px){ .form__live{min-height:calc(3em + 30px)} }
.form__live.is-ok{color:var(--ok);border-color:rgba(var(--tok-str-rgb),.3);
  background:rgba(var(--tok-str-rgb),.08)}
.form__live.is-bad{color:var(--bad);border-color:rgba(var(--bad-rgb),.3);
  border-radius:10px;background:rgba(var(--bad-rgb),.07)}
@media (max-width:720px){.form{grid-template-columns:1fr}}


/* ============================================================================
   THE FIVE STATIC-HERO GATES
   These five strings are duplicated character for character in main.js.
   If they ever drift apart, one side loads assets the other side hides.
   ========================================================================== */
@media (max-width:720px),
       (orientation:portrait) and (max-width:1024px),
       (orientation:portrait) and (pointer:coarse),
       (orientation:landscape) and (pointer:coarse) and (max-height:560px),
       (prefers-reduced-motion:reduce)
{
  .hero__track{height:auto}
  .hero__stage{position:static;height:auto;min-height:100svh;display:flex;flex-direction:column;
    justify-content:center;gap:clamp(18px,4vw,30px);padding:104px var(--gut) 44px}

  /* the same narrative order as the scrub, top to bottom:
     disorder, then the compiled system, then hariri.ch */
  .bands{order:1}
  .lay--monitor{order:2}
  .wordmark{order:3}

  .lay--spill{left:14%;top:12%;width:78%;filter:blur(16cqw)}
  .lay--stand,.lay--keyboard,.lay--frag,.frag-in,.mouse,.cue{display:none}
  .lay--desk{top:auto;height:26%}

  /* placement comes from the avatar's own small block in config.js, anchored
     on the head, so a full-body asset never crops to its feet here */

  .lay--monitor{position:relative;left:auto;top:auto;width:min(100%,540px);margin:0;
    transform:none;aspect-ratio:auto;z-index:4}
  .lay--monitor .screen{min-height:0}
  .screen__body{min-height:clamp(230px,44vw,300px)}

  /* the static hero is a designed layout, not an apology: the settle copy is
     already in place, so the message lands with no scroll required */
  .bands{position:relative;left:auto;top:auto;width:100%;z-index:5;height:auto}
  .band{position:absolute;opacity:0 !important;pointer-events:none}
  .band:first-child{position:relative;opacity:1 !important}
  .band__k{font-size:11.5px}
  .band__h{font-size:clamp(28px,7.4vw,44px)}
  .band__s{font-size:clamp(14.5px,3.4vw,17px)}
  .band__h .ch{opacity:1 !important;transform:none !important}

  .wordmark{position:relative;left:auto;top:auto;transform:none;font-size:clamp(40px,12.5vw,72px);
    opacity:1 !important;margin:0;text-align:left}
  .wordmark .ch{opacity:1 !important;transform:none !important}
  .wordmark .dot::after{opacity:1}

  .editor{display:none}
  .term{position:relative;height:auto !important;border-top:0;font-size:12px;padding:14px 16px}
  .term__list li{opacity:1 !important;transform:none !important;clip-path:none !important;
    white-space:normal;overflow:visible}
  .idcard{position:relative;inset:auto;opacity:1 !important;pointer-events:auto;background:none;
    padding:16px 0 0}
  .idcard__kick{display:none}
  .idcard__name{font-size:clamp(24px,6vw,34px);opacity:1}
  .idcard__role{font-size:13px;opacity:1 !important;transform:none !important;margin-bottom:18px}
  .idcard__act{opacity:1 !important;transform:none !important;gap:10px}
  .idcard__act .btn{font-size:13px;padding:13px 20px;border-radius:9px}
  .screen__tabs .tab{font-size:10px;padding:7px 9px}
  .screen__status{font-size:9.5px}
}

.screen__status{flex:none;display:flex;gap:1.5em;padding:.65em 1.3em;background:var(--c-status);
  border-top:1px solid var(--c-status-2);font-family:var(--mono);font-size:clamp(9px,.78cqw,15px);color:var(--tok-com)}
.screen__status .ok{color:var(--ok)}
.screen__status .sp{margin-left:auto}

/* ============================ reduced motion ============================ */
@media (prefers-reduced-motion:reduce){
  html{scroll-behavior:auto}
  *,*::before,*::after{
    animation-duration:.001ms !important;animation-iteration-count:1 !important;
    transition-duration:.001ms !important;transition-delay:0ms !important}
  .mod,.posts .post,[data-rise]{opacity:1 !important;transform:none !important}
  .step{opacity:1 !important}
  .graph__edges path{stroke-dashoffset:0 !important}
  .mouse--fixed{display:none}
  .cue{display:none}
}

/* pause every loop on a hidden tab. animation-play-state does not inherit, so
   this has to reach elements and pseudo-elements directly. */
body.is-paused *,body.is-paused *::before,body.is-paused *::after{animation-play-state:paused !important}

/* ============================================================================
   THE PHONE HERO — an art-directed still, not a narrowed desktop

   Proportions come from the approved "03 — Mobile adaptation" concept, read off
   its own CSS and rescaled to the scene box:

     concept, as % of its 375x760 screen     here, as % of the scene
     monitor  x 24-96%   y 13.0-35.2%        x 24-96%   y 7.5-40.6%
     figure   x -20-72%  y 29.4-70.0%        y 32-92.5%, bleeding off the left

   The figure ends up about 1.8x the monitor's height and stands in front of it,
   which is the whole point: the monitor is a distant prop, the person is the
   subject, and the words sit underneath in clean flow.

   Sizes are percentages of .hero__scene, whose aspect-ratio is fixed. That also
   keeps its contribution to layout definite, so the max-content path that once
   reserved 1632px of dead scroll cannot come back through this block.

   The concept's figure is a cropped rear view and wider than tall; ours is a
   full front-facing pose and taller than wide, so the two cannot match on both
   axes. Height wins, because height is what carries the dominance.
   ========================================================================== */
/* Portrait only. The scene's aspect is a tall 11/15, which is the shape of the
   concept; in landscape it would stand 890px high inside a 375px viewport and
   push the figure and the words far below the fold. Landscape phones keep the
   stacked static hero, which suits a wide short window. */
@media (max-width:720px) and (orientation:portrait){

  .hero__stage{justify-content:flex-start;gap:clamp(14px,3.4vw,22px);
    padding-top:clamp(84px,22vw,104px);padding-bottom:clamp(28px,7vw,44px)}

  /* scene, then the words */
  .hero__scene{order:1}
  .bands{order:2}

  /* The aspect is the concept's shape. The cap stops a short phone spending so
     much of its screen on the scene that the buttons fall below the fold: both
     the monitor and the figure are sized in percentages of this box, so shorten
     it and they shorten together, keeping the 2:1 relationship between them. */
  .hero__scene{display:block;position:relative;width:100%;aspect-ratio:11/15;
    max-height:58svh;margin:0 0 clamp(2px,1vw,8px)}

  /* No vignette over the figure. The asset is a cut-out, so its lower edge is
     the chair's own and needs nothing fading it into the page; the gradient
     that used to sit here only dimmed a quarter of the photograph. */

  /* the monitor: small, high, set back, and no longer a content container */
  .lay--monitor{position:absolute;left:5%;top:7.5%;width:92%;height:auto;
    aspect-ratio:16/10;margin:0;transform:none;z-index:2}
  .lay--monitor .mon{--mon-pad:2px;--mon-radius:7px;--screen-radius:4px;height:100%}
  .lay--monitor .screen{height:100%;min-height:0;display:flex;flex-direction:column}
  .screen__body{min-height:0;flex:1;overflow:hidden}
  .screen__tabs{flex:none}
  .screen__tabs .tab{font-size:8.5px;padding:5px 7px}
  .screen__status{display:none}

  /* the editor is the only thing on the distant screen; the capabilities list and
     the identity card belong to the desktop stages and to the block below */
  .editor{display:flex;opacity:1 !important}
  .editor__gutter,.editor__code{font-size:8.5px;line-height:1.5}
  /* Deploy lines are long and this screen is far away, so they run past its right
     edge. Fading them out there reads as code continuing beyond the frame, which
     is true, rather than as text that has been chopped. */
  .editor__code{-webkit-mask-image:linear-gradient(90deg,#000 78%,transparent);
    mask-image:linear-gradient(90deg,#000 78%,transparent)}
  .term,.idcard{display:none}

  /* the subject, in front */
  .lay--avatar{z-index:3}

  /* the concept carries no wordmark on a phone: the header already says it */
  .wordmark{display:none}

  /* the words, in flow, with the concept's rhythm */
  .bands{margin:0}
  .band__k{font-size:11px;letter-spacing:.15em}
  .band__h{font-size:clamp(26px,7vw,36px);line-height:1.05}
  .band__s{font-size:clamp(14px,3.6vw,16px);margin-top:6px}

  /* The identity, in the corner of the screen.
     Inside .screen, which is already positioned and already clips, so 2px from
     the bottom and the right needs nothing else. It sits above the editor,
     whose lines are masked out toward that edge anyway, so the two never fight.
     The colours are the monitor's own: --text and --text-2 resolve here to the
     values every season declares for `.mon *`, which is what makes this read as
     the desktop card without restating one of them. */
  .hero__ident{position:absolute;right:2px;bottom:2px;z-index:3;display:block;
    width:auto;max-width:72%;text-align:right;pointer-events:none}
  .hero__ident-name{margin:0;font-family:var(--display);font-weight:700;
    font-size:clamp(11px,3.4vw,16px);line-height:1.15;letter-spacing:-.015em;color:var(--text)}
  .hero__ident-role{margin:1px 0 0;font-family:var(--mono);font-size:clamp(8px,2.4vw,11px);
    line-height:1.2;color:var(--text-2)}
}

/* ============================================================================
   DISPLAY SWITCHES

   Three separate components, each named by its own selector so no switch can
   reach a neighbour:

     .rail                      the blue execution line, one SVG for the page
     .mouse:not(.mouse--fixed)  the large mouse inside the hero scene
     .mouse--fixed              the small mouse that travels between sections

   The pair of queries below is the site's existing small-screen definition and
   its exact complement, so every viewport falls in exactly one of them. The
   classes come from the server, which is why nothing flashes before being hidden.

   The hero mouse is the exception: its rule sits outside both queries, because
   its switch is not viewport-split either. Only the animated hero contains that
   mouse, and the static-hero gates already remove it wherever they apply -- but
   they do not cover every viewport below 901px (a 880x700 landscape window runs
   the animated hero), so keying the rule to the desktop query would leave a band
   where the switch is off yet the mouse still shows. Unconditional matches
   heroMouseOn(), which is deliberately not split for the same reason.
   ========================================================================== */

.hariri-no-heromouse-desktop .mouse:not(.mouse--fixed){display:none}

/* The captions come off at the wrapper, not at `.band`. Four bands live in it
   and the hero writes an opacity onto it every frame, so removing the wrapper
   takes the whole block -- boxes, seasonal frame and text -- in one rule and
   leaves nothing for that inline opacity to argue with. It also matters on a
   phone, where the wrapper is a real item in the stacked hero: `display:none`
   gives its space back rather than leaving a gap where the captions were. */

@media (min-width:901px) and (min-height:621px){
  .hariri-no-rail-desktop .rail{display:none}
  .hariri-no-floatmouse-desktop .mouse--fixed{display:none}
  .hariri-no-bands-desktop .bands{display:none}
}

@media (max-width:900px),(max-height:620px){
  .hariri-no-rail-mobile .rail{display:none}
  .hariri-no-floatmouse-mobile .mouse--fixed{display:none}
  .hariri-no-bands-mobile .bands{display:none}
}


/* ============================ PANNEAU DE PROJET ============================
   `Voir les détails` used to grow the card, which moved every section below it
   and forced the execution line to remeasure. This is a fixed layer instead:
   the document height never changes and nothing reflows.

   On a wide screen it is a right-hand drawer with the project still visible
   beside it, so the detail arrives in context rather than replacing it. On a
   phone it takes the whole viewport, because the real content runs to two
   thousand characters and a sheet would be a peephole.

   The header repeats the browser frame's own vocabulary -- three dots and the
   site's host -- so the panel reads as the project's window opening, not as a
   modal borrowed from a framework. Depth is surface tone and one hairline; there
   is no shadow anywhere in here.
   ========================================================================= */

.ppanel{position:fixed;inset:0;z-index:120;display:flex;justify-content:flex-end}
.ppanel[hidden]{display:none}

.ppanel__veil{position:absolute;inset:0;background:var(--deep);opacity:0;
  transition:opacity .22s var(--ease)}
.ppanel.is-on .ppanel__veil{opacity:.42}   /* enough to signal inert, light enough to keep the project readable */

.ppanel__box{position:relative;display:flex;flex-direction:column;
  width:min(560px,100%);max-width:100%;height:100%;
  background:var(--canvas);border-left:1px solid var(--line);
  transform:translateX(100%);transition:transform .24s var(--ease);
  outline:none}
.ppanel.is-on .ppanel__box{transform:none}

/* the frame bar, borrowed from the project browser so the two read as one idea */
.ppanel__bar{flex:none;display:flex;align-items:center;gap:9px;
  padding:11px 12px 11px 16px;background:var(--deep);
  border-bottom:1px solid var(--line)}
.ppanel__bar .frame__url{margin-left:6px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

.ppanel__x{margin-left:auto;flex:none;width:34px;height:34px;display:grid;place-items:center;
  background:transparent;border:1px solid var(--line);border-radius:9px;
  color:var(--text-2);font-size:13px;line-height:1;cursor:pointer;
  transition:color .2s var(--ease),border-color .2s var(--ease),background .2s var(--ease)}
.ppanel__x:hover{color:var(--text);border-color:var(--line-strong,var(--line));background:var(--panel)}

/* the only scroller in here, so long content never touches the page */
.ppanel__scroll{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;
  -webkit-overflow-scrolling:touch;
  padding:clamp(22px,3vw,34px) clamp(20px,3vw,34px) clamp(40px,6vw,64px)}

.ppanel__kick{font-family:var(--mono);font-size:11px;letter-spacing:.16em;
  text-transform:uppercase;color:var(--ember);margin:0 0 10px}
.ppanel__t{font-weight:700;font-variation-settings:'wdth' 104;
  font-size:clamp(24px,2.4vw,32px);line-height:1.1;letter-spacing:-.02em;
  margin:0 0 22px;color:var(--text)}

/* the labelled metadata: two columns that stay aligned, one hairline per row */
.pdl{margin:0 0 26px;padding:0;border-top:1px solid var(--line)}
.pdl__row{display:grid;grid-template-columns:minmax(96px,34%) 1fr;gap:14px;
  padding:10px 0;border-bottom:1px solid var(--line-soft)}
/* --text-2, not --muted: at 10.5px these labels need 4.5:1, and --muted only
   reaches 4.06 in Winter. */
.pdl dt{font-family:var(--mono);font-size:10.5px;letter-spacing:.12em;
  text-transform:uppercase;color:var(--text-2);padding-top:2px}
.pdl dd{margin:0;color:var(--text);font-size:14px;line-height:1.5}

.ppanel .pbody{color:var(--text-2);font-size:15px;line-height:1.68}
.ppanel .pbody > :first-child{margin-top:0}
.ppanel .pbody h2,.ppanel .pbody h3,.ppanel .pbody h4{
  color:var(--text);font-weight:650;font-variation-settings:'wdth' 102;
  font-size:clamp(15px,1.3vw,17px);letter-spacing:-.008em;
  margin:26px 0 8px;padding-top:18px;border-top:1px solid var(--line-soft)}
.ppanel .pbody h2:first-child,.ppanel .pbody h3:first-child{border-top:0;padding-top:0;margin-top:0}
.ppanel .pbody p{margin:0 0 1em}
.ppanel .pbody ul,.ppanel .pbody ol{margin:0 0 1em;padding-left:1.15em}
.ppanel .pbody li{margin:0 0 .4em}
.ppanel .pbody strong{color:var(--text);font-weight:600}
.ppanel .pbody a{color:var(--accent-hi)}

.ppanel .pfoot{margin:28px 0 0;padding-top:22px;border-top:1px solid var(--line)}

/* ---- phone: the whole viewport, and the dynamic one, not the static one ---- */
@media (max-width:720px){
  .ppanel__box{width:100%;height:100dvh;border-left:0}
  .ppanel__bar{padding-top:max(11px,env(safe-area-inset-top));
    padding-left:max(16px,env(safe-area-inset-left));
    padding-right:max(12px,env(safe-area-inset-right))}
  .ppanel__scroll{padding-left:max(20px,env(safe-area-inset-left));
    padding-right:max(20px,env(safe-area-inset-right));
    padding-bottom:max(48px,calc(env(safe-area-inset-bottom) + 40px))}
  .ppanel__x{width:40px;height:40px}
  .pdl__row{grid-template-columns:1fr;gap:3px}
}

/* The lock keeps scrollTop where it is, so the scrollbar's width has to be given
   back or the content shifts sideways. The fixed header needs it too, since a
   fixed element does not inherit the root's padding. */
html.is-panel-locked{overflow:hidden;padding-right:var(--lockpad,0px)}
html.is-panel-locked .nav{padding-right:var(--lockpad,0px)}

@media (prefers-reduced-motion:reduce){
  .ppanel__box{transition:opacity .12s linear;transform:none;opacity:0}
  .ppanel.is-on .ppanel__box{opacity:1}
  .ppanel__veil{transition:none}
}
