@fasl-work/caos-app-shell 0.20.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,38 @@
1
+ ## [0.22.000] - 2026-11-06
2
+
3
+ ### Added
4
+
5
+ - Add explicit `density="compact"` to `Tabs` and `tabDensity="compact"` to `CaseWorkbench`, preserving labels, keyboard behavior and overflow while reducing vertical navigation chrome for short viewports.
6
+
7
+ ### Fixed
8
+
9
+ - Keep six guided story steps on one horizontally scrollable row at phone, tablet and desktop widths (SHW-055, issue 183).
10
+
11
+ ### Adoption
12
+
13
+ - Products with short viewports may opt into compact tab rows; default density remains unchanged.
14
+
15
+ ## [0.21.000] - 2026-11-05
16
+
17
+ ### Added
18
+
19
+ - Add `ShellConfig.immersiveRoutes` for route-specific full-viewport story framing. The route keeps the skip link and product identity on the main landmark while the shell omits header and footer; phone layouts return to document scrolling.
20
+ - Add the bounded `railWidth` API to `WorkbenchLayout` and `CaseWorkbench`, with shared compact rail spacing for measured workbench layouts.
21
+
22
+ ### Fixed
23
+
24
+ - Make a filling `PlotCard` containing a `Stage` and a direct child marked `data-caos-plot-control` allocate drawing height to the stage while keeping the control reachable, with no product CSS override.
25
+
26
+ ### Adoption
27
+
28
+ - Products use `immersiveRoutes` and `railWidth` for guided full-screen stories and tuned desktop rails, then remove CSS overrides of shell-reserved classes.
29
+ ## [0.20.001] - 2026-11-05
30
+
31
+ ### Fixed
32
+
33
+ - Publish the package from the canonical CI build and verify registry SHA-512 integrity, preventing platform line endings from changing the release tarball.
34
+ - Normalize padded release versions when matching GitHub tags, and make retries safe for an already-published identical package.
35
+
1
36
  ## [0.20.000] - 2026-11-05
2
37
 
3
38
  ### Added
