/* ==========================================================================
   Laptop 3D — a laptop built in CSS 3D whose screen displays real HTML.

   Reproduced by measurement from a WebGL scene: the perspective, the tilt of
   the camera and the angle of the lid are those of the original model, read
   from its matrices then verified by overlay (largest error 0.75 px).

   Frame of reference: origin at the middle of the hinge, X to the right, Y
   downwards, Z towards the viewer. 1 em = 1 world unit, which makes the
   enlargement uniform: the perspective is in em too, so changing the scale does
   not distort the scene.

   No dependency, no external request, no inline style.
   ========================================================================== */

.l3d,
.l3d *,
.l3d *::before,
.l3d *::after {
  /* The component declares its own box-sizing and assumes no reset. */
  box-sizing: border-box;
}

.l3d {
  /* ── Scale ──────────────────────────────────────────────────────────────
     A single variable drives everything. `--l3d-largeur` is the width the
     laptop takes up once projected; the rest follows from it.
     It must be an absolute length or a function that yields one
     (min(), clamp()…): a bare percentage cannot be divided. */
  --l3d-largeur: 44rem;
  --l3d-echelle: calc(var(--l3d-largeur) / 10.7675);

  /* ── Camera, read from the original scene ───────────────────────────────
     perspective 1340.72 px for a laptop projected at 1107 px, camera at
     (0, 4, 17) tilted 7.122° downwards. */
  --l3d-perspective: 13.4072em;
  --l3d-camera-inclinaison: -7.122deg;
  --l3d-camera-y: 4em;
  --l3d-camera-z: -16.721em;
  /* The vanishing point is not at the centre of the frame: the top of the lid
     is 311.3 px above it, the bottom of the reflection 416.5 px below. */
  --l3d-horizon: calc(50% - 0.526em);

  /* ── Lid opening ────────────────────────────────────────────────────────
     11.459° = 0.2 rad exactly, the value read from the object matrix. */
  --l3d-ouverture: 11.459deg;
  --l3d-ouverture-fermee: -87deg;

  /* ── Chassis dimensions, in world units ─────────────────────────────────
     All measured then projected back onto their plane; see
     _mesures/cotes.json. */
  --l3d-socle-largeur: 8.884em;
  --l3d-socle-profondeur: 6.073em;
  --l3d-socle-epaisseur: 0.12em;
  /* Radius of the front corners, measured in the PLANE of the deck then
     reprojected: read off the image it would look three times smaller, the arc
     being seen there at a grazing angle. */
  --l3d-socle-rayon: 0.254em;
  /* The front edge is narrower than the base, and this is not an eyeballed
     adjustment. The deck pulls in at its rounded corners, so its leftmost point
     projects to 385.5 px on the original render; an edge carried all the way to
     the side of the base would project to 363.2 and overhang by 11.2 CSS px on
     each side. 8.7 is the width that makes the two coincide. */
  --l3d-tranche-largeur: 8.7em;
  /* The front edge rises slightly above the deck. It is only visible there at
     the corners: elsewhere the deck covers it. Without that overlap the deck
     draws back into its rounded corners while the edge stays at the front, and a
     light gap opens between the two — the laptop looks as if it were floating
     above its own thickness. */
  --l3d-tranche-recouvrement: 0.05em;
  --l3d-capot-largeur: 8.823em;
  --l3d-capot-hauteur: 5.986em;
  --l3d-capot-rayon: 0.069em;
  --l3d-dalle-largeur: 8.64em;
  --l3d-dalle-hauteur: 5.31em;
  /* Reference width of the page displayed, in em. 90 em ≈ 1440 px. */
  --l3d-ecran-colonnes: 90;
  --l3d-bezel-lateral: 0.094em;
  --l3d-bezel-haut: 0.388em;
  --l3d-bezel-bas: 0.289em;
  --l3d-clavier-largeur: 7.026em;
  --l3d-clavier-profondeur: 2.639em;
  --l3d-clavier-z: 0.601em;
  /* Gap between keys. Measured on the top row of the original render: a pitch
     of 80 image px for a 73 px key, i.e. 8.75% of the pitch — twice what you
     would put in by instinct, and it is what tells a keyboard apart from a grid
     of bricks. */
  --l3d-clavier-jeu: 0.044em;
  --l3d-clavier-marge: 0.04em;
  --l3d-trackpad-largeur: 4.063em;
  --l3d-trackpad-profondeur: 2.565em;
  --l3d-trackpad-z: 3.434em;
  --l3d-encoche-largeur: 1.2em;

  /* ── Materials ──────────────────────────────────────────────────────────
     The deck gradient is a survey, not an invention: luminance sampled every 2%
     of the width on the original render. */
  --l3d-fond: #f0f0f0;
  --l3d-capot-couleur: #000;
  --l3d-capot-arete: #080808;
  --l3d-tranche: #080808;
  --l3d-dalle-fond: #000;
  --l3d-plateau: linear-gradient(
    to right,
    #474747 0%, #4f4f4f 10%, #5a5a5a 20%, #6e6e6e 30%, #8d8d8d 40%,
    #acacac 50%, #bfbfbf 60%, #c2c2c2 65%, #c0c0c0 70%, #adadad 80%,
    #929292 90%, #787878 100%
  );
  /* The deck darkens towards the hinge: at the centre it goes from 172 on the
     palm rest to 149 under the lid. */
  --l3d-plateau-fond: rgba(0, 0, 0, .14);
  --l3d-touche: #191919;
  --l3d-touche-creux: #050505;
  --l3d-touche-reflet: rgba(255, 255, 255, .1);
  --l3d-trait: rgba(0, 0, 0, .35);
  --l3d-ombre: rgba(0, 0, 0, .0875);
  --l3d-ombre-portee: 87.5;

  /* ── Motion ─────────────────────────────────────────────────────────────
     Delays included: they are tunable without touching the JavaScript. */
  --l3d-ouverture-duree: 1400ms;
  --l3d-ouverture-delai: 120ms;
  --l3d-ouverture-courbe: cubic-bezier(.16, .84, .28, 1);
  --l3d-suivi-duree: 600ms;
  /* Maximum travel of the tracking, in degrees: the pointer runs from −0.5 to
     +0.5 either side of the centre, so the scene tilts by ± half of it. */
  --l3d-suivi-amplitude: 5;

  /* Tilts added by the pointer tracking; the script writes these two variables
     only, never a complete transform. */
  --l3d-pointeur-x: 0deg;
  --l3d-pointeur-y: 0deg;

  /* ── Rotating it by hand ────────────────────────────────────────────────
     You grab the laptop and turn it. Here too the script writes two angles and
     nothing else; the travel, the sensitivity and the return are set here. */
  --l3d-rotation-x: 0deg;
  --l3d-rotation-y: 0deg;
  /* Maximum travel, in degrees. Beyond roughly 35° in Y the planes of the
     chassis cross and the depth sorting becomes visible. */
  --l3d-rotation-max-x: 16;
  --l3d-rotation-max-y: 32;
  /* Degrees travelled for one full sweep across the component with the mouse. */
  --l3d-rotation-sensibilite: 1.35;
  /* Duration of the return to the starting position on release. 0 to keep the
     pose. */
  --l3d-rotation-retour: 900ms;

  /* Scroll progress, from 0 to 1, written by the script in `data-l3d-scroll`
     mode. Drives both the opening of the lid and the levelling of the camera. */
  --l3d-progression: 0;
  /* Scroll window: the lid starts to open when the component enters from the
     bottom and finishes when it reaches this fraction of the height. */
  --l3d-scroll-debut: 1;
  --l3d-scroll-fin: .35;
  /* Levelling of the camera over the travel: the viewpoint drops a little,
     which makes the laptop look as if it were rising towards the reader. */
  --l3d-scroll-inclinaison: 3.4deg;

  position: relative;
  width: var(--l3d-largeur);
  /* Projected width 10.7675 em, height 7.278 em from the top of the lid to the
     bottom of the reflection. The height is rounded up — 7.28 — because the
     engine truncates to 1/64 of a pixel and would otherwise invent a pixel of
     overflow. */
  aspect-ratio: 10.7675 / 7.28;
  font-size: var(--l3d-echelle);
  perspective: var(--l3d-perspective);
  perspective-origin: 50% var(--l3d-horizon);
}

