/* ============================================================================
   THE CABDESIGN PANEL CONTRACT — one slide-in language for every product
   ----------------------------------------------------------------------------
   Source of truth: docs/research/p487-menu-system-proposal-2026-08-26.md §6.
   Adopted by the founder as FD287 Option A, 2026-08-27, verbatim:

     "Go ahead and implement option A, and we'll see how I like it. If I don't
      like it, then I'll have you change it."

   FOUR KINDS OF SURFACE AND NO FIFTH. Every panel-like surface in CabDesign and
   StackDesign declares which kind it is with `data-bws-panel`; a surface that
   fits none of them is a design question, not a new kind.

     menu     — a short list of destinations or commands, anchored to the rail
                button that opened it. Overlays, never a scrim, transient.
     panel    — the slide-in. A working surface you keep open. RESERVES its own
                column; never a scrim; never covers the drawing.
     popover  — one transient reference or pick, anchored to its own control.
     dialog   — committing a set of decisions, or a destructive confirm. The one
                kind that carries a scrim and blocks the app.
     teaching — declared OUT of the contract on purpose (coach marks, the guided
                tour). Writing them down as out-of-scope is what stops a future
                session "standardizing" them (§7 row 13).

   THE ONE-LINE TEST (§6.0): does the person need to keep looking at their work
   while this is open? Yes -> panel or popover, no scrim. No, because they are
   committing or destroying something -> dialog.

   THE WIDTHS ARE NOT INVENTED (§6.3). Every number below is a width the product
   already shipped, promoted to a token so a reader learns three widths instead
   of eight: 302 is the rail flyout, 340 is the inspector/shortcuts dock column,
   390 is the AI dock, 420 is the cut-list peek's cap.

   THE MOTION IS NOT INVENTED EITHER (§6.5, §4.2). 220ms is what the AI dock and
   both library drawers already ship, and it sits inside the 200-300ms band NN/g
   gives a substantial screen change; the shorter 160ms exit and the two curves
   are NN/g's ("animating objects appearing... need a subtly longer duration than
   objects disappearing", ease-out entering, ease-in exiting).
   https://www.nngroup.com/articles/animation-duration/

   WHERE THIS LIVES AND WHY IT IS SHARED. §6.8 puts the contract in one file that
   every page loads, "so the grammar cannot fork per page" — the same shape that
   already works for shared/bws-modal.js. Wave 1 (this file's first consumer) is
   cabinet-designer.html; materials.html, hardware.html and StackDesign's
   assistant adopt it in waves 2 and 3 without a second copy of these numbers.
   ============================================================================ */

:root {
  /* --- The width scale (§6.3) ------------------------------------------- */
  --bws-panel-sm: 302px;   /* menu flyouts, and the cabinet-editor parts column */
  --bws-panel-md: 340px;   /* inspector, shortcuts, any reference panel        */
  --bws-panel-lg: 390px;   /* the AI dock, browse and edit panels              */
  --bws-pop-max: 420px;    /* the cap on any popover                           */

  /* --- The one motion speed (§6.5) -------------------------------------- */
  --bws-panel-in: 220ms;                       /* what the AI dock already ships */
  --bws-panel-out: 160ms;                      /* exits are shorter — NN/g       */
  --bws-ease-out: cubic-bezier(0.2, 0, 0, 1);  /* entering: fast, then settles   */
  --bws-ease-in: cubic-bezier(0.4, 0, 1, 1);   /* exiting: accelerates away      */

  /* --- The dialog cap (§6.6) -------------------------------------------- */
  /* A blocking surface still keeps the work framed: CabBuilder's measured
     67.5% x 83% proportion, widened for our narrower minimum viewport. */
  --bws-dialog-w: min(1180px, 78vw);
  --bws-dialog-h: min(880px, 84vh);
}