@@ -1294,3 +1329,17 @@ All notable changes to this product. Format: `X.XX.XXX` (display, see the worksp
1294
1329
 
1295
1330
  - All configured page routes remain reachable on mobile through a compact second header row. Links scroll horizontally inside the header, retain their active-page semantics and remain visible when reached by keyboard focus.
1296
1331
  - Real-browser navigation tests exercise all six routes by click and keyboard at 320, 390 and 1440 px, in EN/ES and both themes. They verify viewport containment and a header below 100 px so the main instrument retains most of the viewport.
1332
+ ## [0.21.000] - 2026-11-05
1333
+
1334
+ ### Added
1335
+
1336
+ - Add `ShellConfig.immersiveRoutes` for route-specific full-viewport story framing. The route keeps the skip link and product identity on the main landmark while the shell omits header and footer; phone layouts return to document scrolling.
1337
+ - Add the bounded `railWidth` API to `WorkbenchLayout` and `CaseWorkbench`, with shared compact rail spacing for measured workbench layouts.
1338
+
1339
+ ### Fixed
1340
+
1341
+ - Make a filling `PlotCard` containing a `Stage` and a direct child marked `data-caos-plot-control` allocate drawing height to the stage while keeping the control reachable, with no product CSS override.
1342
+
1343
+ ### Adoption
1344
+
1345
+ - Products use `immersiveRoutes` and `railWidth` for guided full-screen stories and tuned desktop rails, then remove CSS overrides of shell-reserved classes.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.20.000
1
+ 0.22.000
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { A as APP_ROUTE, a as AppShell, b as ArchTab, c as ArchitectureConfig, d as ArchitectureModal, L as LANDING_ROUTE, e as LANG_STORAGE_KEY, S as STANDARD_ROUTES, f as ShellConfig, g as ShellRoute, T as THEME_BOOT_SCRIPT, h as THEME_STORAGE_KEY, v as validateArchitectureConfig } from './keys-DDQGijwQ.js';
1
+ export { A as APP_ROUTE, a as AppShell, b as ArchTab, c as ArchitectureConfig, d as ArchitectureModal, L as LANDING_ROUTE, e as LANG_STORAGE_KEY, S as STANDARD_ROUTES, f as ShellConfig, g as ShellRoute, T as THEME_BOOT_SCRIPT, h as THEME_STORAGE_KEY, v as validateArchitectureConfig } from './keys-D-pSo8hG.js';
2
2
  import * as react from 'react';
3
3
  import { ReactNode, Component, ErrorInfo, ChangeEvent } from 'react';
4
4
  import * as zustand from 'zustand';
@@ -361,6 +361,8 @@ interface TabDef {
361
361
  declare const MAX_PEER_TABS = 6;
362
362
  interface TabsProps {
363
363
  tabs: TabDef[];
364
+ /** Explicit row density for short workbench viewports; `comfortable` preserves the default spacing. */
365
+ density?: 'comfortable' | 'compact';
364
366
  /** Uncontrolled: the first selected tab. */
365
367
  initial?: string;
366
368
  /** Controlled: the selected tab, owned by the app (for example a view held in the URL). */
@@ -380,7 +382,7 @@ interface TabsProps {
380
382
  * is rendered, inside an error boundary; a row wider than its box fades the hidden end. Controlled when `value` is
381
383
  * given (known shell defect 6). More than six peers is reported (rule 5).
382
384
  */
383
- declare function Tabs({ tabs, initial, value, onChange, ariaLabel, trailing }: TabsProps): react.JSX.Element;
385
+ declare function Tabs({ tabs, initial, value, onChange, ariaLabel, trailing, density }: TabsProps): react.JSX.Element;
384
386
 
385
387
  interface SubTabDef {
386
388
  id: string;
@@ -1169,6 +1171,8 @@ interface WorkbenchLayoutProps {
1169
1171
  /** Accessible name of the rail. */
1170
1172
  railLabel?: BiText;
1171
1173
  className?: string;
1174
+ /** Product-calibrated desktop rail width in CSS pixels. Keep within 150–360 px. */
1175
+ railWidth?: number;
1172
1176
  }
1173
1177
  /**
1174
1178
  * The App route of a CAOS product: a sized control rail beside the instrument, on the full viewport.
@@ -1178,7 +1182,7 @@ interface WorkbenchLayoutProps {
1178
1182
  * explicit width that never scrolls (rule 6), and an instrument marked `data-instrument` so the measured gate
1179
1183
  * can check that it covers at least 60% of the viewport, and Focus at least 80% (ADR-0082). Pair with `ShellConfig.contain`.
1180
1184
  */
1181
- declare function WorkbenchLayout({ rail, railHead, children, railLabel, className }: WorkbenchLayoutProps): react.JSX.Element;
1185
+ declare function WorkbenchLayout({ rail, railHead, children, railLabel, className, railWidth }: WorkbenchLayoutProps): react.JSX.Element;
1182
1186
 
1183
1187
  /** One configured regime of a case: a full parameter vector, never a cosmetic label (ADR-0016 §9). */
1184
1188
  interface VariantDef {
@@ -1232,6 +1236,10 @@ interface CaseWorkbenchProps {
1232
1236
  * as one node or as sections shown one at a time (ADR-0071 rule 6). */
1233
1237
  rail?: ReactNode | RailSection[];
1234
1238
  railLabel?: BiText;
1239
+ /** Desktop control-rail width, passed to WorkbenchLayout (150–360 px). */
1240
+ railWidth?: number;
1241
+ /** Density of the case-group tab row; compact preserves every tab while reducing vertical chrome. */
1242
+ tabDensity?: 'comfortable' | 'compact';
1235
1243
  /** The instrument: the case's own question groups (for example Model, Validation, Impact, Findings). */
1236
1244
  groups: WorkbenchGroup[];
1237
1245
  /** The comparison across the case's variants (one metric per regime; selecting one loads it). */
package/dist/index.js CHANGED
@@ -377,14 +377,15 @@ function Frame({ config, pathname, routed, children }) {
377
377
  }, [lang]);
378
378
  const archLabel = lang === "es" ? "Arquitectura / C\xF3mo funciona" : "Architecture / How it works";
379
379
  const fixed = config.contain === true || config.fixed === true || (config.fixedRoutes ?? []).some((p) => p === pathname || p !== "/" && pathname.startsWith(p.replace(/\/$/, "") + "/"));
380
+ const immersive = (config.immersiveRoutes ?? []).some((p) => p === pathname || p !== "/" && pathname.startsWith(p.replace(/\/$/, "") + "/"));
380
381
  const license = config.license ? pick(config.license, lang) : config.footer?.license?.[lang] ?? "";
381
382
  const brand = /* @__PURE__ */ jsxs(Fragment, { children: [
382
383
  /* @__PURE__ */ jsx("span", { className: "brand-mark", children: config.product.mark ?? /* @__PURE__ */ jsx(Boxes, { size: 18, "aria-hidden": "true" }) }),
383
384
  /* @__PURE__ */ jsx("span", { children: config.product.name })
384
385
  ] });
385
- return /* @__PURE__ */ jsxs("div", { className: config.contain ? "app-shell fixed contain" : fixed ? "app-shell fixed" : "app-shell", children: [
386
+ return /* @__PURE__ */ jsxs("div", { className: ["app-shell", config.contain || fixed || immersive ? "fixed" : "", config.contain ? "contain" : "", immersive ? "immersive" : ""].filter(Boolean).join(" "), children: [
386
387
  /* @__PURE__ */ jsx("a", { className: "skip-link", href: "#main", children: c.skip }),
387
- /* @__PURE__ */ jsx("header", { className: "site-header", children: /* @__PURE__ */ jsxs("div", { className: "header-inner", children: [
388
+ !immersive && /* @__PURE__ */ jsx("header", { className: "site-header", children: /* @__PURE__ */ jsxs("div", { className: "header-inner", children: [
388
389
  routed ? /* @__PURE__ */ jsx(NavLink, { to: "/", className: "brand", "aria-label": config.product.name, "data-brand": config.product.name, children: brand }) : /* @__PURE__ */ jsx("a", { href: "/", className: "brand", "aria-label": config.product.name, "data-brand": config.product.name, children: brand }),
389
390
  routes.length > 1 && /* @__PURE__ */ jsx(Nav, { routes, name: config.product.name, routed, pathname }),
390
391
  /* @__PURE__ */ jsxs("div", { className: "header-actions", children: [
@@ -398,8 +399,8 @@ function Frame({ config, pathname, routed, children }) {
398
399
  ] })
399
400
  ] }) }),
400
401
  config.architecture && archOpen && /* @__PURE__ */ jsx(ArchitectureModal, { config: config.architecture, onClose: () => setArchOpen(false) }),
401
- /* @__PURE__ */ jsx("main", { className: "page", id: "main", tabIndex: -1, children: /* @__PURE__ */ jsx(PanelBoundary, { panel: "page", children }) }),
402
- /* @__PURE__ */ jsx("footer", { className: "site-footer", children: /* @__PURE__ */ jsx("div", { className: "footer-inner", children: /* @__PURE__ */ jsxs("div", { className: "footer-meta", ref: footRef, children: [
402
+ /* @__PURE__ */ jsx("main", { className: "page", id: "main", tabIndex: -1, "data-brand": config.product.name, children: /* @__PURE__ */ jsx(PanelBoundary, { panel: "page", children }) }),
403
+ !immersive && /* @__PURE__ */ jsx("footer", { className: "site-footer", children: /* @__PURE__ */ jsx("div", { className: "footer-inner", children: /* @__PURE__ */ jsxs("div", { className: "footer-meta", ref: footRef, children: [
403
404
  /* @__PURE__ */ jsx("span", { children: config.product.name }),
404
405
  /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: "\xB7" }),
405
406
  /* @__PURE__ */ jsx("span", { children: c.complement }),
@@ -1049,7 +1050,7 @@ function ReferenceList({ ids, heading }) {
1049
1050
  ] });
1050
1051
  }
1051
1052
  var MAX_PEER_TABS = 6;
1052
- function Tabs({ tabs, initial, value, onChange, ariaLabel, trailing }) {
1053
+ function Tabs({ tabs, initial, value, onChange, ariaLabel, trailing, density = "comfortable" }) {
1053
1054
  const baseId = useId();
1054
1055
  const first = tabs[0]?.id ?? "";
1055
1056
  const memory = useContext(ViewMemoryContext);
@@ -1085,7 +1086,7 @@ function Tabs({ tabs, initial, value, onChange, ariaLabel, trailing }) {
1085
1086
  }
1086
1087
  }
1087
1088
  const current = tabs.find((t2) => t2.id === active) ?? tabs[0];
1088
- return /* @__PURE__ */ jsxs("div", { className: trailing === void 0 ? "tabs" : "tabs tabs-has-slot", children: [
1089
+ return /* @__PURE__ */ jsxs("div", { className: `${trailing === void 0 ? "tabs" : "tabs tabs-has-slot"}${density === "compact" ? " tabs-compact" : ""}`, children: [
1089
1090
  /* @__PURE__ */ jsx("div", { className: "tablist", role: "tablist", "aria-label": ariaLabel, ref: rowRef, children: tabs.map((tab, idx) => {
1090
1091
  const selected = tab.id === current?.id;
1091
1092
  return /* @__PURE__ */ jsx(
@@ -3773,10 +3774,13 @@ function RailBody({ rail, railHead, railLabel }) {
3773
3774
  ] }) : /* @__PURE__ */ jsx("div", { className: "caos-wb-rail-panel", children: rail })
3774
3775
  ] });
3775
3776
  }
3776
- function WorkbenchLayout({ rail, railHead, children, railLabel, className }) {
3777
+ function WorkbenchLayout({ rail, railHead, children, railLabel, className, railWidth }) {
3777
3778
  const lang = useShellLang();
3778
3779
  const label = pick(railLabel ?? { en: "Controls", es: "Controles" }, lang);
3779
- return /* @__PURE__ */ jsxs("div", { className: ["page-body", "wide", "caos-wb", className].filter(Boolean).join(" "), "data-workbench": "", children: [
3780
+ if (railWidth !== void 0 && (!Number.isFinite(railWidth) || railWidth < 150 || railWidth > 360)) {
3781
+ throw new RangeError("WorkbenchLayout.railWidth must be a finite value from 150 to 360 px");
3782
+ }
3783
+ return /* @__PURE__ */ jsxs("div", { className: ["page-body", "wide", "caos-wb", className].filter(Boolean).join(" "), style: railWidth === void 0 ? void 0 : { "--rail-w": `${railWidth}px` }, "data-workbench": "", "data-rail-width": railWidth, children: [
3780
3784
  /* @__PURE__ */ jsx("aside", { className: "caos-wb-rail", "aria-label": label, "data-rail": "", children: /* @__PURE__ */ jsx(RailBody, { rail, railHead, railLabel }) }),
3781
3785
  /* @__PURE__ */ jsx("section", { className: "caos-wb-main", "data-instrument": "", children })
3782
3786
  ] });
@@ -3857,7 +3861,7 @@ function panel(id, lane, provenance, content) {
3857
3861
  return /* @__PURE__ */ jsx("div", { className: "caos-cw-panel", "data-group": id, "data-lane": lane, "data-provenance": provenance, children: content });
3858
3862
  }
3859
3863
  function CaseWorkbench(props) {
3860
- const { caseId, cases, source, controls, replayOnly, noteFit, variants, rail, railLabel, groups, compare, context, group, onGroupChange, deepLinkView = true, ariaLabel, focus } = props;
3864
+ const { caseId, cases, source, controls, replayOnly, noteFit, variants, rail, railLabel, railWidth, tabDensity = "comfortable", groups, compare, context, group, onGroupChange, deepLinkView = true, ariaLabel, focus } = props;
3861
3865
  const lang = useShellLang();
3862
3866
  const focusCfg = focus === true ? {} : focus || null;
3863
3867
  const { show: showFocus, setFocus, enterRef, memory } = useFocusMode(focusCfg);
@@ -3937,14 +3941,15 @@ function CaseWorkbench(props) {
3937
3941
  }
3938
3942
  ) }) });
3939
3943
  }
3940
- return /* @__PURE__ */ jsx(ViewMemoryContext.Provider, { value: memory, children: /* @__PURE__ */ jsx(WorkbenchStateContext.Provider, { value: { stateKey, caseId, variantId, source }, children: /* @__PURE__ */ jsx(WorkbenchLayout, { rail: rail ?? null, railHead, railLabel, children: /* @__PURE__ */ jsx("div", { className: "caos-cw", ...instrumentData, children: /* @__PURE__ */ jsx(
3944
+ return /* @__PURE__ */ jsx(ViewMemoryContext.Provider, { value: memory, children: /* @__PURE__ */ jsx(WorkbenchStateContext.Provider, { value: { stateKey, caseId, variantId, source }, children: /* @__PURE__ */ jsx(WorkbenchLayout, { rail: rail ?? null, railHead, railLabel, railWidth, children: /* @__PURE__ */ jsx("div", { className: "caos-cw", ...instrumentData, children: /* @__PURE__ */ jsx(
3941
3945
  Tabs,
3942
3946
  {
3943
3947
  tabs,
3944
3948
  value: openGroup,
3945
3949
  onChange: selectGroup,
3946
3950
  ariaLabel: pick(ariaLabel ?? { en: "Case views", es: "Vistas del caso" }, lang),
3947
- trailing: enter || void 0
3951
+ trailing: enter || void 0,
3952
+ density: tabDensity
3948
3953
  }
3949
3954
  ) }) }) }) });
3950
3955
  }
@@ -94,6 +94,8 @@ interface ShellConfig {
94
94
  };
95
95
  /** Routes whose surface IS the viewport (ADR-0071 rule 1), matched exactly or by prefix for a non-root path. */
96
96
  fixedRoutes?: string[];
97
+ /** Routes that use the full frame without site chrome (for a guided, direct-link StoryFrame). */
98
+ immersiveRoutes?: string[];
97
99
  /** Every route fixed: a single-surface app (a hub, a one-page tool). */
98
100
  fixed?: boolean;
99
101
  /**
package/dist/keys.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { A as APP_ROUTE, L as LANDING_ROUTE, e as LANG_STORAGE_KEY, S as STANDARD_ROUTES, T as THEME_BOOT_SCRIPT, h as THEME_STORAGE_KEY } from './keys-DDQGijwQ.js';
1
+ export { A as APP_ROUTE, L as LANDING_ROUTE, e as LANG_STORAGE_KEY, S as STANDARD_ROUTES, T as THEME_BOOT_SCRIPT, h as THEME_STORAGE_KEY } from './keys-D-pSo8hG.js';
2
2
  import 'react';
3
3
  import './text-BlY47nIt.js';
4
4
  import 'zustand';
package/gate/run.mjs CHANGED
@@ -523,7 +523,13 @@ export async function runGate(o) {
523
523
  await next.click();
524
524
  await page.waitForTimeout(90);
525
525
  const earlyMotion = await page.evaluate(() => window.__caosGate.showcaseFacts().motion);
526
- await page.waitForTimeout(260);
526
+ // Wait for the actual CSS stage animation to finish instead of assuming it starts on the same frame as the click.
527
+ // On loaded runners style/layout can begin a few frames after Playwright's click promise resolves.
528
+ await page.waitForFunction(() => {
529
+ const target = document.querySelector('[data-story-transition] > .caos-story-stage');
530
+ const animations = target ? target.getAnimations().filter((animation) => animation.animationName === 'caos-story-stage-enter') : [];
531
+ return animations.length > 0 && animations.every((animation) => animation.playState === 'finished');
532
+ }, null, { timeout: 1500 }).catch(() => undefined);
527
533
  const lateMotion = await page.evaluate(() => window.__caosGate.showcaseFacts().motion);
528
534
  if (!earlyMotion || earlyMotion.animationName === 'none' || earlyMotion.animationName === 'auto' || earlyMotion.transform === 'none' || earlyMotion.transform === 'matrix(1, 0, 0, 1, 0, 0)') fail('G21', { route: first.route, mode }, 'story step has no visible eased stage transition');
529
535
  else if (Number.parseFloat(earlyMotion.animationDuration) * (earlyMotion.animationDuration.endsWith('ms') ? 1 : 1000) < 200) fail('G21', { route: first.route, mode }, `normal stage transition is shorter than 200 ms (${earlyMotion.animationDuration})`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fasl-work/caos-app-shell",
3
- "version": "0.20.0",
3
+ "version": "0.22.0",
4
4
  "description": "Shared web-app shell, workbench primitives, content primitives, design system and measured gate for the CAOS / Faena public apps (ADR-0016, ADR-0017, ADR-0071).",
5
5
  "license": "MIT",
6
6
  "author": "Felipe Santibáñez-Leal",
@@ -288,6 +288,7 @@
288
288
  "header-sep",
289
289
  "hint",
290
290
  "icon-btn",
291
+ "immersive",
291
292
  "katex",
292
293
  "l-en",
293
294
  "l-es",
@@ -321,6 +322,7 @@
321
322
  "tablist",
322
323
  "tabpanel",
323
324
  "tabs",
325
+ "tabs-compact",
324
326
  "tabs-has-slot",
325
327
  "tabs-slot",
326
328
  "th-refs",
@@ -652,6 +654,7 @@
652
654
  "header-sep",
653
655
  "hint",
654
656
  "icon-btn",
657
+ "immersive",
655
658
  "is-active",
656
659
  "katex",
657
660
  "l-en",
@@ -692,6 +695,7 @@
692
695
  "tablist",
693
696
  "tabpanel",
694
697
  "tabs",
698
+ "tabs-compact",
695
699
  "tabs-has-slot",
696
700
  "tabs-slot",
697
701
  "th-refs",
package/styles.css CHANGED
@@ -201,8 +201,11 @@ code, kbd { font-family: var(--font-mono); }
201
201
  .app-shell.fixed .site-footer { margin-top: 0; }
202
202
  .app-shell.fixed .site-footer .footer-inner { padding: 0.45rem 1.5rem; gap: 0.4rem; font-size: 0.74rem; line-height: 1.3; }
203
203
  .app-shell.fixed.contain > .page { display: flex; flex-direction: column; overflow: hidden; }
204
+ .app-shell.immersive { height: 100dvh; min-height: 100dvh; overflow: hidden; }
205
+ .app-shell.immersive > .page { display: flex; flex: 1 1 auto; min-height: 0; overflow: hidden; padding: 0; }
206
+ .app-shell.immersive > .page > .caos-story { width: 100%; height: 100%; flex: 1 1 auto; }
204
207
  /* A document route scrolls inside the main container; a workbench or a surface fills it. */
205
- .app-shell.fixed.contain > .page:has(> .page-body:not(.caos-wb):not(.caos-surface)) { display: block; overflow-y: auto; overscroll-behavior: contain; }
208
+ .app-shell.fixed.contain:not(.immersive) > .page:has(> .page-body:not(.caos-wb):not(.caos-surface)) { display: block; overflow-y: auto; overscroll-behavior: contain; }
206
209
  /* The chrome of a contained app spans the instrument's width with its inset, so at 2560 px the header and footer do
207
210
  not float in a 1200 px column above a full-width workbench. */
208
211
  .app-shell.fixed.contain .header-inner, .app-shell.fixed.contain .site-footer .footer-inner { max-width: var(--maxw-wide); padding-inline: var(--space-4); }
@@ -213,7 +216,9 @@ code, kbd { font-family: var(--font-mono); }
213
216
  .app-shell.fixed.contain { height: auto; min-height: 100dvh; overflow: visible; }
214
217
  /* Both forms of the rule above are released: the `:has()` form outranked a plain `> .page` and kept a document's
215
218
  `.page` a scroll container that never scrolls, so nothing in it could be sticky (G12, 0.8.0). */
216
- .app-shell.fixed.contain > .page, .app-shell.fixed.contain > .page:has(> .page-body:not(.caos-wb):not(.caos-surface)) { display: block; overflow: visible; }
219
+ .app-shell.fixed.contain:not(.immersive) > .page, .app-shell.fixed.contain:not(.immersive) > .page:has(> .page-body:not(.caos-wb):not(.caos-surface)) { display: block; overflow: visible; }
220
+ /* A stacked phone workbench is a document: its full instrument and controls must remain reachable by page scroll. */
221
+ .app-shell.fixed.contain:not(.immersive) > .page:has(> .caos-wb) { display: block; overflow: visible; }
217
222
  }
218
223
  @media (max-width: 760px) {
219
224
  /* ADR-0071: the route row shares the header row and owns the horizontal scroll; no second row above the
@@ -253,6 +258,18 @@ code, kbd { font-family: var(--font-mono); }
253
258
  .page-body.wide.caos-wb { padding-inline: var(--space-4); }
254
259
  .caos-wb > * { min-width: 0; }
255
260
  .app-shell.fixed.contain > .page > .caos-wb { flex: 1 1 auto; min-height: 0; }
261
+ /* A tuned rail is an API value; keep the compact desktop spacing and control labels within it. */
262
+ .caos-wb[data-rail-width] { gap: 0.4rem; }
263
+ .caos-wb[data-rail-width] .caos-wb-rail,
264
+ .caos-wb[data-rail-width] .caos-wb-rail-head,
265
+ .caos-wb[data-rail-width] .caos-wb-rail-panel { gap: 0.25rem; }
266
+ .caos-wb[data-rail-width] .caos-wb-rail { padding: 0.4rem; }
267
+ .caos-wb[data-rail-width] .caos-readout-row { gap: 0.2rem; }
268
+ .caos-wb[data-rail-width] .tablist { padding-bottom: 0; margin-bottom: 0; }
269
+ .caos-wb[data-rail-width] .tablist .tab { padding-block: 0.2rem; }
270
+ .caos-wb[data-rail-width] .subtabs { margin-block: 0.1rem; }
271
+ .caos-wb[data-rail-width] .subtablist { margin-bottom: 0; }
272
+ .caos-wb[data-rail-width] .subtab { padding-block: 0.18rem; }
256
273
 
257
274
  /* Surface (SurfacePage): a tab App that fills the viewport; its head and tab row hold their height and the open
258
275
  panel scrolls. The containment 15 to 19 products wrote for themselves (audit 2026-10-07, section 3). */
@@ -297,6 +314,8 @@ code, kbd { font-family: var(--font-mono); }
297
314
  .tablist::-webkit-scrollbar { display: none; }
298
315
  .tablist .tab, .tablist button { flex: 0 0 auto; white-space: nowrap; }
299
316
  .tab { display: inline-flex; align-items: center; gap: 0.35rem; padding: 0.5rem 1rem; border: 1px solid transparent; border-radius: var(--radius-md); background: transparent; color: var(--color-fg-subtle); font-size: var(--text-md); font-weight: 550; transition: color 0.15s, background-color 0.15s, border-color 0.15s; }
317
+ .tabs-compact > .tablist { margin-bottom: 0.15rem; padding-bottom: 0; }
318
+ .tabs-compact > .tablist .tab { padding-block: 0.2rem; }
300
319
  .tab:hover { color: var(--color-fg); background: var(--color-surface-2); }
301
320
  .tab.active { color: var(--color-accent); background: var(--color-accent-soft); border-color: color-mix(in oklab, var(--color-accent) 35%, transparent); font-weight: 650; }
302
321
  .tabpanel:focus, .subtabpanel:focus { outline: none; }
@@ -573,6 +592,8 @@ svg.lucide { flex: none; }
573
592
  .caos-cw > .tabs { display: flex; flex-direction: column; min-height: 0; flex: 1 1 auto; }
574
593
  .caos-cw > .tabs > .tablist { margin-bottom: 0.5rem; padding-bottom: var(--space-1); }
575
594
  .caos-cw > .tabs > .tablist .tab { padding: 0.38rem 0.85rem; }
595
+ .caos-cw > .tabs.tabs-compact > .tablist { margin-bottom: 0.15rem; padding-bottom: 0; }
596
+ .caos-cw > .tabs.tabs-compact > .tablist .tab { padding-block: 0.2rem; }
576
597
  .caos-cw > .tabs > .tabpanel:not([hidden]) { flex: 1 1 auto; min-height: 0; overflow: auto; display: flex; flex-direction: column; }
577
598
  .caos-cw-panel { display: flex; flex-direction: column; gap: 0.6rem; min-width: 0; flex: 1 1 auto; min-height: 0; }
578
599
  .caos-cw-panel-head { display: flex; justify-content: flex-end; }
@@ -641,6 +662,11 @@ svg.lucide { flex: none; }
641
662
  .caos-plot-note { margin: 0.4rem 0 0; font-size: 0.78rem; color: var(--color-fg-faint); }
642
663
  .caos-plot.fill { display: flex; flex-direction: column; flex: 1 1 auto; min-height: 0; }
643
664
  .caos-plot.fill > .caos-plot-body { flex: 1 1 0; min-height: 0; overflow: hidden; }
665
+ .caos-plot.fill > .caos-plot-body:has(> [data-caos-plot-control]) { display: flex; flex-direction: column; }
666
+ .caos-plot.fill > .caos-plot-body:has(> [data-caos-plot-control]) > .caos-stage { flex: 1 1 0; height: auto; min-height: 0; }
667
+ .caos-plot.fill > .caos-plot-body > [data-caos-plot-control] { flex: 0 0 auto; display: flex; align-items: center; flex-wrap: wrap; gap: 0.35rem 0.6rem; min-width: 0; min-height: 2rem; padding: 0.2rem 0.35rem; color: var(--color-fg-subtle); font-size: 0.78rem; font-variant-numeric: tabular-nums; }
668
+ .caos-plot.fill > .caos-plot-body > [data-caos-plot-control] input { flex: 1 1 100%; width: 100%; min-width: 8rem; margin: 0; accent-color: var(--color-accent); }
669
+ .caos-plot.fill > .caos-plot-body > [data-caos-plot-control] strong { color: var(--color-fg); font-weight: 600; }
644
670
  .caos-plot.fill > .caos-plot-body > .caos-table { height: 100%; }
645
671
  /* The note of a filling card keeps at most 35% of the card and scrolls beyond it, so the drawing keeps a floor
646
672
  (CAOS_APP_SHELL#95: a long note left plots of 10 to 90 px at 1280 x 800). */
@@ -953,10 +979,6 @@ svg.lucide { flex: none; }
953
979
 
954
980
  /* ADR-0082 showcase frame: one focused stage, one docked panel, readable step navigation and anchored scene notes. */
955
981
  .caos-story { display: flex; flex-direction: column; gap: 0; min-width: 0; min-height: 0; height: 100%; padding: 0; color: var(--color-fg); background: var(--color-bg); }
956
- body:has([data-story]) .app-shell > .site-header,
957
- body:has([data-story]) .app-shell > .site-footer { display: none !important; }
958
- body:has([data-story]) .app-shell > .page { display: flex; flex: 1 1 auto; min-height: 0; overflow: hidden; padding: 0; }
959
- body:has([data-story]) .app-shell > .page > .caos-story { width: 100%; height: 100%; flex: 1 1 auto; }
960
982
  .caos-story-header { display: flex; align-items: center; justify-content: space-between; gap: var(--space-4); min-width: 0; }
961
983
  .caos-story-heading { min-width: 0; }
962
984
  .caos-story-verb { display: block; color: var(--color-step-1); font: 700 var(--text-xs) var(--font-mono); letter-spacing: .09em; text-transform: uppercase; }
@@ -970,7 +992,8 @@ body:has([data-story]) .app-shell > .page > .caos-story { width: 100%; height: 1
970
992
  .caos-story-actions,.caos-story-controls { display: flex; align-items: center; gap: var(--space-2); }
971
993
  .caos-story button { min-height: 36px; padding: .35rem .65rem; color: var(--color-fg); background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-md); cursor: pointer; }
972
994
  .caos-story button:disabled { opacity: .55; cursor: not-allowed; }
973
- .caos-story-stepper { display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: var(--space-2); min-width: 0; }
995
+ .caos-story-actions button { min-height: clamp(28px, 2.5vw, 36px); padding: clamp(.15rem, .5vw, .35rem) clamp(.4rem, .8vw, .65rem); font-size: clamp(.75rem, .8vw, .9rem); line-height: 1.15; white-space: nowrap; }
996
+ .caos-story-stepper { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(120px, 1fr); grid-template-columns: none; gap: var(--space-2); min-width: 0; overflow-x: auto; }
974
997
  .caos-story-step { display: grid; grid-template-columns: auto minmax(0, 1fr); grid-template-rows: auto auto; align-items: center; column-gap: .55rem; min-width: 0; min-height: 40px; padding: .2rem .55rem; color: var(--color-fg-subtle); background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-md); text-decoration: none; }
975
998
  .caos-story-step:hover { color: var(--color-fg); text-decoration: none; }
976
999
  .caos-story-step.is-active { color: var(--color-fg); border-color: var(--color-accent); box-shadow: inset 3px 0 var(--color-accent); }
@@ -1009,7 +1032,10 @@ body:has([data-story]) .app-shell > .page > .caos-story { width: 100%; height: 1
1009
1032
  .caos-callout-trend.trend-down { color: var(--color-good); }
1010
1033
  .caos-callout-source { grid-column: 1 / -1; }
1011
1034
  @media (max-width: 760px) {
1012
- body:has([data-story]) .app-shell > .page { overflow-y: auto; }
1035
+ .app-shell.immersive { height: auto; min-height: 100dvh; overflow: visible; }
1036
+ .app-shell.immersive > .page { display: block; overflow: visible; }
1037
+ .app-shell.immersive > .page > .caos-story { height: auto; min-height: 100%; }
1038
+ .app-shell.immersive > .page { overflow: visible; }
1013
1039
  .caos-story { height: auto; min-height: 100%; }
1014
1040
  .caos-story-header { display: grid; grid-template-columns: minmax(0, 1fr); align-items: start; gap: .45rem; }
1015
1041
  .caos-story-heading { width: 100%; }