/* The world: a point placed at the vanishing point, to which the inverse of the
   camera pose is applied. The order is that of CSS3DRenderer — focal distance,
   then rotation, then translation. */
.l3d__monde {
  position: absolute;
  left: 50%;
  top: var(--l3d-horizon);
  width: 0;
  height: 0;
  transform-style: preserve-3d;
  transform:
    translateZ(var(--l3d-perspective))
    rotateX(var(--l3d-camera-inclinaison))
    rotateX(var(--l3d-pointeur-x))
    rotateX(var(--l3d-rotation-x))
    rotateY(var(--l3d-pointeur-y))
    rotateY(var(--l3d-rotation-y))
    translate3d(0, var(--l3d-camera-y), var(--l3d-camera-z));
  transition: transform var(--l3d-suivi-duree) ease-out;
}

/* ── Rotating it by hand ──────────────────────────────────────────────────
   `data-l3d-rotation-actif` is set by the script: without it nothing changes,
   and a page without JavaScript is not left with a grab cursor that grabs
   nothing. */

.l3d[data-l3d-rotation-actif] {
  cursor: grab;
  /* Vertical scrolling with a finger is preserved: only the horizontal gesture
     is captured. `none` would lock the whole page on mobile. */
  touch-action: pan-y;
}

.l3d[data-l3d-rotation-actif]:active,
.l3d[data-l3d-saisi] {
  cursor: grabbing;
}