/* Reduced motion REPLACES the motion rather than merely shortening it (MDN's
   recommended pattern): the state change stays instant and COMPLETE — the panel
   still opens, it just does not travel. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --bws-panel-in: 0.01ms;
    --bws-panel-out: 0.01ms;
  }
}

/* ---------------------------------------------------------------------------
   THE ENTER MOTION, DECLARED PER SURFACE

   Only `transform` and `opacity` are animated (§6.5 rule 1). Never width, left,
   display or height — they force layout every frame, and "works really fast" is
   a frame-time claim before it is an easing claim.

   Kinds 1 and 3 enter as a 6-8px travel plus a fade, from the edge nearest their
   anchor (rule 2): a full-width slide on a 302px card reads as slow. Kind 2
   enters as a full translateX(100%) -> 0 from the edge it docks against.

   WHICH KIND-2 SURFACES TRAVEL, AND WHICH ONLY FADE. Rule 3's full slide belongs
   to a panel whose RESERVE travels with it: a fixed overlay panel sliding in while
   the canvas pads itself by the same width on the same curve. That is the AI dock,
   and it is the wave-2 shape for the Materials and Hardware drawers, so
   edge-left/edge-right stay defined here for them.

   A GRID-docked panel is a different animal, and pretending otherwise was measured
   twice. Its reserve is a grid track, and a track cannot travel - it simply exists.
   Start such a panel at translateX(100%) and for 220ms it is outside the viewport;
   start it 8px off and for 220ms its box is 8px from where it says it is. Both broke
   shipped assertions that read the panel's rectangle the instant it opens (the
   docked inspector inside the viewport, the narrow sheet flush to both edges), and
   both were the panel telling the truth about where it was. A rectangle is not a
   decoration to animate: it is what a pointer, a hit test and a screen reader all
   read. So the inspector, the shortcuts panel, the Catalog panel and the cabinet
   editor's parts column FADE into a column that is already theirs, and travel is
   left to the surfaces that overlay rather than occupy.

   THE PICK, RECORDED WITH ITS CONSTRAINT (the proposal leaves the mechanism
   open; this is the choice, made once):
     A surface whose closed state is `display: none` — which is every surface in
     the Designer that toggles the `hidden` attribute — ENTERS with motion and
     LEAVES instantly. It cannot spend `--bws-panel-out`, because the attribute
     cannot be deferred behind an exit:
       - tests/e2e/journeys/designer-hidden-integrity.test.mjs requires EVERY
         element carrying the hidden attribute to compute display:none, so the
         closed state may not be faked with visibility or opacity; and
       - tests/e2e/journeys/designer-front-styles.test.mjs (d) reads
         `.hidden === true` 100ms after Escape, so the attribute may not be set
         160ms late either.
     A surface that never leaves the box model — the AI dock, which slides on a
     transform — honours BOTH tokens. That is why --bws-panel-out is consumed
     rather than decorative.

   A one-shot `animation` (not a transition) is what runs the enter: display
   flipping away from `none` starts it, so there is no JS state, no timer, and no
   second thing to keep in step with the `hidden` attribute.
   --------------------------------------------------------------------------- */

@keyframes bws-enter-from-left  { from { opacity: 0; transform: translateX(-8px); } to { opacity: 1; transform: none; } }
@keyframes bws-enter-from-right { from { opacity: 0; transform: translateX(8px);  } to { opacity: 1; transform: none; } }
@keyframes bws-enter-from-above { from { opacity: 0; transform: translateY(-8px); } to { opacity: 1; transform: none; } }
@keyframes bws-enter-from-below { from { opacity: 0; transform: translateY(8px);  } to { opacity: 1; transform: none; } }
@keyframes bws-enter-edge-right { from { transform: translateX(100%);  } to { transform: none; } }
@keyframes bws-enter-edge-left  { from { transform: translateX(-100%); } to { transform: none; } }
@keyframes bws-enter-dialog     { from { opacity: 0; transform: scale(0.98); } to { opacity: 1; transform: none; } }
@keyframes bws-enter-fade       { from { opacity: 0; } to { opacity: 1; } }

/* NOTHING ANIMATES ON FIRST PAINT (§6.5 rule 5). A panel restored open at boot is
   simply open, so the whole arm is gated on a flag the page sets one frame after
   it is laid out. */
