/* THE SHIPMENT SCENE — the 3-D pallet and parcel pictures, and the frame around them.
 *
 * Lifted out of admin/index.html on 30 Oct 2026 so the CLIENT PORTAL could draw the same picture
 * for the route their order is actually taking. Linked by both pages rather than injected by
 * script: a stylesheet in <head> is applied before first paint, and a page whose JS has not run
 * yet should not flash an unstyled stack of cuboids.
 *
 * Every rule below is the admin's own, moved unchanged. Two variables are admin-only
 * (--ka-45, --accent-bg), so they carry the admin dark-theme value as a fallback and the portal
 * gets the same result without having to define them.
 *
 * The stage sizes here MUST keep matching PAL3D_STAGE / PAR3D_STAGE in assets/shipment-scene.js.
 */
    .ship-meta { font-size:12px; color:var(--text-muted); }
    /* A pallet drawn with CSS 3D transforms rather than a WebGL library. The admin has no
       bundler, and this needs a few dozen cuboids, not a scene graph. */
    .pal3d-wrap { padding:20px 16px 8px; display:flex; gap:22px; flex-wrap:wrap;
      align-items:flex-start; }
    /* Square, and the size must equal PAL3D_STAGE in the script. A longer perspective distance than
       the original 1400px: at 2400 the nearest face is magnified about 4% rather than 12%, so less of
       the frame is spent on headroom and the pallet reads as a pallet rather than a wide-angle photo
       of one. */
    /* THE SCENE IS CONFINED TO ITS OWN BOX. (1 Nov 2026 — asked for directly)
     *
     * `.pal3d-world` is transform-style:preserve-3d, so its faces are projected by the perspective
     * above and paint wherever that projection puts them — which, once the pallet is turned, is
     * well outside this 400px square. They are painted ABOVE the controls that follow, and they
     * still take the pointer, so in the admin the tracking field underneath could not be clicked
     * at all. Reported as "grab & move overlaps the content below".
     *
     * `overflow:hidden` makes this element the clip boundary for everything the projection draws.
     *
     * WHAT IT DOES NOT DO IS CROP THE PALLET. Asked on 1 Nov to make the load "stop when the end
     * of the stage is hit" rather than run past the wall, which assumes it reaches the wall. It
     * cannot. The scale in the script fits the load's BOUNDING SPHERE, and a sphere is the same
     * size from every direction, so no rotation can push a corner further out than the sphere
     * already allows. Projecting all eight corners through this perspective at 16,000 pitch/yaw
     * pairs — including the off-centre short pallet in a multi-pallet set, which shares the
     * tallest one's pivot — the closest any corner comes to this edge is 22px. So there is no
     * angle to refuse, and a drag clamp here would be code that can never run.
     *
     * The clip stays as the backstop, because what it was added for is still true: these faces
     * are painted by a preserve-3d projection, they land above the controls below the card, and
     * they take the pointer there. That was the bug ("grab & move overlaps the content below",
     * tracking unclickable in the admin). The margin above is measured, not enforced; if a future
     * load geometry or perspective change eats it, this line is what keeps the tracking field
     * clickable. */
    /* 460, not 400. (1 Nov 2026 — "it can go more to the right as it is cut too short".)
     * Raised together with PAL3D_STAGE and the 0.253 scale ceiling in the script, which was 0.22:
     * 460/400 x 0.22 = 0.253, so every load is drawn 15% larger at the SAME mm-to-px scale as
     * every other. That ratio is the point — the ceiling is what makes a ten-box pallet look
     * smaller than a thirty-box one, and raising the stage without it would only have added
     * margin. See tools/test-nov1-batch.js, which pins the three numbers to each other. */
    .pal3d-stage { perspective:2400px; width:460px; height:460px; flex:0 0 auto;
      overflow:hidden;
      cursor:grab; touch-action:none; outline:none; border-radius:10px; }
    /* touch-action:none so dragging turns the pallet instead of scrolling the page on a tablet. */
    .pal3d-stage.dragging { cursor:grabbing; }
    .pal3d-stage:focus-visible { box-shadow:0 0 0 2px var(--accent); }
    /* `transform-origin:0 0` IS LOAD-BEARING. (2 Nov 2026 — "this is what I see when I want to
     * look straight from above": the pallet sat at the bottom of the frame, half cut off.)
     *
     * pal3dWorldTransform starts with translate(c,c) to put this element's LOCAL ORIGIN at the
     * middle of the stage, and everything after it — the rotations, the translate3d that centres
     * the load — is written in that frame. CSS does not apply a transform about the local origin
     * though: it applies it about transform-origin, as P' = O + M(P - O), and the default O is
     * 50% 50%, which here is (c, c) itself. So the browser was feeding a -c offset through the
     * rotation and then adding the translate(c,c) on top of it — the pivot counted twice. At rest
     * (pitch 16) it looks very nearly right, which is why it survived this long; the error grows
     * with the tilt, and at 88 degrees the load is pushed almost entirely below the frame.
     *
     * The faces inside already declare `transform-origin:0 0` for the same reason. This element
     * was the one that did not, and it is the one every other transform hangs off.
     *
     * Measured, after: at 16/45/70/88/-88 degrees the drawn load sits inside the stage with
     * 40px+ to spare. Measured, before: outside it at every pitch past about 30 degrees. */
    .pal3d-world { width:100%; height:100%; transform-style:preserve-3d;
      transform-origin:0 0;
      transition:transform .35s ease; }
    /* The easing is right for a redraw and wrong for a drag — 350 ms of lag behind the pointer. */
    .pal3d-world.dragging { transition:none; }
    .pal3d-box, .pal3d-deck { position:absolute; transform-style:preserve-3d; }
    .pal3d-face { position:absolute; box-sizing:border-box; border:1px solid var(--ka-45, rgba(0,0,0,.45)); }
    /* All six faces exist, but only the ones on the OUTSIDE of the load are drawn — see the note in
       renderPal3d. Three used to be enough because the viewpoint was fixed; once the pallet turns
       freely, a load with no back or bottom is see-through. Shaded as if lit from above and in front,
       so the solid reads as a solid at any angle. */
    .pal3d-box .f-top    { background:var(--accent); filter:brightness(1.18); }
    .pal3d-box .f-front  { background:var(--accent); filter:brightness(.82); }
    .pal3d-box .f-back   { background:var(--accent); filter:brightness(.58); }
    .pal3d-box .f-right  { background:var(--accent); filter:brightness(.68); }
    .pal3d-box .f-left   { background:var(--accent); filter:brightness(.74); }
    .pal3d-box .f-bottom { background:var(--accent); filter:brightness(.42); }
    .pal3d-deck .f-top    { background:#6b5335; }
    .pal3d-deck .f-front  { background:#4e3d27; }
    .pal3d-deck .f-back   { background:#33281a; }
    .pal3d-deck .f-right  { background:#3d2f1e; }
    .pal3d-deck .f-left   { background:#44351f; }
    .pal3d-deck .f-bottom { background:#291f13; }
    /* The parcel card reuses every class above — same cuboid, same drag, same legend — and adds
       only what is different about it: a smaller stage (five parcels fit a row where one pallet
       fills the card), and inner boxes rather than outer ones as the solid being drawn. */
    .par3d-stage { width:200px; height:200px; }
    .par3d-inner .f-top    { background:var(--accent); filter:brightness(1.10); }
    .par3d-inner .f-front  { background:var(--accent); filter:brightness(.80); }
    .par3d-inner .f-back   { background:var(--accent); filter:brightness(.56); }
    .par3d-inner .f-right  { background:var(--accent); filter:brightness(.66); }
    .par3d-inner .f-left   { background:var(--accent); filter:brightness(.72); }
    .par3d-inner .f-bottom { background:var(--accent); filter:brightness(.40); }
    /* The half of a last outer box that has no inner box in it. Drawn as an empty outline because
       it is not nothing: that outer box's tare is charged in full, and seeing the gap is the point.
       `transparent` rather than a fill, so the solid half behind it stays visible through it. */
    .par3d-empty .pal3d-face { background:transparent; border-style:dashed;
      border-color:var(--text-muted); }
    /* The caption has to be bounded by ITS OWN stage, not by the pallet card's 400px. Inheriting
       that, five 200px parcels ran their captions into one another and read as a single sentence:
       "Parcel 1 · 160 copies · 11.14 kg · 2 inner boxes Parcel 2 · 90 copies".
       ID-scoped deliberately. `.par3d-cap` alone loses to `.pal3d-cap { max-width:400px }` further
       down the sheet — same specificity, later wins — so the rule was written, looked right, and
       did nothing. A browser guard measuring caption width against stage width found it; reading
       the CSS did not. */
    /* Scoped to the SCENE, not to an id. It was `#ship-calc-parcels`, which stopped existing the
       moment the calculator had to be drawable more than once per page — and a rule that stops
       matching fails silently. A browser guard measuring caption width against stage width caught
       it; reading the CSS would not have. Still more specific than the `.pal3d-cap
       { max-width:400px }` further down the sheet, which is why it is not a bare class. */
    .v3d-scene[data-kind="parcels"] .pal3d-cap { max-width:200px; margin-top:2px; line-height:1.45; }
    .par3d-cap b { color:var(--text); }
    .par3d-cap.over b { color:var(--danger); }
    /* The parcels are a row of small stages, so they need room between them rather than the 4px
       the pallet decks sit at — those are one load split up, these are separate boxes. */
    .v3d-scene[data-kind="parcels"] .pal3d-decks { gap:14px; }
    .calc-scene-t { font-size:11px; text-transform:uppercase; letter-spacing:.04em;
      color:var(--text-muted); margin:14px 0 -6px 4px; }
    .calc-verdict { padding:10px 12px; border-radius:8px; font-size:13px; line-height:1.5; }
    .calc-verdict.ok { background:var(--wa-03); }
    .calc-verdict.no { background:var(--accent-bg, rgba(244,162,97,.12)); color:var(--accent-ink); }
    /* A shipment is as many pallets as it needs. They wrap, and each carries its own box count. */
    /* The strip of stages must be ALLOWED TO SHRINK, or `flex-wrap:wrap` below never fires: a
       `flex:0 0 auto` child of `.pal3d-wrap` keeps its full content width whatever the column is,
       so four parcels stay on one 860px line and the fourth is simply off the right edge. Since
       the order view put the section menu in a 212px column the content is narrower than the
       Shipping Manager's, and that is exactly where it showed. With `1 1 320px; min-width:0` the
       strip takes what is left beside the legend and wraps the stages into as many rows as it
       needs. The stages themselves stay `flex:0 0 auto` at their fixed px — their size must keep
       equalling PAL3D_STAGE / PAR3D_STAGE in the script.
       The 480px basis is one pallet stage plus its gap: below that the legend drops to its own
       line and the strip gets the full width, rather than the two of them splitting a column too
       narrow for either. It tracks the stage — it was 420 while the stage was 400. The legend now
       grows too; what stops it squeezing the strip is its max-width, not a grow of 0. */
    /* `justify-content:center` and a basis of exactly one stage. Growing the strip was not enough
     * on its own: the stages pack from its left edge, so every pixel the strip grew by appeared as
     * blank space on its RIGHT — between the pallet and the legend, which is the gap that was
     * complained about. Centred, the strip's share of the spare splits either side of the pallet
     * instead of piling up on the side facing the legend. */
    .pal3d-decks { display:flex; flex-wrap:wrap; justify-content:center; gap:4px;
      flex:1 1 460px; min-width:0; }
    .pal3d-pallet { display:flex; flex-direction:column; align-items:center; }
    .pal3d-cap { font-size:11px; color:var(--text-muted); text-align:center; max-width:460px;
      margin-top:-8px; }
    /* THE DEAD GAP BETWEEN THE STAGE AND THE LEGEND. (1 Nov 2026 — asked for directly: "the space
     * between these two should be used equally, so both use more space".)
     *
     * The legend was `0 1 240px`, which cannot grow — so every pixel the row had spare collected
     * between the two of them, the stage stayed 400 and the legend wrapped "deck coverage" onto
     * three lines next to all that air. Now BOTH carry flex-grow 1, so the spare is split evenly.
     *
     * `max-width` is what the old `0 1` was really protecting: on the Shipping Manager, which is
     * much wider than the order view, an uncapped legend would take half of a very large remainder
     * and leave a column of empty text. Capped, the overflow past that point goes to the stages,
     * which is where more pallets fit. `min-width:0` on the strip stays — it is what lets the
     * stages wrap instead of overflowing when the column is narrower than one stage. */
    .pal3d-legend { flex:1 1 320px; min-width:260px; max-width:460px; font-size:13px; }
    .pal3d-legend dl { display:grid; grid-template-columns:auto 1fr; gap:4px 12px; margin:0; }
    .pal3d-legend dt { color:var(--text-muted); }
    .pal3d-legend dd { margin:0; font-weight:600; }
    .pal3d-note { margin-top:10px; font-size:12px; color:var(--accent-ink); }
    .calc-scene { margin-top: 14px; }