/* While being grabbed, the scene follows the finger with no damping — a
   transition would make the laptop lag behind the cursor. On release it comes
   back over --l3d-rotation-retour. */
.l3d[data-l3d-saisi] .l3d__monde {
  transition: none;
}

.l3d[data-l3d-rotation-actif]:not([data-l3d-saisi]) .l3d__monde {
  transition: transform var(--l3d-rotation-retour) cubic-bezier(.22, .68, .3, 1);
}

/* The content of the panel stays selectable and clickable: the grab never
   starts on the screen. */
.l3d[data-l3d-rotation-actif] .l3d__ecran {
  cursor: auto;
}

.l3d[data-l3d-rotation-actif]:focus-visible {
  outline: .03em solid currentColor;
  outline-offset: .06em;
}

/* Each part is a plane, nothing more: its details — panel, keyboard, trackpad,
   notch — are painted IN its plane, not in depth.

   So they stay `flat`, and that matters. Giving them `preserve-3d` would lift
   every child out of its part's plane and make it a separately sorted 3D
   object; the engine then classes the panel and the lid independently, and the
   bottom of the lid goes behind the deck — the bezel disappears across the
   lower third. Only `.l3d__monde` needs to preserve 3D. */
.l3d__piece {
  position: absolute;
  left: 0;
  top: 0;
  transform-style: flat;
}

/* ── The base ─────────────────────────────────────────────────────────────
   The deck is laid flat: rotateX(90deg) sends the bottom of the box towards the
   viewer, so the height of the div becomes the depth of the deck. */

.l3d__plateau {
  width: var(--l3d-socle-largeur);
  height: var(--l3d-socle-profondeur);
  margin-left: calc(var(--l3d-socle-largeur) / -2);
  transform-origin: 50% 0;
  transform: rotateX(90deg);
  background: var(--l3d-plateau);
  border-radius: 0 0 var(--l3d-socle-rayon) var(--l3d-socle-rayon);
}

/* Darkening towards the hinge, measured at the centre of the deck. */
.l3d__plateau::before {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(to bottom, var(--l3d-plateau-fond), transparent 42%);
  border-radius: inherit;
  pointer-events: none;
}

/* Front face. It is the only edge visible: the camera is head-on, so the sides
   of the base are seen edge-on with zero thickness.

   Its rounding is elliptical and picks up the radius of the deck horizontally:
   without that it stays rectangular while the deck pulls in at its corners, and
   it sticks out on either side — the most visible flaw of the front edge. The
   vertical radius is half the height, which finishes the edge in a rounded
   point, like the curved surface of the base becoming grazing then
   disappearing. */