:root[data-bws-motion="on"] [data-bws-enter="left"]       { animation: bws-enter-from-left  var(--bws-panel-in) var(--bws-ease-out) both; }
:root[data-bws-motion="on"] [data-bws-enter="right"]      { animation: bws-enter-from-right var(--bws-panel-in) var(--bws-ease-out) both; }
:root[data-bws-motion="on"] [data-bws-enter="above"]      { animation: bws-enter-from-above var(--bws-panel-in) var(--bws-ease-out) both; }
:root[data-bws-motion="on"] [data-bws-enter="below"]      { animation: bws-enter-from-below var(--bws-panel-in) var(--bws-ease-out) both; }
:root[data-bws-motion="on"] [data-bws-enter="edge-right"] { animation: bws-enter-edge-right var(--bws-panel-in) var(--bws-ease-out) both; }
:root[data-bws-motion="on"] [data-bws-enter="edge-left"]  { animation: bws-enter-edge-left  var(--bws-panel-in) var(--bws-ease-out) both; }
:root[data-bws-motion="on"] [data-bws-enter="dialog"]     { animation: bws-enter-dialog     var(--bws-panel-in) var(--bws-ease-out) both; }
:root[data-bws-motion="on"] [data-bws-enter="fade"]       { animation: bws-enter-fade       var(--bws-panel-in) var(--bws-ease-out) both; }

@media (prefers-reduced-motion: reduce) {
  :root[data-bws-motion="on"] [data-bws-enter] { animation: none; }
}

/* ---------------------------------------------------------------------------
   ELEVATION FOLLOWS THE KIND, NOT THE TASTE (§6.2)

   A docked kind-2 panel is flush: no radius, one border on the edge it meets the
   canvas at, NO shadow. Kinds 1 and 3 overlay, so they carry the radius, the one
   border and one shadow. These are written as the contract's floor; a page may
   place a surface, but it may not give a docked panel a drop shadow.

   Chrome is never brown (FD166). Content INSIDE a panel — swatches, the
   viewport, wood colours — is exempt permanently.

   WRITTEN, NOT ENFORCED HERE, and that is deliberate rather than lazy. "Docked"
   is not a property of the element: the same panel is a docked column above
   1200px and a fixed bottom sheet below it, and which one it is lives in the
   page's own breakpoint (`:root[data-catalog-panel="open"]` and its siblings).
   A shared selector claiming to know cannot know, and a rule that flattened a
   bottom sheet's radius at every width would be a cosmetic defect shipped in the
   name of consistency. The page states it per surface; this states the law.
   --------------------------------------------------------------------------- */

/* One occupant per edge, at every width (§6.3). Two occupants in one grid cell is
   how the cut-list peek became an invisible dead button (BUG-2026-08-05-02); the
   page enforces the pairing, this is the reminder that it is a law and not a
   repeated patch. */

/* ============================================================================
   THE SETUP WIZARD SHELL — one guided walk, the same shape on every product
   ----------------------------------------------------------------------------
   Founder, 2026-09-04, looking at the two products side by side:

     "For the StackDesign Wizard, I really hate how you made it so small. Why
      would you not make it look/feel like the one you built for CabDesign? ...
      Remember people will be using both products. This is something you should
      codify."

   Cards P1798 (the rebuild) and P1799 (this). The written contract is
   docs/design-system/setup-wizard-contract.md; this file is its mechanism.

   IT IS A `dialog`, NOT A FIFTH KIND. The contract above says four kinds and no
   fifth, and a surface that fits none of them is a design question rather than a
   new kind. A setup wizard fits one of the four cleanly on §6.0's own one-line
   test: you are COMMITTING A SET OF DECISIONS, not keeping an eye on your work.
   So it declares `data-bws-panel="dialog"`, carries the scrim, and takes the
   dialog cap from --bws-dialog-w/h like every other dialog. What is added here
   is not a kind: it is the INTERNAL grammar a dialog takes on when it becomes a
   walk — a numbered rail, one step at a time, and a Back/Continue footer.

   WHY THE NUMBERS LIVE HERE RATHER THAN IN A DOCUMENT. Both wizards existed for
   weeks with a document each and drifted to 1180px against 520px anyway. A page
   can ignore a document; it cannot ignore the token it reads its own width from.
   Every measurement below was already shipping in cabinet-designer.html — this
   promotes them, it does not invent a second set.

   WHAT A PRODUCT MAY VARY: its step list, everything inside the pane, its
   header sentence, and its per-step footer hint. What it may NOT vary: the
   shell's size, the rail, the footer's two buttons and their words, the "Step N
   of M" line, and what the close control promises.
   ============================================================================ */

:root {
  --bws-wiz-rail-w: 244px;      /* the left step rail, wide enough for a named step  */
  /* THE TIER IS A RULE, AND THE NUMBER IS THE PRODUCT'S. A setup wizard sits ABOVE every
     working surface it dims - docks, side panels, floating launchers - and BELOW two things:
     the page's own SPEECH (toasts, a progress card), because a toast raised BY the wizard that
     the wizard then covers is worse than no toast at all, and the page's own BLOCKING DIALOGS
     (bwsConfirm / bwsPrompt), because the wizard's own buttons summon them.

     THAT SECOND HALF WAS MISSING AND IT COST A P0 (P1798 review, 2026-09-04). The rule said
     "speech" and enumerated toasts and the progress card; a confirm is neither, so nobody counted
     it - and StackDesign's confirm sat at 300 under a wizard at 1400. The guide's own last step,
     Send it to the cut list, raises that confirm on every brand-new account, and it opened
     invisible and unclickable underneath the scrim. A worked example of the whole tier, on the
     StackDesign planner: wizard 1400, confirm 1450, progress card 1490, toasts 1500.

     The two products' stacks are different heights, so each sets this token to the number that
     satisfies the rule where it lives, rather than the file pretending one integer is true in
     both. tests/setup-wizard-tier.audit.cjs is what makes that checkable rather than remembered. */
  --bws-wiz-z: 1150;
  --bws-wiz-radius: 16px;       /* the panel's own corner, matched to the dialog cap */
  /* lint-ok-hex: a neutral scrim over a canvas, deliberately theme-independent —
     it dims a drawing, and a drawing is not themed. */
  --bws-wiz-scrim: rgba(0, 0, 0, 0.62);
  /* lint-ok-hex: neutral elevation, the same shadow every dialog in the suite carries */
  --bws-wiz-shadow: 0 24px 64px rgba(0, 0, 0, 0.28);
}

/* The overlay. It steals zero canvas width and costs nothing to lay out when it is
   closed, which is what keeps the standing no-layout-shift rule: opening and closing
   the wizard moves nothing on the page behind it.

   CLOSED IS THE `hidden` ATTRIBUTE, and it really computes to display:none. A closed
   state faked with visibility or opacity is a surface a screen reader still walks and
   a pointer can still hit; the Designer's own hidden-integrity journey refuses it. A
   page that would rather drive this from its own root attribute may still do so - it
   sets `hidden` in the same breath, so there is one fact and not two. */
.bws-wiz {
  position: fixed; inset: 0; z-index: var(--bws-wiz-z, 1150);
  display: flex;
  align-items: center; justify-content: center;
  padding: max(18px, 3vh) max(18px, 3vw);
}
.bws-wiz[hidden] { display: none; }
.bws-wiz-scrim { position: absolute; inset: 0; z-index: 0; background: var(--bws-wiz-scrim); }

/* The panel. THE ONE NUMBER THE FOUNDER WAS READING. It is capped rather than
   full-screen on purpose (§6.6): even the one blocking surface leaves the
   drawing framed around it. */
.bws-wiz-panel {
  position: relative; z-index: 1;
  width: var(--bws-dialog-w); height: var(--bws-dialog-h);
  display: flex; flex-direction: column;
  background: var(--surface); color: var(--text);
  border: 1px solid var(--border); border-radius: var(--bws-wiz-radius);
  box-shadow: var(--bws-wiz-shadow);
  overflow: hidden;
}

.bws-wiz-head {
  display: flex; align-items: flex-start; justify-content: space-between; gap: 16px;
  padding: 18px 22px; border-bottom: 1px solid var(--border); flex: none;
}
.bws-wiz-head-titles { min-width: 0; }
.bws-wiz-head h2 { margin: 0; font-size: 18px; color: var(--text); }
.bws-wiz-sub { margin: 4px 0 0; font-size: 12.5px; color: var(--text-muted); max-width: 900px; line-height: 1.45; }
.bws-wiz-head-actions { display: flex; align-items: center; gap: 10px; flex: none; }

.bws-wiz-body { flex: 1; min-height: 0; display: flex; }

/* The rail. ONE STEP AT A TIME, and the rail is how you see where you are in the
   whole walk without leaving the one you are on. */