.l3d__tranche {
  width: var(--l3d-tranche-largeur);
  height: calc(var(--l3d-socle-epaisseur) + var(--l3d-tranche-recouvrement));
  margin-left: calc(var(--l3d-tranche-largeur) / -2);
  transform: translate3d(0, calc(var(--l3d-tranche-recouvrement) * -1), var(--l3d-socle-profondeur));
  background: var(--l3d-tranche);
  border-radius: var(--l3d-socle-rayon) / calc(var(--l3d-socle-epaisseur) / 2);
}

/* Opening notch. It is not a grey recess but a lit lip: on the original render,
   a band of 2.5 CSS px reaching 248 out of 255, at 16% down from the top of the
   front edge. */
.l3d__encoche {
  position: absolute;
  left: 50%;
  /* Located from the front edge of the deck, not from the top of the front
     face: the latter rises above the deck and a percentage would follow that
     offset. */
  top: calc(var(--l3d-tranche-recouvrement) + var(--l3d-socle-epaisseur) * .16);
  width: var(--l3d-encoche-largeur);
  height: calc(var(--l3d-socle-epaisseur) * .18);
  margin-left: calc(var(--l3d-encoche-largeur) / -2);
  background: linear-gradient(to bottom, #c9c9c9, #f8f8f8 55%, #bdbdbd);
  border-radius: .012em;
}

/* ── The keyboard ─────────────────────────────────────────────────────────
   Laid flat on the deck, so a plain 2D box in the deck's plane. The keys are in
   the HTML rather than generated: the component has to hold up without
   JavaScript. */

.l3d__clavier {
  position: absolute;
  left: 50%;
  top: calc(var(--l3d-clavier-z) - var(--l3d-clavier-marge));
  display: flex;
  flex-direction: column;
  gap: var(--l3d-clavier-jeu);
  width: calc(var(--l3d-clavier-largeur) + var(--l3d-clavier-marge) * 2);
  height: calc(var(--l3d-clavier-profondeur) + var(--l3d-clavier-marge) * 2);
  margin-left: calc((var(--l3d-clavier-largeur) + var(--l3d-clavier-marge) * 2) / -2);
  padding: var(--l3d-clavier-marge);
  background: var(--l3d-touche-creux);
  border-radius: .05em;
}

.l3d__rangee {
  display: flex;
  flex: 1 1 0;
  gap: var(--l3d-clavier-jeu);
  min-height: 0;
}

/* The function row is far shorter than the others: on the original render it is
   less than half the height of a letter row. */
.l3d__rangee:first-child {
  flex: .46 1 0;
}

/* The relief of a key comes down to four things: a light edge at the top, a
   right-hand flank that catches the light, a body that darkens towards the
   bottom, and a short drop shadow that lifts it out of the well. Without them
   the grid reads as a flat chequerboard — that is what set the first attempt
   apart from the original render more than any dimension did. */
.l3d__touche {
  position: relative;
  flex: 1 1 0;
  min-width: 0;
  background: linear-gradient(to bottom, #242424, #141414 58%, #1b1b1b);
  border-radius: .03em;
  box-shadow:
    inset 0 .014em 0 var(--l3d-touche-reflet),
    inset -.008em 0 0 rgba(255, 255, 255, .07),
    inset 0 -.012em .014em rgba(0, 0, 0, .6),
    0 .014em .016em rgba(0, 0, 0, .55);
}

.l3d__touche--large { flex: 1.6 1 0; }
.l3d__touche--maj { flex: 2.4 1 0; }
.l3d__touche--espace { flex: 5.6 1 0; }

/* ── The trackpad ─────────────────────────────────────────────────────────
   It has no colour of its own in the original render: only a line one or two
   levels of grey apart marks it out. */

.l3d__trackpad {
  position: absolute;
  left: 50%;
  top: var(--l3d-trackpad-z);
  width: var(--l3d-trackpad-largeur);
  height: var(--l3d-trackpad-profondeur);
  margin-left: calc(var(--l3d-trackpad-largeur) / -2);
  border: .012em solid var(--l3d-trait);
  border-bottom: 0;
  border-radius: .05em .05em 0 0;
}

/* ── The lid ──────────────────────────────────────────────────────────────
   Hinged on its bottom edge, which IS the hinge. */

.l3d__capot {
  width: var(--l3d-capot-largeur);
  height: var(--l3d-capot-hauteur);
  margin-left: calc(var(--l3d-capot-largeur) / -2);
  margin-top: calc(var(--l3d-capot-hauteur) * -1);
  transform-origin: 50% 100%;
  transform: rotateX(var(--l3d-ouverture));
  background: var(--l3d-capot-couleur);
  border-radius: var(--l3d-capot-rayon) var(--l3d-capot-rayon) 0 0;
  box-shadow: 0 0 0 .01em var(--l3d-capot-arete);
}

.l3d__dalle {
  position: absolute;
  left: var(--l3d-bezel-lateral);
  top: var(--l3d-bezel-haut);
  width: var(--l3d-dalle-largeur);
  height: var(--l3d-dalle-hauteur);
  /* overflow clips the content, therefore flattens: this is a leaf of the 3D
     tree, no child may be positioned in depth after it. */
  overflow: hidden;
  background: var(--l3d-dalle-fond);
  border-radius: .02em;
}

/* What the component exposes: the block where you put your page.
 *
 * Its `font-size` is computed so that the panel is exactly
 * `--l3d-ecran-colonnes` em wide — 90 em, i.e. the 1440 px of a desktop mockup
 * if you count 16 px to the em. In other words: you write your content in em
 * just as you would write it in px on a 1440 px page, and it follows the scale
 * of the mockup instead of fighting it.
 *
 * Content in px would keep its absolute size while the laptop changes size: at
 * 470 px wide, a library thumbnail was showing a 54 px heading spilling out of
 * the panel. */
.l3d__ecran {
  width: 100%;
  height: 100%;
  overflow: hidden;
  font-size: calc(var(--l3d-dalle-largeur) / var(--l3d-ecran-colonnes));
  line-height: 1.5;
}

/* ── The reflection on the floor ──────────────────────────────────────────
   A horizontal plane set at the height of the bottom of the front edge.
   Projected back onto that plane it is not a diffuse halo but a crisp rectangle
   of 7.52 × 1.64 units flush with the front edge: 21 to 24 levels below the
   background, near constant.

   Two traps avoided here. It carries no `filter`: a filter flattens the element
   and takes it out of the depth sorting, which made it show through the deck in
   patches. And it does not run under the whole base, it starts just behind the
   front edge: no point making two parallel planes coexist along their whole
   length. */

.l3d__sol {
  width: 7.52em;
  /* The reflection is flush at z = 6.074 — the front edge of the base — and
     fades out at 7.608. It is started slightly earlier, under the deck, so that
     its edge does not coincide with that of the base. */
  height: 2.11em;
  margin-left: -3.76em;
  transform-origin: 50% 0;
  transform: translate3d(0, .13em, 5.5em) rotateX(90deg);
  /* The reflection is a translucent black, never a tint: that way it sits on
     any background. Feathering its edges with `--l3d-fond` would leave a grey
     band the moment the background changes. */
  background: linear-gradient(
    to bottom,
    var(--l3d-ombre) 0%,
    var(--l3d-ombre) 62%,
    color-mix(in srgb, var(--l3d-ombre) 78%, transparent) 100%
  );
}

/* ── Animated opening ─────────────────────────────────────────────────────
   The lid starts closed and unfolds when the component enters the viewport. The
   class is set by the script; with no script nothing is ever closed, so the
   laptop is displayed open — the useful state by default. */

.l3d[data-l3d-anime] .l3d__capot {
  transform: rotateX(var(--l3d-ouverture-fermee));
  transition: transform var(--l3d-ouverture-duree) var(--l3d-ouverture-courbe) var(--l3d-ouverture-delai);
}

.l3d[data-l3d-anime].is-in .l3d__capot {
  transform: rotateX(var(--l3d-ouverture));
}

/* The panel only lights up once the lid is raised. */
.l3d[data-l3d-anime] .l3d__ecran {
  opacity: 0;
  transition: opacity calc(var(--l3d-ouverture-duree) * .4) ease-out
    calc(var(--l3d-ouverture-delai) + var(--l3d-ouverture-duree) * .55);
}

.l3d[data-l3d-anime].is-in .l3d__ecran {
  opacity: 1;
}

/* Transitions are switched off while the closed state is being set: without
   that you would watch the lid close before it opens. The script removes this
   attribute two frames later. */
.l3d[data-l3d-pose] .l3d__capot,
.l3d[data-l3d-pose] .l3d__ecran,
.l3d[data-l3d-pose] .l3d__monde {
  transition: none;
}

/* ── Scroll-driven opening ────────────────────────────────────────────────
   The lid follows the scroll instead of springing open when it enters the
   viewport. The angle is interpolated in CSS between the closed state and the
   measured angle: the script writes a single number, `--l3d-progression`, which
   leaves the travel, the window and the levelling tunable without touching the
   JavaScript.

   No `animation-timeline` here: it is not available everywhere yet, and a
   library component cannot depend on a single engine. */

.l3d[data-l3d-scroll-actif] .l3d__capot {
  transform: rotateX(
    calc(
      var(--l3d-ouverture-fermee) +
      (var(--l3d-ouverture) - var(--l3d-ouverture-fermee)) * var(--l3d-progression)
    )
  );
  /* A short transition smooths the scroll steps without lagging behind. */
  transition: transform 90ms linear;
}

/* This rule redefines the whole transform: any rotation added to
   `.l3d__monde` therefore has to be repeated here, otherwise it is silently
   overwritten as soon as scroll mode is active. The variables carry on being
   written, the component stops moving, and nothing says so. */
.l3d[data-l3d-scroll-actif] .l3d__monde {
  transform:
    translateZ(var(--l3d-perspective))
    rotateX(calc(var(--l3d-camera-inclinaison) + var(--l3d-scroll-inclinaison) * (1 - var(--l3d-progression))))
    rotateX(var(--l3d-pointeur-x))
    rotateX(var(--l3d-rotation-x))
    rotateY(var(--l3d-pointeur-y))
    rotateY(var(--l3d-rotation-y))
    translate3d(0, var(--l3d-camera-y), var(--l3d-camera-z));
  transition: transform 90ms linear;
}

/* While being grabbed, the rotation takes priority over the scroll smoothing. */
.l3d[data-l3d-scroll-actif][data-l3d-saisi] .l3d__monde {
  transition: none;
}

/* The screen lights up over the end of the travel, once the lid is open enough
   to see it. */
.l3d[data-l3d-scroll-actif] .l3d__ecran {
  opacity: clamp(0, calc((var(--l3d-progression) - .45) * 3), 1);
  transition: opacity 90ms linear;
}

/* ── Preferences and rendering modes ──────────────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  /* A delay is movement spread over time: it goes to zero too, otherwise the
     lid would stay closed for the length of the wait. */
  .l3d {
    --l3d-ouverture-duree: 1ms;
    --l3d-ouverture-delai: 0ms;
    --l3d-suivi-duree: 1ms;
    --l3d-suivi-amplitude: 0;
    /* Rotating stays possible — it is the user's own gesture, not an animation
       imposed on them. Only its springy return is removed. */
    --l3d-rotation-retour: 1ms;
  }

  .l3d[data-l3d-anime] .l3d__capot,
  .l3d[data-l3d-scroll-actif] .l3d__capot {
    transform: rotateX(var(--l3d-ouverture));
    transition: none;
  }

  .l3d[data-l3d-anime] .l3d__ecran,
  .l3d[data-l3d-scroll-actif] .l3d__ecran {
    opacity: 1;
    transition: none;
  }

  /* The levelling of the camera is movement too: it is neutralised rather than
     frozen halfway. */
  .l3d[data-l3d-scroll-actif] {
    --l3d-scroll-inclinaison: 0deg;
  }
}

@media (forced-colors: active) {
  /* Backgrounds are replaced by the system: with no edges the laptop would
     become a solid silhouette. The parts are made legible as outlines. */
  .l3d__capot,
  .l3d__plateau,
  .l3d__tranche,
  .l3d__dalle,
  .l3d__clavier,
  .l3d__trackpad {
    border: 1px solid CanvasText;
  }

  .l3d__touche {
    border: 1px solid CanvasText;
    box-shadow: none;
  }

  .l3d__sol {
    display: none;
  }
}