.bws-wiz-rail {
  flex: 0 0 var(--bws-wiz-rail-w); min-height: 0; overflow-y: auto;
  border-right: 1px solid var(--border);
  padding: 12px 10px; background: var(--surface-2, var(--surface));
  display: flex; flex-direction: column; gap: 2px;
}
.bws-wiz-rail-head { margin: 2px 6px 3px; font-size: 10.5px; font-weight: 700; letter-spacing: 0.07em; text-transform: uppercase; color: var(--text-muted); }
.bws-wiz-progress { height: 4px; border-radius: 999px; background: var(--surface-hi, var(--border)); margin: 2px 6px 8px; overflow: hidden; }
.bws-wiz-progress-fill { height: 100%; border-radius: 999px; background: var(--accent); transition: width 0.3s cubic-bezier(0.32, 0.72, 0, 1); }
/* The one thing in this shell that moves on its own, so it is silenced with the rest. The two
   reduced-motion blocks above cover the panel tokens and [data-bws-enter]; a width transition is
   neither, which is how it was missed (P1798 review, P3). */
@media (prefers-reduced-motion: reduce) { .bws-wiz-progress-fill { transition: none; } }
.bws-wiz-step {
  display: flex; align-items: center; gap: 9px; width: 100%;
  padding: 9px 11px; border: 1px solid transparent; border-radius: 9px;
  background: transparent; color: var(--text); font: inherit; font-size: 13px; text-align: left;
  cursor: pointer;
}
.bws-wiz-step:hover { background: var(--surface-hi); }
.bws-wiz-step:focus-visible { outline: 2px solid var(--accent-text); outline-offset: 2px; }
/* P925: text on a --*-soft fill takes the -ink token, never -text — the soft fills
   stay light in every theme while -text resolves bright in the dark ones.

   TWO SELECTORS, BECAUSE THE TWO PRODUCTS SAY "HERE" DIFFERENTLY AND BOTH ARE RIGHT.
   CabDesign's rail is a real tablist over sections that are all in the DOM, so its
   aria-selected resolves. StackDesign's pane holds ONE step at a time, so a tab whose
   aria-controls names six absent bodies would be a promise the widget cannot keep — it
   uses aria-current="step" and marks the visual state in data-current (P1798 review, P2). */
.bws-wiz-step[aria-selected="true"],
.bws-wiz-step[data-current="true"] { background: var(--accent-soft); border-color: var(--accent); color: var(--accent-ink); font-weight: 600; }
.bws-wiz-num {
  flex: 0 0 auto; display: inline-flex; align-items: center; justify-content: center;
  width: 20px; height: 20px; border-radius: 999px;
  border: 1px solid var(--border); background: var(--surface);
  font-size: 11px; font-weight: 700; color: var(--text-muted);
}
.bws-wiz-step[aria-selected="true"] .bws-wiz-num,
.bws-wiz-step[data-current="true"] .bws-wiz-num { background: var(--on-accent-fill); border-color: var(--on-accent-fill); color: var(--on-accent); }
.bws-wiz-step[data-done="true"] .bws-wiz-num { background: var(--accent-soft); border-color: var(--accent); color: var(--accent-ink); }
.bws-wiz-step-label { flex: 1; min-width: 0; }

.bws-wiz-pane { flex: 1; min-height: 0; overflow-y: auto; padding: 22px 26px 40px; }

/* The footer. TWO BUTTONS AND A SENTENCE, and the sentence is the product's to
   write per step — it is the only place the footer varies. */
.bws-wiz-foot {
  display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap;
  padding: 12px 22px; border-top: 1px solid var(--border); flex: none;
  background: var(--surface-2, var(--surface));
}
.bws-wiz-foot[hidden] { display: none; }
.bws-wiz-foot-hint { font-size: 12px; color: var(--text-muted); }
.bws-wiz-foot-btns { display: flex; gap: 10px; margin-left: auto; }

/* Below the tablet floor the rail cannot hold its width beside a readable pane,
   so it lies down above the pane and the panel takes the whole viewport. Desktop
   and tablet are the target (founder, 2026-08-05); this is the floor, not a
   phone layout. */
@media (max-width: 720px) {
  .bws-wiz { padding: 0; }
  .bws-wiz-panel { width: 100vw; height: 100vh; border: 0; border-radius: 0; }
  .bws-wiz-body { flex-direction: column; }
  .bws-wiz-rail { flex: none; flex-direction: row; overflow-x: auto; border-right: 0; border-bottom: 1px solid var(--border); }
  .bws-wiz-progress { display: none; }
}

@media print { .bws-wiz { display: none !important; } }
